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.
- investing_algorithm_framework/__init__.py +147 -44
- investing_algorithm_framework/app/__init__.py +23 -6
- investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +1322 -707
- investing_algorithm_framework/app/context.py +196 -88
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +16 -5
- investing_algorithm_framework/app/reporting/ascii.py +57 -202
- investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
- investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
- investing_algorithm_framework/app/reporting/generate.py +100 -114
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
- investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
- investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
- investing_algorithm_framework/app/strategy.py +315 -175
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/cli/cli.py +30 -12
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
- investing_algorithm_framework/cli/initialize_app.py +20 -1
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
- investing_algorithm_framework/create_app.py +3 -5
- investing_algorithm_framework/dependency_container.py +25 -39
- investing_algorithm_framework/domain/__init__.py +45 -38
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +27 -0
- investing_algorithm_framework/domain/constants.py +6 -34
- investing_algorithm_framework/domain/data_provider.py +200 -56
- investing_algorithm_framework/domain/exceptions.py +34 -1
- investing_algorithm_framework/domain/models/__init__.py +10 -19
- investing_algorithm_framework/domain/models/base_model.py +0 -6
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
- investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +34 -13
- investing_algorithm_framework/domain/models/order/order_status.py +1 -1
- investing_algorithm_framework/domain/models/order/order_type.py +1 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +9 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
- investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
- investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
- investing_algorithm_framework/domain/models/time_frame.py +7 -0
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +63 -1
- investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
- investing_algorithm_framework/domain/models/trade/trade.py +56 -32
- investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
- investing_algorithm_framework/domain/order_executor.py +19 -0
- investing_algorithm_framework/domain/portfolio_provider.py +20 -1
- investing_algorithm_framework/domain/services/__init__.py +0 -13
- investing_algorithm_framework/domain/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +5 -1
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +17 -14
- investing_algorithm_framework/download_data.py +40 -10
- investing_algorithm_framework/infrastructure/__init__.py +13 -25
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
- investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
- investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
- investing_algorithm_framework/services/__init__.py +105 -8
- investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
- investing_algorithm_framework/services/configuration_service.py +14 -4
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
- investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
- investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
- investing_algorithm_framework/services/order_service/order_service.py +9 -71
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
- investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
- investing_algorithm_framework/services/repository_service.py +5 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
- investing_algorithm_framework/services/trade_service/__init__.py +7 -1
- investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
- investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
- investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
- investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
- investing_algorithm_framework/app/reporting/evaluation.py +0 -243
- investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
- investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
- investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
- investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
- investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
- investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
- investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
- investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
- investing_algorithm_framework/domain/models/data_source.py +0 -21
- investing_algorithm_framework/domain/models/date_range.py +0 -64
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
- investing_algorithm_framework/domain/services/market_service.py +0 -153
- investing_algorithm_framework/domain/services/observable.py +0 -51
- investing_algorithm_framework/domain/services/observer.py +0 -19
- investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
- investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
- investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
- investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
- investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
- investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
- investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
- investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
- investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
- investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
- investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
- investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
- investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
- investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
- /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
- {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
|
+
)
|