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,242 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BacktestEvaluationFocus(Enum):
|
|
5
|
+
"""
|
|
6
|
+
Enumeration for backtest evaluation focus areas.
|
|
7
|
+
|
|
8
|
+
The available metrics are:
|
|
9
|
+
- backtest_start_date
|
|
10
|
+
- backtest_end_date
|
|
11
|
+
- equity_curve
|
|
12
|
+
- final_value
|
|
13
|
+
- total_growth
|
|
14
|
+
- total_growth_percentage
|
|
15
|
+
- total_net_gain
|
|
16
|
+
- total_net_gain_percentage
|
|
17
|
+
- total_loss
|
|
18
|
+
- total_loss_percentage
|
|
19
|
+
- cumulative_return
|
|
20
|
+
- cumulative_return_series
|
|
21
|
+
- cagr
|
|
22
|
+
- sharpe_ratio
|
|
23
|
+
- rolling_sharpe_ratio
|
|
24
|
+
- sortino_ratio
|
|
25
|
+
- calmar_ratio
|
|
26
|
+
- profit_factor
|
|
27
|
+
- annual_volatility
|
|
28
|
+
- monthly_returns
|
|
29
|
+
- yearly_returns
|
|
30
|
+
- drawdown_series
|
|
31
|
+
- max_drawdown
|
|
32
|
+
- max_drawdown_absolute
|
|
33
|
+
- max_daily_drawdown
|
|
34
|
+
- max_drawdown_duration
|
|
35
|
+
- trades_per_year
|
|
36
|
+
- trade_per_day
|
|
37
|
+
- exposure_ratio
|
|
38
|
+
- cumulative_exposure
|
|
39
|
+
- best_trade
|
|
40
|
+
- worst_trade
|
|
41
|
+
- number_of_positive_trades
|
|
42
|
+
- percentage_positive_trades
|
|
43
|
+
- number_of_negative_trades
|
|
44
|
+
- percentage_negative_trades
|
|
45
|
+
- average_trade_duration
|
|
46
|
+
- average_trade_size
|
|
47
|
+
- average_trade_loss
|
|
48
|
+
- average_trade_loss_percentage
|
|
49
|
+
- average_trade_gain
|
|
50
|
+
- average_trade_gain_percentage
|
|
51
|
+
- average_trade_return
|
|
52
|
+
- average_trade_return_percentage
|
|
53
|
+
- median_trade_return
|
|
54
|
+
- number_of_trades
|
|
55
|
+
- number_of_trades_closed
|
|
56
|
+
- number_of_trades_opened
|
|
57
|
+
- number_of_trades_open_at_end
|
|
58
|
+
- win_rate
|
|
59
|
+
- current_win_rate
|
|
60
|
+
- win_loss_ratio
|
|
61
|
+
- current_win_loss_ratio
|
|
62
|
+
- percentage_winning_months
|
|
63
|
+
- percentage_winning_years
|
|
64
|
+
- average_monthly_return
|
|
65
|
+
- average_monthly_return_losing_months
|
|
66
|
+
- average_monthly_return_winning_months
|
|
67
|
+
- best_month
|
|
68
|
+
- best_year
|
|
69
|
+
- worst_month
|
|
70
|
+
- worst_year
|
|
71
|
+
- total_number_of_days
|
|
72
|
+
- current_average_trade_gain
|
|
73
|
+
- current_average_trade_return
|
|
74
|
+
- current_average_trade_duration
|
|
75
|
+
- current_average_trade_loss
|
|
76
|
+
"""
|
|
77
|
+
BALANCED = "balanced"
|
|
78
|
+
PROFIT = "profit"
|
|
79
|
+
FREQUENCY = "frequency"
|
|
80
|
+
RISK_ADJUSTED = "risk_adjusted"
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def from_string(value: str):
|
|
84
|
+
|
|
85
|
+
if isinstance(value, str):
|
|
86
|
+
|
|
87
|
+
for entry in BacktestEvaluationFocus:
|
|
88
|
+
|
|
89
|
+
if value.upper() == entry.value:
|
|
90
|
+
return entry
|
|
91
|
+
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Could not convert {value} to BacktestEvaluationFocus"
|
|
94
|
+
)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def from_value(value):
|
|
99
|
+
|
|
100
|
+
if isinstance(value, str):
|
|
101
|
+
return BacktestEvaluationFocus.from_string(value)
|
|
102
|
+
|
|
103
|
+
if isinstance(value, BacktestEvaluationFocus):
|
|
104
|
+
|
|
105
|
+
for entry in BacktestEvaluationFocus:
|
|
106
|
+
|
|
107
|
+
if value == entry:
|
|
108
|
+
return entry
|
|
109
|
+
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"Could not convert {value} to BacktestEvaluationFocus"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def equals(self, other):
|
|
115
|
+
|
|
116
|
+
if isinstance(other, Enum):
|
|
117
|
+
return self.value == other.value
|
|
118
|
+
else:
|
|
119
|
+
return BacktestEvaluationFocus.from_string(other) == self
|
|
120
|
+
|
|
121
|
+
def get_weights(self):
|
|
122
|
+
"""
|
|
123
|
+
Get evaluation weights for different focus areas.
|
|
124
|
+
Returns a dictionary with metric weights where:
|
|
125
|
+
- Positive weights favor higher values
|
|
126
|
+
- Negative weights favor lower values (penalties)
|
|
127
|
+
- Zero weights ignore the metric
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
if self == BacktestEvaluationFocus.BALANCED:
|
|
131
|
+
return {
|
|
132
|
+
# Core profitability metrics (moderate weight)
|
|
133
|
+
"total_net_gain_percentage": 2.0,
|
|
134
|
+
"cagr": 1.5,
|
|
135
|
+
"average_trade_return_percentage": 1.0,
|
|
136
|
+
|
|
137
|
+
# Risk-adjusted returns (important for balance)
|
|
138
|
+
"sharpe_ratio": 2.0,
|
|
139
|
+
"sortino_ratio": 1.5,
|
|
140
|
+
"calmar_ratio": 1.0,
|
|
141
|
+
"profit_factor": 1.5,
|
|
142
|
+
|
|
143
|
+
# Risk management (penalties for bad metrics)
|
|
144
|
+
"max_drawdown": -1.5,
|
|
145
|
+
"max_drawdown_duration": -0.5,
|
|
146
|
+
"annual_volatility": -0.8,
|
|
147
|
+
|
|
148
|
+
# Trading consistency
|
|
149
|
+
"win_rate": 1.5,
|
|
150
|
+
"win_loss_ratio": 1.0,
|
|
151
|
+
"number_of_trades": 0.8, # Some activity needed
|
|
152
|
+
|
|
153
|
+
# Efficiency metrics
|
|
154
|
+
"exposure_ratio": 0.5,
|
|
155
|
+
"trades_per_year": 0.3,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
elif self == BacktestEvaluationFocus.PROFIT:
|
|
159
|
+
return {
|
|
160
|
+
# Maximize absolute and relative profits
|
|
161
|
+
"total_net_gain_percentage": 3.0,
|
|
162
|
+
"cagr": 2.5,
|
|
163
|
+
"total_net_gain": 2.0,
|
|
164
|
+
"average_trade_return_percentage": 1.5,
|
|
165
|
+
"best_trade": 1.0,
|
|
166
|
+
|
|
167
|
+
# Profit consistency
|
|
168
|
+
"win_rate": 2.0,
|
|
169
|
+
"profit_factor": 2.0,
|
|
170
|
+
"percentage_positive_trades": 1.0,
|
|
171
|
+
|
|
172
|
+
# Risk secondary (but still important)
|
|
173
|
+
"sharpe_ratio": 1.0,
|
|
174
|
+
"max_drawdown": -1.0,
|
|
175
|
+
|
|
176
|
+
# Activity level (need some trades)
|
|
177
|
+
"number_of_trades": 0.5,
|
|
178
|
+
|
|
179
|
+
# Monthly/yearly consistency
|
|
180
|
+
"percentage_winning_months": 0.8,
|
|
181
|
+
"average_monthly_return": 1.0,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
elif self == BacktestEvaluationFocus.FREQUENCY:
|
|
185
|
+
return {
|
|
186
|
+
# High trading activity with good results
|
|
187
|
+
"number_of_trades": 3.0,
|
|
188
|
+
"trades_per_year": 2.5,
|
|
189
|
+
"exposure_ratio": 2.0,
|
|
190
|
+
|
|
191
|
+
# Profitability per trade (scaled for frequency)
|
|
192
|
+
"average_trade_return_percentage": 2.0,
|
|
193
|
+
"win_rate": 2.5,
|
|
194
|
+
"total_net_gain_percentage": 1.5,
|
|
195
|
+
|
|
196
|
+
# Consistency across many trades
|
|
197
|
+
"win_loss_ratio": 1.5,
|
|
198
|
+
"percentage_positive_trades": 1.0,
|
|
199
|
+
|
|
200
|
+
# Risk management for frequent trading
|
|
201
|
+
"max_drawdown": -1.5,
|
|
202
|
+
"sharpe_ratio": 1.0,
|
|
203
|
+
"profit_factor": 1.2,
|
|
204
|
+
|
|
205
|
+
# Duration efficiency
|
|
206
|
+
"average_trade_duration": -0.3, # Prefer shorter trades
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
elif self == BacktestEvaluationFocus.RISK_ADJUSTED:
|
|
210
|
+
return {
|
|
211
|
+
# Risk-adjusted performance metrics (highest priority)
|
|
212
|
+
"sharpe_ratio": 3.0,
|
|
213
|
+
"sortino_ratio": 2.5,
|
|
214
|
+
"calmar_ratio": 2.0,
|
|
215
|
+
|
|
216
|
+
# Risk management (strong penalties)
|
|
217
|
+
"max_drawdown": -3.0,
|
|
218
|
+
"max_drawdown_duration": -1.5,
|
|
219
|
+
"annual_volatility": -2.0,
|
|
220
|
+
"worst_trade": -1.0,
|
|
221
|
+
|
|
222
|
+
# Consistent performance
|
|
223
|
+
"win_rate": 2.0,
|
|
224
|
+
"win_loss_ratio": 1.5,
|
|
225
|
+
"percentage_winning_months": 1.5,
|
|
226
|
+
|
|
227
|
+
# Stable returns
|
|
228
|
+
"average_trade_return_percentage": 1.5,
|
|
229
|
+
"total_net_gain_percentage": 1.0,
|
|
230
|
+
"profit_factor": 1.8,
|
|
231
|
+
|
|
232
|
+
# Reasonable activity
|
|
233
|
+
"number_of_trades": 0.5,
|
|
234
|
+
|
|
235
|
+
# Downside protection
|
|
236
|
+
"average_trade_loss_percentage": -1.0,
|
|
237
|
+
"percentage_negative_trades": -1.0,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Fallback to balanced if unknown focus
|
|
241
|
+
return self.get_weights() \
|
|
242
|
+
if self != BacktestEvaluationFocus.BALANCED else {}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from typing import Tuple, List, Dict
|
|
6
|
+
from datetime import datetime, date
|
|
7
|
+
import json
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from investing_algorithm_framework.domain.models import Trade
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class BacktestMetrics:
|
|
18
|
+
"""
|
|
19
|
+
Represents the result of a backtest, including metrics such as
|
|
20
|
+
total return, annualized return, volatility, Sharpe ratio,
|
|
21
|
+
and maximum drawdown.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
backtest_start_date (datetime): The start date of the backtest.
|
|
25
|
+
backtest_end_date (datetime): The end date of the backtest.
|
|
26
|
+
final_value (float): The final value of the portfolio at the end
|
|
27
|
+
of the backtest.
|
|
28
|
+
equity_curve (List[Tuple[datetime, float]]): A list of
|
|
29
|
+
tuples representing the equity curve, where each tuple
|
|
30
|
+
contains a date and the corresponding portfolio value.
|
|
31
|
+
total_growth (float): The growth of the portfolio over the
|
|
32
|
+
backtest period.
|
|
33
|
+
total_growth_percentage (float): The percentage growth of the portfolio
|
|
34
|
+
over the backtest period.
|
|
35
|
+
total_net_gain (float): The total return of the backtest.
|
|
36
|
+
total_net_gain_percentage (float): The total return percentage
|
|
37
|
+
total_loss (float): The total loss of the backtest.
|
|
38
|
+
total_loss_percentage (float): The total loss percentage
|
|
39
|
+
cagr (float): The compound annual growth rate of the backtest.
|
|
40
|
+
sharpe_ratio (float): The Sharpe ratio of the backtest, indicating
|
|
41
|
+
risk-adjusted return.
|
|
42
|
+
rolling_sharpe_ratio (List[Tuple[datetime, float]): A list of rolling
|
|
43
|
+
Sharpe ratios over the backtest period.
|
|
44
|
+
sortino_ratio (float): The Sortino ratio of the backtest, focusing
|
|
45
|
+
on downside risk.
|
|
46
|
+
calmar_ratio (float): The Calmar ratio of the backtest, comparing
|
|
47
|
+
CAGR to maximum drawdown.
|
|
48
|
+
profit_factor (float): The profit factor of the backtest, calculated
|
|
49
|
+
as total profit divided by total loss.
|
|
50
|
+
annual_volatility (float): The annualized volatility of the
|
|
51
|
+
portfolio returns.
|
|
52
|
+
monthly_returns (List[Tuple[datetime, float]]): A list of monthly
|
|
53
|
+
returns during the backtest.
|
|
54
|
+
yearly_returns (List[Tuple[datetime, float]]): A list of yearly returns
|
|
55
|
+
during the backtest.
|
|
56
|
+
drawdown_series (List[Tuple[datetime, float]]): A list of drawdown
|
|
57
|
+
values over the backtest period.
|
|
58
|
+
max_drawdown (float): The maximum drawdown observed during
|
|
59
|
+
the backtest.
|
|
60
|
+
max_drawdown_absolute (float): The maximum absolute drawdown
|
|
61
|
+
observed during the backtest.
|
|
62
|
+
max_daily_drawdown (float): The maximum daily drawdown
|
|
63
|
+
observed during the backtest.
|
|
64
|
+
max_drawdown_duration (int): The duration of the maximum
|
|
65
|
+
drawdown in days.
|
|
66
|
+
trades_per_year (float): The average number of trades
|
|
67
|
+
executed per year.
|
|
68
|
+
trade_per_day (float): The average number of trades executed per day.
|
|
69
|
+
exposure_ratio (float): The exposure ratio, indicating the
|
|
70
|
+
average exposure of the portfolio.
|
|
71
|
+
cumulative_exposure (float): The cumulative exposure, indicating the
|
|
72
|
+
total exposure of the portfolio over the backtest period.
|
|
73
|
+
average_trade_size (float): The average size of trades executed
|
|
74
|
+
during the backtest.
|
|
75
|
+
average_trade_loss (float): The average loss from losing trades.
|
|
76
|
+
average_trade_loss_percentage (float): The average loss percentage
|
|
77
|
+
from losing trades.
|
|
78
|
+
average_trade_gain (float): The average gain from winning trades.
|
|
79
|
+
average_trade_gain_percentage (float): The average gain percentage
|
|
80
|
+
from winning trades.
|
|
81
|
+
average_trade_return (float): The average return from all trades.
|
|
82
|
+
average_trade_return_percentage (float): The average return percentage
|
|
83
|
+
from all trades.
|
|
84
|
+
median_trade_return (float): The median return from all trades.
|
|
85
|
+
median_trade_return_percentage (float): The median return percentage
|
|
86
|
+
from all trades.
|
|
87
|
+
number_of_positive_trades (int): The total number of profitable trades
|
|
88
|
+
executed during the backtest.
|
|
89
|
+
number_of_negative_trades (int): The total number of unprofitable
|
|
90
|
+
trades executed during the backtest.
|
|
91
|
+
best_trade (float): A string representation of the best trade,
|
|
92
|
+
including net gain and percentage.
|
|
93
|
+
worst_trade (float): A string representation of the worst trade,
|
|
94
|
+
including net loss and percentage.
|
|
95
|
+
average_trade_duration (float): The average duration of
|
|
96
|
+
trades in hours.
|
|
97
|
+
number_of_trades (int): The total number of trades executed
|
|
98
|
+
during the backtest.
|
|
99
|
+
number_of_trades_closed (int): The total number of trades close
|
|
100
|
+
during the backtest.
|
|
101
|
+
number_of_trades_opened (int): The total number of trades opened
|
|
102
|
+
during the backtest.
|
|
103
|
+
number_of_trades_open_at_end (int): The number of trades
|
|
104
|
+
still open at the end of the backtest.
|
|
105
|
+
win_rate (float): The win rate of the trades, expressed
|
|
106
|
+
as a percentage.
|
|
107
|
+
current_win_rate (float): The current win rate of the trades,
|
|
108
|
+
including open trades.
|
|
109
|
+
win_loss_ratio (float): The ratio of winning trades
|
|
110
|
+
to losing trades.
|
|
111
|
+
current_win_loss_ratio (float): The current ratio of winning
|
|
112
|
+
trades to losing trades, including open trades.
|
|
113
|
+
percentage_winning_months (float): The percentage of months
|
|
114
|
+
with positive returns.
|
|
115
|
+
percentage_winning_years (float): The percentage of years with
|
|
116
|
+
positive returns.
|
|
117
|
+
percentage_positive_trades (float): The percentage of trades that
|
|
118
|
+
were profitable.
|
|
119
|
+
percentage_negative_trades (float): The percentage of trades that
|
|
120
|
+
were unprofitable.
|
|
121
|
+
average_monthly_return (float): The average monthly return
|
|
122
|
+
of the portfolio.
|
|
123
|
+
average_monthly_return_losing_months (float): The average monthly
|
|
124
|
+
return during losing months.
|
|
125
|
+
average_monthly_return_winning_months (float): The average monthly
|
|
126
|
+
return during winning months.
|
|
127
|
+
best_month (datetime): A string representation of the best month,
|
|
128
|
+
including return and date.
|
|
129
|
+
best_year (datetime): A string representation of the best year,
|
|
130
|
+
including return and date.
|
|
131
|
+
worst_month (datetime): A string representation of the worst month,
|
|
132
|
+
including return and date.
|
|
133
|
+
worst_year (datetime): A string representation of the worst year,
|
|
134
|
+
including return and date.
|
|
135
|
+
metadata (Dict[str, str]): A dictionary to store any additional
|
|
136
|
+
metadata related to the backtest.
|
|
137
|
+
"""
|
|
138
|
+
backtest_start_date: datetime
|
|
139
|
+
backtest_end_date: datetime
|
|
140
|
+
equity_curve: List[Tuple[float, datetime]] = field(default_factory=list)
|
|
141
|
+
total_growth: float = 0.0
|
|
142
|
+
total_growth_percentage: float = 0.0
|
|
143
|
+
total_net_gain: float = 0.0
|
|
144
|
+
total_net_gain_percentage: float = 0.0
|
|
145
|
+
total_loss: float = 0.0
|
|
146
|
+
total_loss_percentage: float = 0.0
|
|
147
|
+
final_value: float = 0.0
|
|
148
|
+
cumulative_return: float = 0.0
|
|
149
|
+
cumulative_return_series: List[Tuple[float, datetime]] = \
|
|
150
|
+
field(default_factory=list)
|
|
151
|
+
cagr: float = 0.0
|
|
152
|
+
sharpe_ratio: float = 0.0
|
|
153
|
+
rolling_sharpe_ratio: List[Tuple[float, datetime]] = \
|
|
154
|
+
field(default_factory=list)
|
|
155
|
+
sortino_ratio: float = 0.0
|
|
156
|
+
calmar_ratio: float = 0.0
|
|
157
|
+
profit_factor: float = 0.0
|
|
158
|
+
gross_profit: float = None
|
|
159
|
+
gross_loss: float = None
|
|
160
|
+
annual_volatility: float = 0.0
|
|
161
|
+
monthly_returns: List[Tuple[float, datetime]] = field(default_factory=list)
|
|
162
|
+
yearly_returns: List[Tuple[float, date]] = field(default_factory=list)
|
|
163
|
+
drawdown_series: List[Tuple[float, datetime]] = field(default_factory=list)
|
|
164
|
+
max_drawdown: float = 0.0
|
|
165
|
+
max_drawdown_absolute: float = 0.0
|
|
166
|
+
max_daily_drawdown: float = 0.0
|
|
167
|
+
max_drawdown_duration: int = 0
|
|
168
|
+
trades_per_year: float = 0.0
|
|
169
|
+
trade_per_day: float = 0.0
|
|
170
|
+
exposure_ratio: float = 0.0
|
|
171
|
+
cumulative_exposure: float = 0.0
|
|
172
|
+
best_trade: Trade = None
|
|
173
|
+
worst_trade: Trade = None
|
|
174
|
+
number_of_positive_trades: int = 0
|
|
175
|
+
percentage_positive_trades: float = 0.0
|
|
176
|
+
number_of_negative_trades: int = 0
|
|
177
|
+
percentage_negative_trades: float = 0.0
|
|
178
|
+
average_trade_duration: float = 0.0
|
|
179
|
+
average_trade_size: float = 0.0
|
|
180
|
+
average_trade_loss: float = 0.0
|
|
181
|
+
average_trade_loss_percentage: float = 0.0
|
|
182
|
+
average_trade_gain: float = 0.0
|
|
183
|
+
average_trade_gain_percentage: float = 0.0
|
|
184
|
+
average_trade_return: float = 0.0
|
|
185
|
+
average_trade_return_percentage: float = 0.0
|
|
186
|
+
current_average_trade_gain: float = 0.0
|
|
187
|
+
current_average_trade_gain_percentage: float = 0.0
|
|
188
|
+
current_average_trade_return: float = 0.0
|
|
189
|
+
current_average_trade_return_percentage: float = 0.0
|
|
190
|
+
current_average_trade_duration: float = 0.0
|
|
191
|
+
current_average_trade_loss: float = 0.0
|
|
192
|
+
current_average_trade_loss_percentage: float = 0.0
|
|
193
|
+
median_trade_return: float = 0.0
|
|
194
|
+
median_trade_return_percentage: float = 0.0
|
|
195
|
+
number_of_trades: int = 0
|
|
196
|
+
number_of_trades_closed: int = 0
|
|
197
|
+
number_of_trades_opened: int = 0
|
|
198
|
+
number_of_trades_open_at_end: int = 0
|
|
199
|
+
win_rate: float = 0.0
|
|
200
|
+
current_win_rate: float = 0.0
|
|
201
|
+
win_loss_ratio: float = 0.0
|
|
202
|
+
current_win_loss_ratio: float = 0.0
|
|
203
|
+
percentage_winning_months: float = 0.0
|
|
204
|
+
percentage_winning_years: float = 0.0
|
|
205
|
+
average_monthly_return: float = 0.0
|
|
206
|
+
average_monthly_return_losing_months: float = 0.0
|
|
207
|
+
average_monthly_return_winning_months: float = 0.0
|
|
208
|
+
best_month: Tuple[float, datetime] = None
|
|
209
|
+
best_year: Tuple[float, date] = None
|
|
210
|
+
worst_month: Tuple[float, datetime] = None
|
|
211
|
+
worst_year: Tuple[float, date] = None
|
|
212
|
+
total_number_of_days: int = None
|
|
213
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
|
214
|
+
|
|
215
|
+
def __post_init__(self):
|
|
216
|
+
self.total_number_of_days = (self.backtest_end_date -
|
|
217
|
+
self.backtest_start_date).days
|
|
218
|
+
|
|
219
|
+
def to_dict(self) -> dict:
|
|
220
|
+
"""
|
|
221
|
+
Convert the BacktestMetrics instance to a dictionary.
|
|
222
|
+
Ensures all datetime values are serialized to ISO format, but
|
|
223
|
+
leaves strings unchanged.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
dict: A dictionary representation of the BacktestMetrics instance.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def ensure_iso(value):
|
|
230
|
+
return value.isoformat() \
|
|
231
|
+
if hasattr(value, "isoformat") else value
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"backtest_start_date": ensure_iso(self.backtest_start_date),
|
|
235
|
+
"backtest_end_date": ensure_iso(self.backtest_end_date),
|
|
236
|
+
"equity_curve": [(value, ensure_iso(date))
|
|
237
|
+
for value, date in self.equity_curve],
|
|
238
|
+
"final_value": self.final_value,
|
|
239
|
+
"total_net_gain": self.total_net_gain,
|
|
240
|
+
"total_net_gain_percentage": self.total_net_gain_percentage,
|
|
241
|
+
"total_growth": self.total_growth,
|
|
242
|
+
"total_growth_percentage": self.total_growth_percentage,
|
|
243
|
+
"total_loss": self.total_loss,
|
|
244
|
+
"total_loss_percentage": self.total_loss_percentage,
|
|
245
|
+
"cumulative_return": self.cumulative_return,
|
|
246
|
+
"cumulative_return_series": [(value, ensure_iso(date))
|
|
247
|
+
for value, date in
|
|
248
|
+
self.cumulative_return_series],
|
|
249
|
+
"cagr": self.cagr,
|
|
250
|
+
"sharpe_ratio": self.sharpe_ratio,
|
|
251
|
+
"rolling_sharpe_ratio": [(value, ensure_iso(date))
|
|
252
|
+
for value, date in
|
|
253
|
+
self.rolling_sharpe_ratio],
|
|
254
|
+
"sortino_ratio": self.sortino_ratio,
|
|
255
|
+
"calmar_ratio": self.calmar_ratio,
|
|
256
|
+
"profit_factor": self.profit_factor,
|
|
257
|
+
"annual_volatility": self.annual_volatility,
|
|
258
|
+
"monthly_returns": [(value, ensure_iso(date))
|
|
259
|
+
for value, date in self.monthly_returns],
|
|
260
|
+
"yearly_returns": [(value, ensure_iso(date))
|
|
261
|
+
for value, date in self.yearly_returns],
|
|
262
|
+
"drawdown_series": [(value, ensure_iso(date))
|
|
263
|
+
for value, date in self.drawdown_series],
|
|
264
|
+
"max_drawdown": self.max_drawdown,
|
|
265
|
+
"max_drawdown_absolute": self.max_drawdown_absolute,
|
|
266
|
+
"max_daily_drawdown": self.max_daily_drawdown,
|
|
267
|
+
"max_drawdown_duration": self.max_drawdown_duration,
|
|
268
|
+
"trades_per_year": self.trades_per_year,
|
|
269
|
+
"trade_per_day": self.trade_per_day,
|
|
270
|
+
"exposure_ratio": self.exposure_ratio,
|
|
271
|
+
"cumulative_exposure": self.cumulative_exposure,
|
|
272
|
+
"average_trade_gain": self.average_trade_gain,
|
|
273
|
+
"average_trade_gain_percentage":
|
|
274
|
+
self.average_trade_gain_percentage,
|
|
275
|
+
"average_trade_loss": self.average_trade_loss,
|
|
276
|
+
"average_trade_loss_percentage":
|
|
277
|
+
self.average_trade_loss_percentage,
|
|
278
|
+
"average_trade_return": self.average_trade_return,
|
|
279
|
+
"average_trade_return_percentage":
|
|
280
|
+
self.average_trade_return_percentage,
|
|
281
|
+
"median_trade_return": self.median_trade_return,
|
|
282
|
+
"median_trade_return_percentage":
|
|
283
|
+
self.median_trade_return_percentage,
|
|
284
|
+
"number_of_positive_trades": self.number_of_positive_trades,
|
|
285
|
+
"percentage_positive_trades": self.percentage_positive_trades,
|
|
286
|
+
"number_of_negative_trades": self.number_of_negative_trades,
|
|
287
|
+
"percentage_negative_trades": self.percentage_negative_trades,
|
|
288
|
+
"best_trade": self.best_trade.to_dict()
|
|
289
|
+
if self.best_trade else None,
|
|
290
|
+
"worst_trade": self.worst_trade.to_dict()
|
|
291
|
+
if self.worst_trade else None,
|
|
292
|
+
"average_trade_duration": self.average_trade_duration,
|
|
293
|
+
"average_trade_size": self.average_trade_size,
|
|
294
|
+
"number_of_trades": self.number_of_trades,
|
|
295
|
+
"number_of_trades_closed": self.number_of_trades_closed,
|
|
296
|
+
"number_of_trades_opened": self.number_of_trades_opened,
|
|
297
|
+
"win_rate": self.win_rate,
|
|
298
|
+
"current_win_rate": self.current_win_rate,
|
|
299
|
+
"win_loss_ratio": self.win_loss_ratio,
|
|
300
|
+
"current_win_loss_ratio": self.current_win_loss_ratio,
|
|
301
|
+
"percentage_winning_months": self.percentage_winning_months,
|
|
302
|
+
"percentage_winning_years": self.percentage_winning_years,
|
|
303
|
+
"average_monthly_return": self.average_monthly_return,
|
|
304
|
+
"average_monthly_return_losing_months":
|
|
305
|
+
self.average_monthly_return_losing_months,
|
|
306
|
+
"average_monthly_return_winning_months":
|
|
307
|
+
self.average_monthly_return_winning_months,
|
|
308
|
+
"best_month": self.best_month,
|
|
309
|
+
"best_year": self.best_year,
|
|
310
|
+
"worst_month": self.worst_month,
|
|
311
|
+
"worst_year": self.worst_year
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
def save(self, file_path: str | Path) -> None:
|
|
315
|
+
"""
|
|
316
|
+
Save the backtest metrics to a file in JSON format. The metrics will
|
|
317
|
+
always be saved in a file named `metrics.json`
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
file_path (str): The directory where the metrics
|
|
321
|
+
file will be saved.
|
|
322
|
+
"""
|
|
323
|
+
with open(file_path, 'w') as file:
|
|
324
|
+
json.dump(self.to_dict(), file, indent=4, default=str)
|
|
325
|
+
|
|
326
|
+
@staticmethod
|
|
327
|
+
def _parse_tuple_list_datetime(
|
|
328
|
+
data: List[List]
|
|
329
|
+
) -> List[Tuple[float, datetime]]:
|
|
330
|
+
"""
|
|
331
|
+
Parse a list of [value, datetime_string]
|
|
332
|
+
into List[Tuple[float, datetime]]
|
|
333
|
+
"""
|
|
334
|
+
return [
|
|
335
|
+
(float(value), datetime.fromisoformat(date_str))
|
|
336
|
+
for value, date_str in data
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def _parse_tuple_list_date(data: List[List]) -> List[Tuple[float, date]]:
|
|
341
|
+
"""
|
|
342
|
+
Parse a list of [value, date_string] into List[Tuple[float, date]]
|
|
343
|
+
"""
|
|
344
|
+
return [
|
|
345
|
+
(float(value), datetime.fromisoformat(date_str).date())
|
|
346
|
+
for value, date_str in data
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def _parse_tuple_datetime(data) -> Tuple[float, datetime]:
|
|
351
|
+
"""Parse a [value, datetime_string] into Tuple[float, datetime]"""
|
|
352
|
+
if data is None:
|
|
353
|
+
return None, None
|
|
354
|
+
|
|
355
|
+
# Check if the value is NaN or None
|
|
356
|
+
if pd.isna(data[0]) or data[0] is None:
|
|
357
|
+
value = pd.NA
|
|
358
|
+
else:
|
|
359
|
+
value = float(data[0])
|
|
360
|
+
|
|
361
|
+
# Parse the datetime string
|
|
362
|
+
if data[1] is None or pd.isna(data[1]):
|
|
363
|
+
value = None
|
|
364
|
+
else:
|
|
365
|
+
# Convert the string to a datetime object
|
|
366
|
+
date = datetime.fromisoformat(data[1])
|
|
367
|
+
|
|
368
|
+
return (value, date)
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def _parse_tuple_date(data) -> Tuple[float, date]:
|
|
372
|
+
"""Parse a [value, date_string] into Tuple[float, date]"""
|
|
373
|
+
if data is None:
|
|
374
|
+
return None, None
|
|
375
|
+
|
|
376
|
+
# Check if the value is
|
|
377
|
+
if pd.isna(data[0]) or data[0] is None:
|
|
378
|
+
value = pd.NA
|
|
379
|
+
else:
|
|
380
|
+
value = float(data[0])
|
|
381
|
+
|
|
382
|
+
# Parse the date string
|
|
383
|
+
if data[1] is None or pd.isna(data[1]):
|
|
384
|
+
date = None
|
|
385
|
+
else:
|
|
386
|
+
date = datetime.fromisoformat(data[1]).date()
|
|
387
|
+
|
|
388
|
+
return (value, date)
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def open(file_path: str | Path) -> 'BacktestMetrics':
|
|
392
|
+
"""
|
|
393
|
+
Open a backtest metrics file from a directory and
|
|
394
|
+
return a BacktestMetrics instance.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
file_path (str): The path to the metrics file.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
BacktestMetrics: An instance of BacktestMetrics
|
|
401
|
+
loaded from the file.
|
|
402
|
+
"""
|
|
403
|
+
if not os.path.exists(file_path):
|
|
404
|
+
raise FileNotFoundError(f"Metrics file not found at {file_path}")
|
|
405
|
+
|
|
406
|
+
with open(file_path, 'r') as file:
|
|
407
|
+
data = json.load(file)
|
|
408
|
+
|
|
409
|
+
# Parse datetime fields
|
|
410
|
+
data['backtest_start_date'] = datetime.fromisoformat(
|
|
411
|
+
data['backtest_start_date']
|
|
412
|
+
)
|
|
413
|
+
data['backtest_end_date'] = datetime.fromisoformat(
|
|
414
|
+
data['backtest_end_date']
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Parse tuple lists with datetime
|
|
418
|
+
data['equity_curve'] = BacktestMetrics._parse_tuple_list_datetime(
|
|
419
|
+
data.get('equity_curve', [])
|
|
420
|
+
)
|
|
421
|
+
data['rolling_sharpe_ratio'] = BacktestMetrics\
|
|
422
|
+
._parse_tuple_list_datetime(data.get('rolling_sharpe_ratio', []))
|
|
423
|
+
data['monthly_returns'] = BacktestMetrics\
|
|
424
|
+
._parse_tuple_list_datetime(data.get('monthly_returns', []))
|
|
425
|
+
data['drawdown_series'] = BacktestMetrics\
|
|
426
|
+
._parse_tuple_list_datetime(data.get('drawdown_series', []))
|
|
427
|
+
|
|
428
|
+
# Parse tuple lists with date
|
|
429
|
+
data['yearly_returns'] = BacktestMetrics\
|
|
430
|
+
._parse_tuple_list_date(data.get('yearly_returns', []))
|
|
431
|
+
|
|
432
|
+
# Parse single tuples
|
|
433
|
+
data['best_month'] = BacktestMetrics\
|
|
434
|
+
._parse_tuple_datetime(data.get('best_month'))
|
|
435
|
+
data['worst_month'] = BacktestMetrics\
|
|
436
|
+
._parse_tuple_datetime(data.get('worst_month'))
|
|
437
|
+
data['best_year'] = BacktestMetrics\
|
|
438
|
+
._parse_tuple_date(data.get('best_year'))
|
|
439
|
+
data['worst_year'] = BacktestMetrics\
|
|
440
|
+
._parse_tuple_date(data.get('worst_year'))
|
|
441
|
+
|
|
442
|
+
# Parse Trade objects if they exist
|
|
443
|
+
if data.get('best_trade'):
|
|
444
|
+
data['best_trade'] = Trade.from_dict(data['best_trade'])
|
|
445
|
+
if data.get('worst_trade'):
|
|
446
|
+
data['worst_trade'] = Trade.from_dict(data['worst_trade'])
|
|
447
|
+
|
|
448
|
+
return BacktestMetrics(**data)
|
|
449
|
+
|
|
450
|
+
def __repr__(self):
|
|
451
|
+
"""
|
|
452
|
+
Return a string representation of the Backtest instance.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
str: A string representation of the Backtest instance.
|
|
456
|
+
"""
|
|
457
|
+
return json.dumps(
|
|
458
|
+
self.to_dict(), indent=4, sort_keys=True, default=str
|
|
459
|
+
)
|