investing-algorithm-framework 7.19.14__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 +197 -0
- investing_algorithm_framework/app/__init__.py +47 -0
- investing_algorithm_framework/app/algorithm/__init__.py +7 -0
- investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
- 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 +2204 -0
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1667 -0
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +27 -0
- investing_algorithm_framework/app/reporting/ascii.py +921 -0
- investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
- investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
- 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 +74 -0
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
- investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
- investing_algorithm_framework/app/reporting/generate.py +185 -0
- investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
- investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
- investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
- investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
- investing_algorithm_framework/app/stateless/__init__.py +35 -0
- investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
- investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
- investing_algorithm_framework/app/strategy.py +675 -0
- investing_algorithm_framework/app/task.py +41 -0
- investing_algorithm_framework/app/web/__init__.py +5 -0
- investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
- investing_algorithm_framework/app/web/controllers/orders.py +20 -0
- investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
- investing_algorithm_framework/app/web/controllers/positions.py +18 -0
- investing_algorithm_framework/app/web/create_app.py +20 -0
- investing_algorithm_framework/app/web/error_handler.py +59 -0
- investing_algorithm_framework/app/web/responses.py +20 -0
- investing_algorithm_framework/app/web/run_strategies.py +4 -0
- investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
- investing_algorithm_framework/app/web/schemas/order.py +12 -0
- investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
- investing_algorithm_framework/app/web/schemas/position.py +15 -0
- investing_algorithm_framework/app/web/setup_cors.py +6 -0
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +207 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
- investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
- investing_algorithm_framework/cli/initialize_app.py +603 -0
- investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
- investing_algorithm_framework/cli/templates/app.py.template +18 -0
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
- investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
- investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
- 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_readme.md.template +110 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
- investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
- investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
- investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
- investing_algorithm_framework/cli/templates/env.example.template +2 -0
- investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
- investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
- investing_algorithm_framework/cli/templates/readme.md.template +135 -0
- investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
- investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
- investing_algorithm_framework/create_app.py +54 -0
- investing_algorithm_framework/dependency_container.py +155 -0
- investing_algorithm_framework/domain/__init__.py +148 -0
- 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 +435 -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 +111 -0
- investing_algorithm_framework/domain/constants.py +83 -0
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +42 -0
- investing_algorithm_framework/domain/decimal_parsing.py +40 -0
- investing_algorithm_framework/domain/exceptions.py +112 -0
- investing_algorithm_framework/domain/models/__init__.py +43 -0
- investing_algorithm_framework/domain/models/app_mode.py +34 -0
- investing_algorithm_framework/domain/models/base_model.py +25 -0
- 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/data/data_type.py +46 -0
- investing_algorithm_framework/domain/models/event.py +35 -0
- investing_algorithm_framework/domain/models/market/__init__.py +5 -0
- investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
- investing_algorithm_framework/domain/models/order/__init__.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +384 -0
- investing_algorithm_framework/domain/models/order/order_side.py +36 -0
- investing_algorithm_framework/domain/models/order/order_status.py +37 -0
- investing_algorithm_framework/domain/models/order/order_type.py +30 -0
- investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
- investing_algorithm_framework/domain/models/position/__init__.py +4 -0
- investing_algorithm_framework/domain/models/position/position.py +68 -0
- investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
- investing_algorithm_framework/domain/models/time_frame.py +153 -0
- investing_algorithm_framework/domain/models/time_interval.py +124 -0
- investing_algorithm_framework/domain/models/time_unit.py +149 -0
- investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
- investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
- investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
- investing_algorithm_framework/domain/models/trade/trade.py +388 -0
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
- investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -0
- investing_algorithm_framework/domain/positions/__init__.py +4 -0
- investing_algorithm_framework/domain/positions/position_size.py +41 -0
- investing_algorithm_framework/domain/services/__init__.py +11 -0
- investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
- investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
- investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
- investing_algorithm_framework/domain/services/rounding_service.py +27 -0
- investing_algorithm_framework/domain/services/state_handler.py +38 -0
- investing_algorithm_framework/domain/stateless_actions.py +7 -0
- investing_algorithm_framework/domain/strategy.py +44 -0
- investing_algorithm_framework/domain/utils/__init__.py +27 -0
- investing_algorithm_framework/domain/utils/csv.py +104 -0
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/dates.py +57 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +53 -0
- investing_algorithm_framework/domain/utils/random.py +41 -0
- investing_algorithm_framework/domain/utils/signatures.py +17 -0
- investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
- investing_algorithm_framework/domain/utils/synchronized.py +12 -0
- investing_algorithm_framework/download_data.py +108 -0
- investing_algorithm_framework/infrastructure/__init__.py +50 -0
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
- investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
- investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
- investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
- investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
- investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
- investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
- investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
- investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
- investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
- investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
- investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
- investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
- investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
- investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
- investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
- investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
- investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
- investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
- investing_algorithm_framework/services/__init__.py +132 -0
- investing_algorithm_framework/services/backtesting/__init__.py +5 -0
- investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
- investing_algorithm_framework/services/configuration_service.py +96 -0
- 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/services/market_credential_service.py +40 -0
- investing_algorithm_framework/services/metrics/__init__.py +114 -0
- investing_algorithm_framework/services/metrics/alpha.py +0 -0
- investing_algorithm_framework/services/metrics/beta.py +0 -0
- investing_algorithm_framework/services/metrics/cagr.py +60 -0
- investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
- investing_algorithm_framework/services/metrics/drawdown.py +181 -0
- investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
- investing_algorithm_framework/services/metrics/exposure.py +210 -0
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
- investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
- investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
- investing_algorithm_framework/services/metrics/recovery.py +113 -0
- investing_algorithm_framework/services/metrics/returns.py +452 -0
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
- investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
- investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
- investing_algorithm_framework/services/metrics/ulcer.py +0 -0
- investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/services/metrics/win_rate.py +177 -0
- investing_algorithm_framework/services/order_service/__init__.py +9 -0
- investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
- investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
- investing_algorithm_framework/services/order_service/order_service.py +826 -0
- investing_algorithm_framework/services/portfolios/__init__.py +16 -0
- investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
- investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
- investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
- investing_algorithm_framework/services/positions/__init__.py +7 -0
- investing_algorithm_framework/services/positions/position_service.py +210 -0
- investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
- investing_algorithm_framework/services/repository_service.py +40 -0
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
- investing_algorithm_framework/services/trade_service/__init__.py +3 -0
- investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
- investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
- investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
- investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
- investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
- investing_algorithm_framework-7.19.14.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
from datetime import timezone
|
|
5
|
+
from investing_algorithm_framework.domain import BacktestDateRange, \
|
|
6
|
+
OperationalException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def select_backtest_date_ranges(
|
|
10
|
+
df: pd.DataFrame, window: Union[str, int] = '365D'
|
|
11
|
+
) -> List[BacktestDateRange]:
|
|
12
|
+
"""
|
|
13
|
+
Identifies the best upturn, worst downturn, and sideways periods
|
|
14
|
+
for the given window duration. This allows you to quickly select
|
|
15
|
+
interesting periods for backtesting.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
df (pd.DataFrame): DataFrame with a DateTime index
|
|
19
|
+
and 'Close' column.
|
|
20
|
+
window (Union[str, int]): Duration of the window
|
|
21
|
+
to analyze. Can be a string like '365D' or an
|
|
22
|
+
integer representing days.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
List[BacktestDateRange]: List of BacktestDateRange
|
|
26
|
+
objects representing the best upturn, worst
|
|
27
|
+
downturn, and most sideways periods.
|
|
28
|
+
"""
|
|
29
|
+
df = df.copy()
|
|
30
|
+
df = df.sort_index()
|
|
31
|
+
|
|
32
|
+
if isinstance(window, int):
|
|
33
|
+
window = pd.Timedelta(days=window)
|
|
34
|
+
elif isinstance(window, str):
|
|
35
|
+
window = pd.to_timedelta(window)
|
|
36
|
+
else:
|
|
37
|
+
raise OperationalException("window must be a string or integer")
|
|
38
|
+
|
|
39
|
+
# Check if the window is larger than the DataFrame
|
|
40
|
+
if len(df) == 0:
|
|
41
|
+
raise OperationalException("DataFrame is empty")
|
|
42
|
+
|
|
43
|
+
if df.index[-1] - df.index[0] < window:
|
|
44
|
+
raise OperationalException(
|
|
45
|
+
"Window duration is larger than the data duration"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if len(df) < 2 or df.index[-1] - df.index[0] < window:
|
|
49
|
+
raise OperationalException(
|
|
50
|
+
"DataFrame must contain at least two rows and span "
|
|
51
|
+
"the full window duration"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
best_upturn = {
|
|
55
|
+
"name": "UpTurn", "return": float('-inf'), "start": None, "end": None
|
|
56
|
+
}
|
|
57
|
+
worst_downturn = {
|
|
58
|
+
"name": "DownTurn", "return": float('inf'), "start": None, "end": None
|
|
59
|
+
}
|
|
60
|
+
most_sideways = {
|
|
61
|
+
"name": "SideWays",
|
|
62
|
+
"volatility": float('inf'),
|
|
63
|
+
"return": None,
|
|
64
|
+
"start": None,
|
|
65
|
+
"end": None
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for i in range(len(df)):
|
|
69
|
+
start_time = df.index[i]
|
|
70
|
+
end_time = start_time + window
|
|
71
|
+
window_df = df[(df.index >= start_time) & (df.index <= end_time)]
|
|
72
|
+
|
|
73
|
+
if len(window_df) < 2 or (window_df.index[-1] - start_time) < window:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
start_price = window_df['Close'].iloc[0]
|
|
77
|
+
end_price = window_df['Close'].iloc[-1]
|
|
78
|
+
ret = (end_price / start_price) - 1 # relative return
|
|
79
|
+
volatility = window_df['Close'].std()
|
|
80
|
+
|
|
81
|
+
# Ensure datetime for BacktestDateRange and with timezone utc
|
|
82
|
+
start_time = pd.Timestamp(start_time).to_pydatetime()
|
|
83
|
+
start_time = start_time.replace(tzinfo=timezone.utc)
|
|
84
|
+
end_time = pd.Timestamp(window_df.index[-1]).to_pydatetime()
|
|
85
|
+
end_time = end_time.replace(tzinfo=timezone.utc)
|
|
86
|
+
|
|
87
|
+
if ret > best_upturn["return"]:
|
|
88
|
+
best_upturn.update(
|
|
89
|
+
{"return": ret, "start": start_time, "end": end_time}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if ret < worst_downturn["return"]:
|
|
93
|
+
worst_downturn.update(
|
|
94
|
+
{"return": ret, "start": start_time, "end": end_time}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if volatility < most_sideways["volatility"]:
|
|
98
|
+
most_sideways.update({
|
|
99
|
+
"return": ret,
|
|
100
|
+
"volatility": volatility,
|
|
101
|
+
"start": start_time,
|
|
102
|
+
"end": end_time
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
BacktestDateRange(
|
|
107
|
+
start_date=best_upturn['start'],
|
|
108
|
+
end_date=best_upturn['end'],
|
|
109
|
+
name=best_upturn['name']
|
|
110
|
+
),
|
|
111
|
+
BacktestDateRange(
|
|
112
|
+
start_date=worst_downturn['start'],
|
|
113
|
+
end_date=worst_downturn['end'],
|
|
114
|
+
name=worst_downturn['name']
|
|
115
|
+
),
|
|
116
|
+
BacktestDateRange(
|
|
117
|
+
start_date=most_sideways['start'],
|
|
118
|
+
end_date=most_sideways['end'],
|
|
119
|
+
name=most_sideways['name']
|
|
120
|
+
)
|
|
121
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Union, Callable
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from random import Random
|
|
6
|
+
|
|
7
|
+
from investing_algorithm_framework.domain import Backtest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = getLogger("investing_algorithm_framework")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def save_backtests_to_directory(
|
|
14
|
+
backtests: List[Backtest],
|
|
15
|
+
directory_path: Union[str, Path],
|
|
16
|
+
dir_name_generation_function: Callable[[Backtest], str] = None,
|
|
17
|
+
filter_function: Callable[[Backtest], bool] = None
|
|
18
|
+
) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Saves a list of Backtest objects to the specified directory.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
backtests (List[Backtest]): List of Backtest objects to save.
|
|
24
|
+
directory_path (str): Path to the directory where backtests
|
|
25
|
+
will be saved.
|
|
26
|
+
dir_name_generation_function (Callable[[Backtest], str], optional):
|
|
27
|
+
A function that takes a Backtest object as input and returns
|
|
28
|
+
a string to be used as the directory name for that backtest.
|
|
29
|
+
If not provided, the backtest's metadata 'id' will be used.
|
|
30
|
+
Defaults to None.
|
|
31
|
+
filter_function (Callable[[Backtest], bool], optional): A function
|
|
32
|
+
that takes a Backtest object as input and returns True if the
|
|
33
|
+
backtest should be saved. Defaults to None.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
None
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
if not os.path.exists(directory_path):
|
|
40
|
+
os.makedirs(directory_path)
|
|
41
|
+
|
|
42
|
+
for backtest in backtests:
|
|
43
|
+
|
|
44
|
+
if filter_function is not None:
|
|
45
|
+
if not filter_function(backtest):
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if dir_name_generation_function is not None:
|
|
49
|
+
dir_name = dir_name_generation_function(backtest)
|
|
50
|
+
else:
|
|
51
|
+
# Check if there is an ID in the backtest metadata
|
|
52
|
+
dir_name = backtest.metadata.get('id', None)
|
|
53
|
+
|
|
54
|
+
if dir_name is None:
|
|
55
|
+
logger.warning(
|
|
56
|
+
"Backtest metadata does not contain an 'id' field. "
|
|
57
|
+
"Generating a random directory name."
|
|
58
|
+
)
|
|
59
|
+
dir_name = str(Random().randint(100000, 999999))
|
|
60
|
+
|
|
61
|
+
backtest.save(os.path.join(directory_path, dir_name))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_backtests_from_directory(
|
|
65
|
+
directory_path: Union[str, Path],
|
|
66
|
+
filter_function: Callable[[Backtest], bool] = None
|
|
67
|
+
) -> List[Backtest]:
|
|
68
|
+
"""
|
|
69
|
+
Loads Backtest objects from the specified directory.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
directory_path (str): Path to the directory from which backtests
|
|
73
|
+
will be loaded.
|
|
74
|
+
filter_function (Callable[[Backtest], bool], optional): A function
|
|
75
|
+
that takes a Backtest object as input and returns True if the
|
|
76
|
+
backtest should be included in the result. Defaults to None.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
List[Backtest]: List of loaded Backtest objects.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
backtests = []
|
|
83
|
+
|
|
84
|
+
if not os.path.exists(directory_path):
|
|
85
|
+
logger.warning(
|
|
86
|
+
f"Directory {directory_path} does not exist. "
|
|
87
|
+
"No backtests loaded."
|
|
88
|
+
)
|
|
89
|
+
return backtests
|
|
90
|
+
|
|
91
|
+
for file_name in os.listdir(directory_path):
|
|
92
|
+
file_path = os.path.join(directory_path, file_name)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
backtest = Backtest.open(file_path)
|
|
96
|
+
|
|
97
|
+
if filter_function is not None:
|
|
98
|
+
if not filter_function(backtest):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
backtests.append(backtest)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(
|
|
104
|
+
f"Failed to load backtest from {file_path}: {e}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return backtests
|
|
@@ -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
|
+
)
|