11import math
2+ from typing import List
3+ from statistics import mean
24
3- from investing_algorithm_framework .domain import BacktestEvaluationFocus
4- BacktestEvaluationFocus
5+ from investing_algorithm_framework .domain import BacktestEvaluationFocus , \
6+ BacktestDateRange , Backtest , BacktestMetrics , OperationalException
57
68
79def normalize (value , min_val , max_val ):
@@ -61,9 +63,6 @@ def create_weights(
6163
6264 Args:
6365 focus (BacktestEvaluationFocus | str | None): The focus for ranking.
64- gain (float): Weight for total_net_gain (default only).
65- win_rate (float): Weight for win_rate (default only).
66- trades (float): Weight for number_of_trades (default only).
6766 custom_weights (dict): Full override for weights (all metrics).
6867 If provided, it takes precedence over presets.
6968
@@ -82,12 +81,18 @@ def create_weights(
8281 return weights
8382
8483
85- def rank_results (backtests , focus = None , weights = None , filter_fn = None ):
84+ def rank_results (
85+ backtests : List [Backtest ],
86+ focus = None ,
87+ weights = None ,
88+ filter_fn = None ,
89+ backtest_date_range : BacktestDateRange = None
90+ ) -> List [Backtest ]:
8691 """
8792 Rank backtest results based on specified focus, weights, and filters.
8893
8994 Args:
90- backtests (list ): List of backtest results to rank.
95+ backtests (List[Backtest] ): List of backtest results to rank.
9196 focus (str, optional): Focus for ranking. If None,
9297 uses default weights. Options: "balanced", "profit",
9398 "frequency", "risk_adjusted".
@@ -98,45 +103,178 @@ def rank_results(backtests, focus=None, weights=None, filter_fn=None):
98103 - If callable: receives metrics and should return True/False.
99104 - If dict: mapping {metric_name: condition_fn},
100105 all conditions must pass.
106+ backtest_date_range (BacktestDateRange, optional): If provided,
107+ only backtests matching this date range are considered.
101108
102109 Returns:
103- list : Sorted list of backtests based on computed scores.
110+ List[Backtest] : Sorted list of backtests based on computed scores.
104111 """
105112
106113 if weights is None :
107114 weights = create_weights (focus = focus )
108115
109- # Apply filtering
116+ # Pair backtests with their metrics
117+ paired = []
118+ for backtest in backtests :
119+ if backtest_date_range is not None :
120+ metrics = backtest .get_backtest_metrics (backtest_date_range )
121+ else :
122+ metrics = backtest .backtest_summary
123+
124+ if metrics is not None :
125+ paired .append ((backtest , metrics ))
126+
127+ # Apply filtering on metrics
110128 if filter_fn is not None :
111129 if callable (filter_fn ):
112- backtests = [
113- bt for bt in backtests
114- if filter_fn (bt .backtest_metrics )
130+ paired = [
131+ (bt , m ) for bt , m in paired if filter_fn (m )
115132 ]
116133 elif isinstance (filter_fn , dict ):
117- backtests = [
118- bt for bt in backtests
134+ paired = [
135+ ( bt , m ) for bt , m in paired
119136 if all (
120- cond (getattr (bt . backtest_metrics , key , None ))
137+ cond (getattr (m , key , None ))
121138 for key , cond in filter_fn .items ()
122139 )
123140 ]
124141
125- # First compute metric ranges for normalization
142+ # Compute normalization ranges
126143 ranges = {}
127144 for key in weights :
128- values = [getattr (bt .backtest_metrics , key , None ) for bt in backtests ]
145+ values = [
146+ getattr (m , key , None ) for _ , m in paired
147+ ]
129148 values = [
130149 v for v in values
131150 if isinstance (v , (int , float )) and v is not None
132151 and not math .isnan (v ) and not math .isinf (v )
133152 ]
134-
135153 if values :
136154 ranges [key ] = (min (values ), max (values ))
137155
138- return sorted (
139- backtests ,
140- key = lambda bt : compute_score (bt .backtest_metrics , weights , ranges ),
156+ # Sort Backtests by score
157+ ranked = sorted (
158+ paired ,
159+ key = lambda bm : compute_score (bm [1 ], weights , ranges ),
141160 reverse = True
142161 )
162+
163+ return [bt for bt , _ in ranked ]
164+
165+
166+ def combine_backtest_metrics (
167+ backtest_metrics : List [BacktestMetrics ]
168+ ) -> BacktestMetrics :
169+ """
170+ Combine backtest metrics from multiple backtests into a single list.
171+
172+ Args:
173+ backtest_metrics (List[BacktestMetrics]): List of backtest
174+ metrics to combine.
175+
176+ Returns:
177+ BacktestMetrics: Combined list of backtest metrics.
178+ """
179+ if not backtest_metrics :
180+ raise OperationalException ("No BacktestMetrics provided" )
181+
182+ # Helper to take mean safely
183+
184+ def safe_mean (values ):
185+ vals = [v for v in values if v is not None ]
186+ return mean (vals ) if vals else 0.0
187+
188+ # Dates
189+
190+ start_date = min (m .backtest_start_date for m in backtest_metrics )
191+ end_date = max (m .backtest_end_date for m in backtest_metrics )
192+
193+ # Aggregate
194+ return BacktestMetrics (
195+ backtest_start_date = start_date ,
196+ backtest_end_date = end_date ,
197+ equity_curve = [], # leave empty to avoid misleading curves
198+ growth = safe_mean ([m .growth for m in backtest_metrics ]),
199+ growth_percentage = safe_mean (
200+ [m .growth_percentage for m in backtest_metrics ]),
201+ total_net_gain = safe_mean ([m .total_net_gain for m in backtest_metrics ]),
202+ total_net_gain_percentage = safe_mean (
203+ [m .total_net_gain_percentage for m in backtest_metrics ]),
204+ final_value = safe_mean ([m .final_value for m in backtest_metrics ]),
205+ cagr = safe_mean ([m .cagr for m in backtest_metrics ]),
206+ sharpe_ratio = safe_mean ([m .sharpe_ratio for m in backtest_metrics ]),
207+ rolling_sharpe_ratio = [],
208+ sortino_ratio = safe_mean ([m .sortino_ratio for m in backtest_metrics ]),
209+ calmar_ratio = safe_mean ([m .calmar_ratio for m in backtest_metrics ]),
210+ profit_factor = safe_mean ([m .profit_factor for m in backtest_metrics ]),
211+ gross_profit = sum (m .gross_profit or 0 for m in backtest_metrics ),
212+ gross_loss = sum (m .gross_loss or 0 for m in backtest_metrics ),
213+ annual_volatility = safe_mean (
214+ [m .annual_volatility for m in backtest_metrics ]),
215+ monthly_returns = [],
216+ yearly_returns = [],
217+ drawdown_series = [],
218+ max_drawdown = max (m .max_drawdown for m in backtest_metrics ),
219+ max_drawdown_absolute = max (
220+ m .max_drawdown_absolute for m in backtest_metrics ),
221+ max_daily_drawdown = max (m .max_daily_drawdown for m in backtest_metrics ),
222+ max_drawdown_duration = max (
223+ m .max_drawdown_duration for m in backtest_metrics ),
224+ trades_per_year = safe_mean (
225+ [m .trades_per_year for m in backtest_metrics ]
226+ ),
227+ trade_per_day = safe_mean ([m .trade_per_day for m in backtest_metrics ]),
228+ exposure_ratio = safe_mean (
229+ [m .exposure_ratio for m in backtest_metrics ]
230+ ),
231+ trades_average_gain = safe_mean (
232+ [m .trades_average_gain for m in backtest_metrics ]),
233+ trades_average_loss = safe_mean (
234+ [m .trades_average_loss for m in backtest_metrics ]),
235+ best_trade = max ((
236+ m .best_trade for m in backtest_metrics if m .best_trade ),
237+ key = lambda t : t .net_gain if t else float ('-inf' ),
238+ default = None
239+ ),
240+ worst_trade = min (
241+ (m .worst_trade for m in backtest_metrics if m .worst_trade ),
242+ key = lambda t : t .net_gain if t else float ('inf' ),
243+ default = None
244+ ),
245+ average_trade_duration = safe_mean (
246+ [m .average_trade_duration for m in backtest_metrics ]),
247+ number_of_trades = sum (m .number_of_trades for m in backtest_metrics ),
248+ win_rate = safe_mean ([m .win_rate for m in backtest_metrics ]),
249+ win_loss_ratio = safe_mean ([m .win_loss_ratio for m in backtest_metrics ]),
250+ percentage_winning_months = safe_mean (
251+ [m .percentage_winning_months for m in backtest_metrics ]),
252+ percentage_winning_years = safe_mean (
253+ [m .percentage_winning_years for m in backtest_metrics ]),
254+ average_monthly_return = safe_mean (
255+ [m .average_monthly_return for m in backtest_metrics ]),
256+ average_monthly_return_losing_months = safe_mean (
257+ [m .average_monthly_return_losing_months for m in backtest_metrics ]
258+ ),
259+ average_monthly_return_winning_months = safe_mean (
260+ [m .average_monthly_return_winning_months for m in backtest_metrics ]
261+ ),
262+ best_month = max (
263+ (m .best_month for m in backtest_metrics if m .best_month ),
264+ key = lambda x : x [0 ] if x else float ('-inf' ),
265+ default = None
266+ ),
267+ best_year = max ((m .best_year for m in backtest_metrics if m .best_year ),
268+ key = lambda x : x [0 ] if x else float ('-inf' ),
269+ default = None ),
270+ worst_month = min (
271+ (m .worst_month for m in backtest_metrics if m .worst_month ),
272+ key = lambda x : x [0 ] if x else float ('inf' ),
273+ default = None
274+ ),
275+ worst_year = min (
276+ (m .worst_year for m in backtest_metrics if m .worst_year ),
277+ key = lambda x : x [0 ] if x else float ('inf' ),
278+ default = None
279+ ),
280+ )
0 commit comments