investing-algorithm-framework 6.9.1__py3-none-any.whl → 7.19.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of investing-algorithm-framework might be problematic. Click here for more details.

Files changed (192) hide show
  1. investing_algorithm_framework/__init__.py +147 -44
  2. investing_algorithm_framework/app/__init__.py +23 -6
  3. investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
  4. investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
  5. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  6. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  7. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  8. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  9. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  10. investing_algorithm_framework/app/app.py +1322 -707
  11. investing_algorithm_framework/app/context.py +196 -88
  12. investing_algorithm_framework/app/eventloop.py +590 -0
  13. investing_algorithm_framework/app/reporting/__init__.py +16 -5
  14. investing_algorithm_framework/app/reporting/ascii.py +57 -202
  15. investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
  16. investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
  17. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  18. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  19. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
  20. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  21. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  22. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
  23. investing_algorithm_framework/app/reporting/generate.py +100 -114
  24. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
  25. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
  26. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
  27. investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
  28. investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
  29. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
  30. investing_algorithm_framework/app/strategy.py +315 -175
  31. investing_algorithm_framework/app/task.py +5 -3
  32. investing_algorithm_framework/cli/cli.py +30 -12
  33. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
  34. investing_algorithm_framework/cli/initialize_app.py +20 -1
  35. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
  36. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  37. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  38. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
  39. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
  40. investing_algorithm_framework/create_app.py +3 -5
  41. investing_algorithm_framework/dependency_container.py +25 -39
  42. investing_algorithm_framework/domain/__init__.py +45 -38
  43. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  44. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  45. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  46. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  47. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  48. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  49. investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
  50. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  51. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  52. investing_algorithm_framework/domain/config.py +27 -0
  53. investing_algorithm_framework/domain/constants.py +6 -34
  54. investing_algorithm_framework/domain/data_provider.py +200 -56
  55. investing_algorithm_framework/domain/exceptions.py +34 -1
  56. investing_algorithm_framework/domain/models/__init__.py +10 -19
  57. investing_algorithm_framework/domain/models/base_model.py +0 -6
  58. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  59. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  60. investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
  61. investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
  62. investing_algorithm_framework/domain/models/order/order.py +34 -13
  63. investing_algorithm_framework/domain/models/order/order_status.py +1 -1
  64. investing_algorithm_framework/domain/models/order/order_type.py +1 -1
  65. investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
  66. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
  67. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
  68. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  69. investing_algorithm_framework/domain/models/position/position.py +9 -0
  70. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  71. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  72. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  73. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  74. investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
  75. investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
  76. investing_algorithm_framework/domain/models/time_frame.py +7 -0
  77. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  78. investing_algorithm_framework/domain/models/time_unit.py +63 -1
  79. investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
  80. investing_algorithm_framework/domain/models/trade/trade.py +56 -32
  81. investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
  82. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
  83. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
  84. investing_algorithm_framework/domain/order_executor.py +19 -0
  85. investing_algorithm_framework/domain/portfolio_provider.py +20 -1
  86. investing_algorithm_framework/domain/services/__init__.py +0 -13
  87. investing_algorithm_framework/domain/strategy.py +1 -29
  88. investing_algorithm_framework/domain/utils/__init__.py +5 -1
  89. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  90. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  91. investing_algorithm_framework/domain/utils/polars.py +17 -14
  92. investing_algorithm_framework/download_data.py +40 -10
  93. investing_algorithm_framework/infrastructure/__init__.py +13 -25
  94. investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
  95. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
  96. investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
  97. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  98. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  99. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
  100. investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
  101. investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
  102. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
  103. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
  104. investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
  105. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  106. investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
  107. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
  108. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
  109. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
  110. investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
  111. investing_algorithm_framework/services/__init__.py +105 -8
  112. investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
  113. investing_algorithm_framework/services/configuration_service.py +14 -4
  114. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  115. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  116. investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
  117. investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
  118. investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
  119. investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
  120. investing_algorithm_framework/services/metrics/generate.py +358 -0
  121. investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
  122. investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
  123. investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
  124. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  125. investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
  126. investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
  127. investing_algorithm_framework/services/metrics/trades.py +500 -0
  128. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  129. investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
  130. investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
  131. investing_algorithm_framework/services/order_service/order_service.py +9 -71
  132. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
  133. investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
  134. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
  135. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
  136. investing_algorithm_framework/services/repository_service.py +5 -2
  137. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  138. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
  139. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  140. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  141. investing_algorithm_framework/services/trade_service/__init__.py +7 -1
  142. investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
  143. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  144. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  145. investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
  146. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
  147. investing_algorithm_framework/app/reporting/evaluation.py +0 -243
  148. investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
  149. investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
  150. investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
  151. investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
  152. investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
  153. investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
  154. investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
  155. investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
  156. investing_algorithm_framework/domain/models/data_source.py +0 -21
  157. investing_algorithm_framework/domain/models/date_range.py +0 -64
  158. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
  159. investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
  160. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  161. investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
  162. investing_algorithm_framework/domain/services/market_service.py +0 -153
  163. investing_algorithm_framework/domain/services/observable.py +0 -51
  164. investing_algorithm_framework/domain/services/observer.py +0 -19
  165. investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
  166. investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
  167. investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
  168. investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
  169. investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
  170. investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
  171. investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
  172. investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
  173. investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
  174. investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
  175. investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
  176. investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
  177. investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
  178. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
  179. investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
  180. /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
  181. /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
  182. /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
  183. /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
  184. /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
  185. /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
  186. /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
  187. /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
  188. /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
  189. /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
  190. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
  191. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
  192. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,116 @@
1
+ from typing import Union
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import polars as pl
6
+
7
+ from investing_algorithm_framework.domain import OperationalException
8
+
9
+
10
+ def create_ohlcv_permutation(
11
+ data: Union[pd.DataFrame, pl.DataFrame],
12
+ start_index: int = 0,
13
+ seed: int | None = None,
14
+ ) -> Union[pd.DataFrame, pl.DataFrame]:
15
+ """
16
+ Create a permuted OHLCV dataset by shuffling relative price moves.
17
+
18
+ Args:
19
+ data: A single OHLCV DataFrame (pandas or polars)
20
+ with columns ['Open', 'High', 'Low', 'Close', 'Volume'].
21
+ For pandas: Datetime can be either
22
+ index or a 'Datetime' column. For polars: Datetime
23
+ must be a 'Datetime' column.
24
+ start_index: Index at which the permutation should begin
25
+ (bars before remain unchanged).
26
+ seed: Random seed for reproducibility.
27
+
28
+ Returns:
29
+ DataFrame of the same type (pandas or polars) with
30
+ permuted OHLCV values, preserving the datetime
31
+ structure (index vs column) of the input.
32
+ """
33
+
34
+ if start_index < 0:
35
+ raise OperationalException("start_index must be >= 0")
36
+
37
+ if seed is None:
38
+ seed = np.random.randint(0, 1_000_000)
39
+
40
+ np.random.seed(seed)
41
+ is_polars = isinstance(data, pl.DataFrame)
42
+
43
+ # Normalize input to pandas
44
+ if is_polars:
45
+ has_datetime_col = "Datetime" in data.columns
46
+ ohlcv_pd = data.to_pandas().copy()
47
+ if has_datetime_col:
48
+ time_index = pd.to_datetime(ohlcv_pd["Datetime"])
49
+ else:
50
+ time_index = np.arange(len(ohlcv_pd))
51
+ else:
52
+ has_datetime_col = "Datetime" in data.columns
53
+ if isinstance(data.index, pd.DatetimeIndex):
54
+ time_index = data.index
55
+ elif has_datetime_col:
56
+ time_index = pd.to_datetime(data["Datetime"])
57
+ else:
58
+ time_index = np.arange(len(data))
59
+ ohlcv_pd = data.copy()
60
+
61
+ # Prepare data
62
+ n_bars = len(ohlcv_pd)
63
+ perm_index = start_index + 1
64
+ perm_n = n_bars - perm_index
65
+
66
+ log_bars = np.log(ohlcv_pd[["Open", "High", "Low", "Close"]])
67
+
68
+ # Start bar
69
+ start_bar = log_bars.iloc[start_index].to_numpy()
70
+
71
+ # Relative series
72
+ rel_open = (log_bars["Open"] - log_bars["Close"].shift()).to_numpy()
73
+ rel_high = (log_bars["High"] - log_bars["Open"]).to_numpy()
74
+ rel_low = (log_bars["Low"] - log_bars["Open"]).to_numpy()
75
+ rel_close = (log_bars["Close"] - log_bars["Open"]).to_numpy()
76
+
77
+ # Shuffle independently
78
+ idx = np.arange(perm_n)
79
+ rel_high = rel_high[perm_index:][np.random.permutation(idx)]
80
+ rel_low = rel_low[perm_index:][np.random.permutation(idx)]
81
+ rel_close = rel_close[perm_index:][np.random.permutation(idx)]
82
+ rel_open = rel_open[perm_index:][np.random.permutation(idx)]
83
+
84
+ # Build permuted OHLC
85
+ perm_bars = np.zeros((n_bars, 4))
86
+ perm_bars[:start_index] = log_bars.iloc[:start_index].to_numpy()
87
+ perm_bars[start_index] = start_bar
88
+
89
+ for i in range(perm_index, n_bars):
90
+ k = i - perm_index
91
+ perm_bars[i, 0] = perm_bars[i - 1, 3] + rel_open[k] # Open
92
+ perm_bars[i, 1] = perm_bars[i, 0] + rel_high[k] # High
93
+ perm_bars[i, 2] = perm_bars[i, 0] + rel_low[k] # Low
94
+ perm_bars[i, 3] = perm_bars[i, 0] + rel_close[k] # Close
95
+
96
+ perm_bars = np.exp(perm_bars)
97
+
98
+ # Rebuild OHLCV
99
+ perm_df = pd.DataFrame(
100
+ perm_bars,
101
+ columns=["Open", "High", "Low", "Close"],
102
+ )
103
+ perm_df["Volume"] = ohlcv_pd["Volume"].values
104
+
105
+ # Restore datetime structure
106
+ if is_polars:
107
+ if has_datetime_col:
108
+ perm_df.insert(0, "Datetime", time_index)
109
+ return pl.from_pandas(perm_df)
110
+ else:
111
+ if isinstance(data.index, pd.DatetimeIndex):
112
+ perm_df.index = time_index
113
+ perm_df.index.name = data.index.name or "Datetime"
114
+ elif has_datetime_col:
115
+ perm_df.insert(0, "Datetime", time_index)
116
+ return perm_df
@@ -0,0 +1,297 @@
1
+ import math
2
+ from typing import List
3
+ from statistics import mean
4
+
5
+ from investing_algorithm_framework.domain import BacktestEvaluationFocus, \
6
+ BacktestDateRange, Backtest, BacktestMetrics, OperationalException
7
+
8
+
9
+ def normalize(value, min_val, max_val):
10
+ """
11
+ Normalize a value to a range [0, 1].
12
+ """
13
+ if value is None or math.isnan(value) or math.isinf(value):
14
+ return 0
15
+ if min_val == max_val:
16
+ return 0
17
+ return (value - min_val) / (max_val - min_val)
18
+
19
+
20
+ def compute_score(metrics, weights, ranges):
21
+ """
22
+ Compute a weighted score for the given metrics.
23
+
24
+ Args:
25
+ metrics: The metrics to evaluate.
26
+ weights: The weights to apply to each metric.
27
+ ranges: The min/max ranges for each metric.
28
+
29
+ Returns:
30
+ float: The computed score.
31
+ """
32
+ score = 0
33
+ for key, weight in weights.items():
34
+ if not hasattr(metrics, key):
35
+ continue
36
+ value = getattr(metrics, key)
37
+ if value is None or (
38
+ isinstance(value, float) and
39
+ (math.isnan(value) or math.isinf(value))
40
+ ):
41
+ continue
42
+ if key in ranges:
43
+ value = normalize(value, ranges[key][0], ranges[key][1])
44
+ score += weight * value
45
+ return score
46
+
47
+
48
+ def create_weights(
49
+ focus: BacktestEvaluationFocus | str | None = None,
50
+ custom_weights: dict | None = None,
51
+ ) -> dict:
52
+ """
53
+ Utility to generate weights dicts for ranking backtests.
54
+
55
+ This function does not assign weights to every possible performance
56
+ metric. Instead, it focuses on a curated subset of commonly relevant
57
+ ones (profitability, win rate, trade frequency, and risk-adjusted returns).
58
+ The rationale is to avoid overfitting ranking logic to noisy or redundant
59
+ statistics (e.g., monthly return breakdowns, best/worst trade), while
60
+ keeping the weighting system simple and interpretable.
61
+ Users who need fine-grained control can pass `custom_weights` to fully
62
+ override defaults.
63
+
64
+ Args:
65
+ focus (BacktestEvaluationFocus | str | None): The focus for ranking.
66
+ custom_weights (dict): Full override for weights (all metrics).
67
+ If provided, it takes precedence over presets.
68
+
69
+ Returns:
70
+ dict: A dictionary of weights for ranking backtests.
71
+ """
72
+ if focus is None:
73
+ focus = BacktestEvaluationFocus.BALANCED
74
+
75
+ weights = focus.get_weights()
76
+
77
+ # if full custom dict is given → override everything
78
+ if custom_weights is not None:
79
+ weights = {**weights, **custom_weights}
80
+
81
+ return weights
82
+
83
+
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]:
91
+ """
92
+ Rank backtest results based on specified focus, weights, and filters.
93
+
94
+ Args:
95
+ backtests (List[Backtest]): List of backtest results to rank.
96
+ focus (str, optional): Focus for ranking. If None,
97
+ uses default weights. Options: "balanced", "profit",
98
+ "frequency", "risk_adjusted".
99
+ weights (dict, optional): Custom weights for ranking metrics.
100
+ If None, uses default weights based on focus.
101
+ filter_fn (callable | dict, optional): A filter to apply to
102
+ backtests before ranking.
103
+ - If callable: receives metrics and should return True/False.
104
+ - If dict: mapping {metric_name: condition_fn},
105
+ all conditions must pass.
106
+ backtest_date_range (BacktestDateRange, optional): If provided,
107
+ only backtests matching this date range are considered.
108
+
109
+ Returns:
110
+ List[Backtest]: Sorted list of backtests based on computed scores.
111
+ """
112
+
113
+ if weights is None:
114
+ weights = create_weights(focus=focus)
115
+
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
128
+ if filter_fn is not None:
129
+ if callable(filter_fn):
130
+ paired = [
131
+ (bt, m) for bt, m in paired if filter_fn(m)
132
+ ]
133
+ elif isinstance(filter_fn, dict):
134
+ paired = [
135
+ (bt, m) for bt, m in paired
136
+ if all(
137
+ cond(getattr(m, key, None))
138
+ for key, cond in filter_fn.items()
139
+ )
140
+ ]
141
+
142
+ # Compute normalization ranges
143
+ ranges = {}
144
+ for key in weights:
145
+ values = [
146
+ getattr(m, key, None) for _, m in paired
147
+ ]
148
+ values = [
149
+ v for v in values
150
+ if isinstance(v, (int, float)) and v is not None
151
+ and not math.isnan(v) and not math.isinf(v)
152
+ ]
153
+ if values:
154
+ ranges[key] = (min(values), max(values))
155
+
156
+ # Sort Backtests by score
157
+ ranked = sorted(
158
+ paired,
159
+ key=lambda bm: compute_score(bm[1], weights, ranges),
160
+ reverse=True
161
+ )
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
+ total_growth=safe_mean([m.total_growth for m in backtest_metrics]),
199
+ total_growth_percentage=safe_mean(
200
+ [m.total_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
+ average_trade_gain=safe_mean(
232
+ [m.average_trade_gain for m in backtest_metrics]),
233
+ average_trade_gain_percentage=(
234
+ safe_mean(
235
+ [m.average_trade_gain_percentage for m in backtest_metrics]
236
+ )
237
+ ),
238
+ average_trade_loss=safe_mean(
239
+ [m.average_trade_loss for m in backtest_metrics]),
240
+ average_trade_loss_percentage=(
241
+ safe_mean(
242
+ [m.average_trade_loss_percentage for m in backtest_metrics]
243
+ )
244
+ ),
245
+ median_trade_return=safe_mean(
246
+ [m.median_trade_return for m in backtest_metrics]),
247
+ median_trade_return_percentage=(
248
+ safe_mean(
249
+ [m.median_trade_return_percentage for m in backtest_metrics]
250
+ )
251
+ ),
252
+ best_trade=max((
253
+ m.best_trade for m in backtest_metrics if m.best_trade),
254
+ key=lambda t: t.net_gain if t else float('-inf'),
255
+ default=None
256
+ ),
257
+ worst_trade=min(
258
+ (m.worst_trade for m in backtest_metrics if m.worst_trade),
259
+ key=lambda t: t.net_gain if t else float('inf'),
260
+ default=None
261
+ ),
262
+ average_trade_duration=safe_mean(
263
+ [m.average_trade_duration for m in backtest_metrics]),
264
+ number_of_trades=sum(m.number_of_trades for m in backtest_metrics),
265
+ win_rate=safe_mean([m.win_rate for m in backtest_metrics]),
266
+ win_loss_ratio=safe_mean([m.win_loss_ratio for m in backtest_metrics]),
267
+ percentage_winning_months=safe_mean(
268
+ [m.percentage_winning_months for m in backtest_metrics]),
269
+ percentage_winning_years=safe_mean(
270
+ [m.percentage_winning_years for m in backtest_metrics]),
271
+ average_monthly_return=safe_mean(
272
+ [m.average_monthly_return for m in backtest_metrics]),
273
+ average_monthly_return_losing_months=safe_mean(
274
+ [m.average_monthly_return_losing_months for m in backtest_metrics]
275
+ ),
276
+ average_monthly_return_winning_months=safe_mean(
277
+ [m.average_monthly_return_winning_months for m in backtest_metrics]
278
+ ),
279
+ best_month=max(
280
+ (m.best_month for m in backtest_metrics if m.best_month),
281
+ key=lambda x: x[0] if x else float('-inf'),
282
+ default=None
283
+ ),
284
+ best_year=max((m.best_year for m in backtest_metrics if m.best_year),
285
+ key=lambda x: x[0] if x else float('-inf'),
286
+ default=None),
287
+ worst_month=min(
288
+ (m.worst_month for m in backtest_metrics if m.worst_month),
289
+ key=lambda x: x[0] if x else float('inf'),
290
+ default=None
291
+ ),
292
+ worst_year=min(
293
+ (m.worst_year for m in backtest_metrics if m.worst_year),
294
+ key=lambda x: x[0] if x else float('inf'),
295
+ default=None
296
+ ),
297
+ )