investing-algorithm-framework 1.5__py3-none-any.whl → 7.25.6__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.
- investing_algorithm_framework/__init__.py +192 -16
- investing_algorithm_framework/analysis/__init__.py +16 -0
- investing_algorithm_framework/analysis/backtest_data_ranges.py +202 -0
- investing_algorithm_framework/analysis/data.py +170 -0
- investing_algorithm_framework/analysis/markdown.py +91 -0
- investing_algorithm_framework/analysis/ranking.py +298 -0
- investing_algorithm_framework/app/__init__.py +29 -4
- investing_algorithm_framework/app/algorithm/__init__.py +7 -0
- investing_algorithm_framework/app/algorithm/algorithm.py +193 -0
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +118 -0
- investing_algorithm_framework/app/app.py +2220 -379
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1724 -0
- investing_algorithm_framework/app/eventloop.py +620 -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/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/action_handlers/__init__.py +6 -3
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +2 -1
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
- investing_algorithm_framework/app/strategy.py +867 -60
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/app/web/__init__.py +2 -1
- investing_algorithm_framework/app/web/controllers/__init__.py +2 -2
- investing_algorithm_framework/app/web/controllers/orders.py +3 -2
- investing_algorithm_framework/app/web/controllers/positions.py +2 -2
- investing_algorithm_framework/app/web/create_app.py +4 -2
- investing_algorithm_framework/app/web/schemas/position.py +1 -0
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +231 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -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/cli/validate_backtest_checkpoints.py +197 -0
- investing_algorithm_framework/create_app.py +40 -7
- investing_algorithm_framework/dependency_container.py +100 -47
- investing_algorithm_framework/domain/__init__.py +97 -30
- investing_algorithm_framework/domain/algorithm_id.py +69 -0
- investing_algorithm_framework/domain/backtesting/__init__.py +25 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +548 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +113 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +241 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +470 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +663 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/backtest_utils.py +198 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +392 -0
- investing_algorithm_framework/domain/config.py +59 -136
- investing_algorithm_framework/domain/constants.py +18 -37
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +42 -0
- investing_algorithm_framework/domain/exceptions.py +51 -1
- investing_algorithm_framework/domain/models/__init__.py +26 -19
- investing_algorithm_framework/domain/models/app_mode.py +34 -0
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +222 -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 +3 -4
- investing_algorithm_framework/domain/models/order/order.py +198 -65
- investing_algorithm_framework/domain/models/order/order_status.py +2 -2
- investing_algorithm_framework/domain/models/order/order_type.py +1 -3
- investing_algorithm_framework/domain/models/portfolio/__init__.py +6 -2
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +98 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +37 -43
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +20 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/position/position_snapshot.py +0 -2
- 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 +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -141
- investing_algorithm_framework/domain/models/time_frame.py +94 -98
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +66 -2
- 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 +11 -0
- investing_algorithm_framework/domain/models/trade/trade.py +389 -0
- investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -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/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +15 -5
- investing_algorithm_framework/domain/utils/csv.py +22 -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 +29 -0
- investing_algorithm_framework/download_data.py +244 -0
- investing_algorithm_framework/infrastructure/__init__.py +37 -11
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1152 -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 +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
- investing_algorithm_framework/infrastructure/models/__init__.py +7 -3
- investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -2
- investing_algorithm_framework/infrastructure/models/order/order.py +53 -53
- 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 +1 -1
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -2
- investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -6
- investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +3 -1
- 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 +59 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -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 +10 -4
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +16 -5
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +2 -2
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +84 -30
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +9 -4
- investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
- investing_algorithm_framework/infrastructure/services/aws/state_handler.py +193 -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/infrastructure/services/backtesting/__init__.py +9 -0
- investing_algorithm_framework/infrastructure/services/backtesting/backtest_service.py +2596 -0
- investing_algorithm_framework/infrastructure/services/backtesting/event_backtest_service.py +285 -0
- investing_algorithm_framework/infrastructure/services/backtesting/vector_backtest_service.py +468 -0
- investing_algorithm_framework/services/__init__.py +123 -15
- investing_algorithm_framework/services/configuration_service.py +77 -11
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +1058 -0
- investing_algorithm_framework/services/market_credential_service.py +40 -0
- investing_algorithm_framework/services/metrics/__init__.py +119 -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 +218 -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 +84 -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 +156 -0
- investing_algorithm_framework/services/metrics/trades.py +473 -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 +118 -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/{portfolio_configuration_service.py → portfolios/portfolio_configuration_service.py} +27 -12
- 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/repository_service.py +8 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +117 -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 +9 -0
- investing_algorithm_framework/services/trade_service/trade_service.py +1099 -0
- 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.25.6.dist-info/METADATA +535 -0
- investing_algorithm_framework-7.25.6.dist-info/RECORD +268 -0
- {investing_algorithm_framework-1.5.dist-info → investing_algorithm_framework-7.25.6.dist-info}/WHEEL +1 -2
- investing_algorithm_framework-7.25.6.dist-info/entry_points.txt +3 -0
- investing_algorithm_framework/app/algorithm.py +0 -630
- investing_algorithm_framework/domain/models/backtest_profile.py +0 -414
- investing_algorithm_framework/domain/models/market_data/__init__.py +0 -11
- investing_algorithm_framework/domain/models/market_data/asset_price.py +0 -50
- investing_algorithm_framework/domain/models/market_data/ohlcv.py +0 -105
- investing_algorithm_framework/domain/models/market_data/order_book.py +0 -63
- investing_algorithm_framework/domain/models/market_data/ticker.py +0 -92
- investing_algorithm_framework/domain/models/order/order_fee.py +0 -45
- investing_algorithm_framework/domain/models/trade.py +0 -78
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/singleton.py +0 -9
- investing_algorithm_framework/domain/utils/backtesting.py +0 -82
- investing_algorithm_framework/infrastructure/models/order/order_fee.py +0 -21
- investing_algorithm_framework/infrastructure/repositories/order_fee_repository.py +0 -15
- investing_algorithm_framework/infrastructure/services/market_backtest_service.py +0 -360
- investing_algorithm_framework/infrastructure/services/market_service.py +0 -410
- investing_algorithm_framework/infrastructure/services/performance_service.py +0 -192
- investing_algorithm_framework/services/backtest_service.py +0 -268
- investing_algorithm_framework/services/market_data_service.py +0 -77
- investing_algorithm_framework/services/order_backtest_service.py +0 -122
- investing_algorithm_framework/services/order_service.py +0 -752
- investing_algorithm_framework/services/portfolio_service.py +0 -164
- investing_algorithm_framework/services/portfolio_snapshot_service.py +0 -68
- investing_algorithm_framework/services/position_cost_service.py +0 -5
- investing_algorithm_framework/services/position_service.py +0 -63
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -225
- investing_algorithm_framework-1.5.dist-info/AUTHORS.md +0 -8
- investing_algorithm_framework-1.5.dist-info/METADATA +0 -230
- investing_algorithm_framework-1.5.dist-info/RECORD +0 -119
- investing_algorithm_framework-1.5.dist-info/top_level.txt +0 -1
- /investing_algorithm_framework/{infrastructure/services/performance_backtest_service.py → app/reporting/tables/stop_loss_table.py} +0 -0
- /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
- {investing_algorithm_framework-1.5.dist-info → investing_algorithm_framework-7.25.6.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High exposure (>1) means you’re deploying capital aggressively, possibly with many simultaneous positions.
|
|
3
|
+
|
|
4
|
+
Exposure around 1 means capital is nearly fully invested most of the time, but not overlapping.
|
|
5
|
+
|
|
6
|
+
Low exposure (<1) means capital is mostly idle or only partially invested.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from investing_algorithm_framework.domain import Trade
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_exposure_ratio(
|
|
16
|
+
trades: List["Trade"], start_date: datetime, end_date: datetime
|
|
17
|
+
) -> float:
|
|
18
|
+
"""
|
|
19
|
+
Calculates the exposure ratio (time in market) as the fraction of the total
|
|
20
|
+
backtest duration where at least one position was open.
|
|
21
|
+
|
|
22
|
+
Unlike cumulative exposure, overlapping trades are not double-counted.
|
|
23
|
+
The result is always between 0 and 1.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
trades (List[Trade]): List of trades executed during the backtest.
|
|
27
|
+
start_date (datetime): The start date of the backtest.
|
|
28
|
+
end_date (datetime): The end date of the backtest.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A float between 0 and 1 representing the exposure ratio.
|
|
32
|
+
"""
|
|
33
|
+
if not trades:
|
|
34
|
+
return 0.0
|
|
35
|
+
|
|
36
|
+
# Collect trade intervals
|
|
37
|
+
intervals = []
|
|
38
|
+
for trade in trades:
|
|
39
|
+
entry = max(trade.opened_at, start_date)
|
|
40
|
+
exit = min(trade.closed_at or end_date, end_date)
|
|
41
|
+
if exit > entry:
|
|
42
|
+
intervals.append((entry, exit))
|
|
43
|
+
|
|
44
|
+
if not intervals:
|
|
45
|
+
return 0.0
|
|
46
|
+
|
|
47
|
+
# Sort intervals by start time
|
|
48
|
+
intervals.sort(key=lambda x: x[0])
|
|
49
|
+
|
|
50
|
+
# Merge overlapping intervals
|
|
51
|
+
merged = []
|
|
52
|
+
current_start, current_end = intervals[0]
|
|
53
|
+
for start, end in intervals[1:]:
|
|
54
|
+
if start <= current_end: # overlap
|
|
55
|
+
current_end = max(current_end, end)
|
|
56
|
+
else:
|
|
57
|
+
merged.append((current_start, current_end))
|
|
58
|
+
current_start, current_end = start, end
|
|
59
|
+
merged.append((current_start, current_end))
|
|
60
|
+
|
|
61
|
+
# Total time with at least one open trade
|
|
62
|
+
total_exposed_time = sum((end - start for start, end in merged), timedelta(0))
|
|
63
|
+
|
|
64
|
+
backtest_duration = end_date - start_date
|
|
65
|
+
if backtest_duration.total_seconds() == 0:
|
|
66
|
+
return 0.0
|
|
67
|
+
|
|
68
|
+
return total_exposed_time.total_seconds() \
|
|
69
|
+
/ backtest_duration.total_seconds()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_cumulative_exposure(
|
|
73
|
+
trades: List[Trade], start_date: datetime, end_date: datetime
|
|
74
|
+
) -> float:
|
|
75
|
+
"""
|
|
76
|
+
Calculates the exposure time as a fraction of the total backtest duration
|
|
77
|
+
that the strategy had capital deployed (i.e., at least one open position).
|
|
78
|
+
|
|
79
|
+
This value can be greater than 1 if the strategy had overlapping trades.
|
|
80
|
+
For example, if the strategy had two trades open at the same time,
|
|
81
|
+
the exposure factor would be 2.0, indicating that capital was deployed
|
|
82
|
+
for twice the duration of the backtest period.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
trades (List[Trade]): List of trades executed during the backtest.
|
|
86
|
+
start_date (datetime): The start date.
|
|
87
|
+
end_date (datetime): The end date.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A float representing the exposure factor, which is the fraction of time
|
|
91
|
+
the strategy had capital deployed during the backtest period.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
if not trades:
|
|
95
|
+
return 0.0
|
|
96
|
+
|
|
97
|
+
total_trade_duration = timedelta(0)
|
|
98
|
+
for trade in trades:
|
|
99
|
+
entry = trade.opened_at
|
|
100
|
+
exit = trade.closed_at or end_date # open trades counted up to end
|
|
101
|
+
|
|
102
|
+
if exit > entry:
|
|
103
|
+
total_trade_duration += exit - entry
|
|
104
|
+
|
|
105
|
+
backtest_duration = end_date - start_date
|
|
106
|
+
|
|
107
|
+
if backtest_duration.total_seconds() == 0:
|
|
108
|
+
return 0.0
|
|
109
|
+
|
|
110
|
+
return (total_trade_duration.total_seconds()
|
|
111
|
+
/ backtest_duration.total_seconds())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_average_trade_duration(trades: List[Trade]):
|
|
115
|
+
"""
|
|
116
|
+
Calculates the average duration of trades in the backtest report.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
trades (List[Trade]): List of trades executed during the backtest.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
A float representing the average trade duration in hours.
|
|
123
|
+
"""
|
|
124
|
+
if not trades:
|
|
125
|
+
return 0.0
|
|
126
|
+
|
|
127
|
+
total_duration = 0
|
|
128
|
+
|
|
129
|
+
for trade in trades:
|
|
130
|
+
trade_duration = trade.duration
|
|
131
|
+
|
|
132
|
+
if trade_duration is not None:
|
|
133
|
+
total_duration += trade_duration
|
|
134
|
+
|
|
135
|
+
average_trade_duration = total_duration / len(trades)
|
|
136
|
+
return average_trade_duration
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_trade_frequency(
|
|
140
|
+
trades: List[Trade], start_date: datetime, end_date: datetime
|
|
141
|
+
) -> float:
|
|
142
|
+
"""
|
|
143
|
+
Calculates the trade frequency as the number of trades per day
|
|
144
|
+
during the backtest period.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
trades (List[Trade]): List of trades executed during the backtest.
|
|
148
|
+
start_date (datetime): The start date of the backtest.
|
|
149
|
+
end_date (datetime): The end date of the backtest.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
A float representing the average number of trades per day.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
if not trades:
|
|
156
|
+
return 0.0
|
|
157
|
+
|
|
158
|
+
total_days = (end_date - start_date).days + 1
|
|
159
|
+
if total_days <= 0:
|
|
160
|
+
return 0.0
|
|
161
|
+
|
|
162
|
+
return len(trades) / total_days
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_trades_per_day(
|
|
166
|
+
trades: List[Trade], start_date: datetime, end_date: datetime
|
|
167
|
+
) -> float:
|
|
168
|
+
"""
|
|
169
|
+
Calculates the average number of trades per day during the backtest period.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
trades (List[Trade]): List of trades executed during the backtest.
|
|
173
|
+
start_date (datetime): The start date of the backtest.
|
|
174
|
+
end_date (datetime): The end date of the backtest.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A float representing the average number of trades per day.
|
|
178
|
+
"""
|
|
179
|
+
if not trades:
|
|
180
|
+
return 0.0
|
|
181
|
+
|
|
182
|
+
total_days = (end_date - start_date).days + 1
|
|
183
|
+
if total_days <= 0:
|
|
184
|
+
return 0.0
|
|
185
|
+
|
|
186
|
+
return len(trades) / total_days
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_trades_per_year(
|
|
190
|
+
trades: List[Trade], start_date: datetime, end_date: datetime
|
|
191
|
+
) -> float:
|
|
192
|
+
"""
|
|
193
|
+
Calculates the average number of trades per year during the backtest period.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
trades (List[Trade]): List of trades executed during the backtest.
|
|
197
|
+
start_date (datetime): The start date of the backtest.
|
|
198
|
+
end_date (datetime): The end date of the backtest.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
A float representing the average number of trades per year.
|
|
202
|
+
"""
|
|
203
|
+
if not trades:
|
|
204
|
+
return 0.0
|
|
205
|
+
|
|
206
|
+
total_years = (end_date - start_date).days / 365.25
|
|
207
|
+
if total_years <= 0:
|
|
208
|
+
return 0.0
|
|
209
|
+
|
|
210
|
+
return len(trades) / total_years
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
|
|
4
|
+
from investing_algorithm_framework.domain import BacktestMetrics, \
|
|
5
|
+
BacktestRun, OperationalException, Backtest, BacktestDateRange
|
|
6
|
+
from .cagr import get_cagr
|
|
7
|
+
from .calmar_ratio import get_calmar_ratio
|
|
8
|
+
from .drawdown import get_drawdown_series, get_max_drawdown, \
|
|
9
|
+
get_max_daily_drawdown, get_max_drawdown_absolute, \
|
|
10
|
+
get_max_drawdown_duration
|
|
11
|
+
from .equity_curve import get_equity_curve
|
|
12
|
+
from .exposure import get_exposure_ratio, get_cumulative_exposure, \
|
|
13
|
+
get_trades_per_year, get_trades_per_day
|
|
14
|
+
from .profit_factor import get_profit_factor, get_gross_loss, get_gross_profit
|
|
15
|
+
from .returns import get_monthly_returns, get_yearly_returns, \
|
|
16
|
+
get_worst_year, get_best_year, get_best_month, get_worst_month, \
|
|
17
|
+
get_percentage_winning_months, get_percentage_winning_years, \
|
|
18
|
+
get_average_monthly_return, get_average_monthly_return_winning_months, \
|
|
19
|
+
get_average_monthly_return_losing_months, get_cumulative_return, \
|
|
20
|
+
get_cumulative_return_series
|
|
21
|
+
from .returns import get_total_return, get_final_value, get_total_loss, \
|
|
22
|
+
get_total_growth
|
|
23
|
+
from .sharpe_ratio import get_sharpe_ratio, get_rolling_sharpe_ratio
|
|
24
|
+
from .sortino_ratio import get_sortino_ratio
|
|
25
|
+
from .volatility import get_annual_volatility
|
|
26
|
+
from .win_rate import get_win_rate, get_win_loss_ratio, get_current_win_rate, \
|
|
27
|
+
get_current_win_loss_ratio
|
|
28
|
+
from .trades import get_average_trade_duration, get_average_trade_size, \
|
|
29
|
+
get_number_of_trades, get_positive_trades, get_number_of_closed_trades, \
|
|
30
|
+
get_negative_trades, get_average_trade_return, get_number_of_open_trades, \
|
|
31
|
+
get_worst_trade, get_best_trade, get_average_trade_gain, \
|
|
32
|
+
get_average_trade_loss, get_median_trade_return, \
|
|
33
|
+
get_current_average_trade_gain, get_current_average_trade_return, \
|
|
34
|
+
get_current_average_trade_duration, get_current_average_trade_loss
|
|
35
|
+
|
|
36
|
+
logger = getLogger("investing_algorithm_framework")
|
|
37
|
+
|
|
38
|
+
def create_backtest_metrics_for_backtest(
|
|
39
|
+
backtest: Backtest,
|
|
40
|
+
risk_free_rate: float, metrics: List[str] = None,
|
|
41
|
+
backtest_date_range: BacktestDateRange = None
|
|
42
|
+
) -> Backtest:
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
Create BacktestMetrics for a Backtest object.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
backtest (Backtest): The Backtest object containing
|
|
49
|
+
backtest runs.
|
|
50
|
+
risk_free_rate (float): The risk-free rate used in certain
|
|
51
|
+
metric calculations.
|
|
52
|
+
metrics (List[str], optional): List of metric names to compute.
|
|
53
|
+
If None, a default set of metrics will be computed.
|
|
54
|
+
backtest_date_range (BacktestDateRange, optional): The date range
|
|
55
|
+
for the backtest. If None, all backtest metrics will be computed
|
|
56
|
+
for each backtest run.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Backtest: The Backtest object with computed metrics for each run.
|
|
60
|
+
"""
|
|
61
|
+
if backtest_date_range is not None:
|
|
62
|
+
backtest_runs = [
|
|
63
|
+
backtest.get_backtest_run(backtest_date_range)
|
|
64
|
+
]
|
|
65
|
+
else:
|
|
66
|
+
backtest_runs = backtest.get_all_backtest_runs()
|
|
67
|
+
|
|
68
|
+
for backtest_run in backtest_runs:
|
|
69
|
+
# If a date range is provided, check if the backtest run falls
|
|
70
|
+
# within the range
|
|
71
|
+
backtest_metrics = create_backtest_metrics(
|
|
72
|
+
backtest_run, risk_free_rate, metrics
|
|
73
|
+
)
|
|
74
|
+
backtest_run.backtest_metrics = backtest_metrics
|
|
75
|
+
|
|
76
|
+
backtest.backtest_runs = backtest_runs
|
|
77
|
+
return backtest
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_backtest_metrics(
|
|
81
|
+
backtest_run: BacktestRun, risk_free_rate: float, metrics: List[str] = None
|
|
82
|
+
) -> BacktestMetrics:
|
|
83
|
+
"""
|
|
84
|
+
Create a BacktestMetrics instance and optionally save it to a file.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
backtest_run (BacktestRun): The BacktestRun object containing
|
|
88
|
+
portfolio snapshots and trades.
|
|
89
|
+
risk_free_rate (float): The risk-free rate used in certain
|
|
90
|
+
metric calculations.
|
|
91
|
+
metrics (List[str], optional): List of metric names to compute.
|
|
92
|
+
If None, a default set of metrics will be computed.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
BacktestMetrics: The computed backtest metrics.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
if metrics is None:
|
|
99
|
+
metrics = [
|
|
100
|
+
"backtest_start_date",
|
|
101
|
+
"backtest_end_date",
|
|
102
|
+
"equity_curve",
|
|
103
|
+
"final_value",
|
|
104
|
+
"total_growth",
|
|
105
|
+
"total_growth_percentage",
|
|
106
|
+
"total_net_gain",
|
|
107
|
+
"total_net_gain_percentage",
|
|
108
|
+
"total_loss",
|
|
109
|
+
"total_loss_percentage",
|
|
110
|
+
"cumulative_return",
|
|
111
|
+
"cumulative_return_series",
|
|
112
|
+
"cagr",
|
|
113
|
+
"sharpe_ratio",
|
|
114
|
+
"rolling_sharpe_ratio",
|
|
115
|
+
"sortino_ratio",
|
|
116
|
+
"calmar_ratio",
|
|
117
|
+
"profit_factor",
|
|
118
|
+
"annual_volatility",
|
|
119
|
+
"monthly_returns",
|
|
120
|
+
"yearly_returns",
|
|
121
|
+
"drawdown_series",
|
|
122
|
+
"max_drawdown",
|
|
123
|
+
"max_drawdown_absolute",
|
|
124
|
+
"max_daily_drawdown",
|
|
125
|
+
"max_drawdown_duration",
|
|
126
|
+
"trades_per_year",
|
|
127
|
+
"trade_per_day",
|
|
128
|
+
"exposure_ratio",
|
|
129
|
+
"cumulative_exposure",
|
|
130
|
+
"best_trade",
|
|
131
|
+
"worst_trade",
|
|
132
|
+
"number_of_positive_trades",
|
|
133
|
+
"percentage_positive_trades",
|
|
134
|
+
"number_of_negative_trades",
|
|
135
|
+
"percentage_negative_trades",
|
|
136
|
+
"average_trade_duration",
|
|
137
|
+
"average_trade_size",
|
|
138
|
+
"average_trade_loss",
|
|
139
|
+
"average_trade_loss_percentage",
|
|
140
|
+
"average_trade_gain",
|
|
141
|
+
"average_trade_gain_percentage",
|
|
142
|
+
"average_trade_return",
|
|
143
|
+
"average_trade_return_percentage",
|
|
144
|
+
"median_trade_return",
|
|
145
|
+
"number_of_trades",
|
|
146
|
+
"number_of_trades_closed",
|
|
147
|
+
"number_of_trades_opened",
|
|
148
|
+
"number_of_trades_open_at_end",
|
|
149
|
+
"win_rate",
|
|
150
|
+
"current_win_rate",
|
|
151
|
+
"win_loss_ratio",
|
|
152
|
+
"current_win_loss_ratio",
|
|
153
|
+
"percentage_winning_months",
|
|
154
|
+
"percentage_winning_years",
|
|
155
|
+
"average_monthly_return",
|
|
156
|
+
"average_monthly_return_losing_months",
|
|
157
|
+
"average_monthly_return_winning_months",
|
|
158
|
+
"best_month",
|
|
159
|
+
"best_year",
|
|
160
|
+
"worst_month",
|
|
161
|
+
"worst_year",
|
|
162
|
+
"total_number_of_days",
|
|
163
|
+
"current_average_trade_gain",
|
|
164
|
+
"current_average_trade_return",
|
|
165
|
+
"current_average_trade_duration",
|
|
166
|
+
"current_average_trade_loss",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
backtest_metrics = BacktestMetrics(
|
|
170
|
+
backtest_start_date=backtest_run.backtest_start_date,
|
|
171
|
+
backtest_end_date=backtest_run.backtest_end_date,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def safe_set(metric_name, func, *args, index=None):
|
|
175
|
+
if metric_name in metrics:
|
|
176
|
+
try:
|
|
177
|
+
value = func(*args)
|
|
178
|
+
if index is not None and isinstance(value, (list, tuple)):
|
|
179
|
+
setattr(backtest_metrics, metric_name, value[index])
|
|
180
|
+
else:
|
|
181
|
+
setattr(backtest_metrics, metric_name, value)
|
|
182
|
+
except OperationalException as e:
|
|
183
|
+
logger.warning(f"{metric_name} failed: {e}")
|
|
184
|
+
|
|
185
|
+
# Grouped metrics needing special handling
|
|
186
|
+
if "total_net_gain" in metrics or "total_net_gain_percentage" in metrics:
|
|
187
|
+
try:
|
|
188
|
+
total_return = get_total_return(backtest_run.portfolio_snapshots)
|
|
189
|
+
if "total_net_gain" in metrics:
|
|
190
|
+
backtest_metrics.total_net_gain = total_return[0]
|
|
191
|
+
if "total_net_gain_percentage" in metrics:
|
|
192
|
+
backtest_metrics.total_net_gain_percentage = total_return[1]
|
|
193
|
+
except OperationalException as e:
|
|
194
|
+
logger.warning(f"total_return failed: {e}")
|
|
195
|
+
|
|
196
|
+
if "total_growth" in metrics or "total_growth_percentage" in metrics:
|
|
197
|
+
try:
|
|
198
|
+
total_growth = get_total_growth(backtest_run.portfolio_snapshots)
|
|
199
|
+
if "total_growth" in metrics:
|
|
200
|
+
backtest_metrics.total_growth = total_growth[0]
|
|
201
|
+
if "total_growth_percentage" in metrics:
|
|
202
|
+
backtest_metrics.total_growth_percentage = total_growth[1]
|
|
203
|
+
except OperationalException as e:
|
|
204
|
+
logger.warning(f"total_growth failed: {e}")
|
|
205
|
+
|
|
206
|
+
if "total_loss" in metrics or "total_loss_percentage" in metrics:
|
|
207
|
+
try:
|
|
208
|
+
total_loss = get_total_loss(backtest_run.portfolio_snapshots)
|
|
209
|
+
if "total_loss" in metrics:
|
|
210
|
+
backtest_metrics.total_loss = total_loss[0]
|
|
211
|
+
if "total_loss_percentage" in metrics:
|
|
212
|
+
backtest_metrics.total_loss_percentage = total_loss[1]
|
|
213
|
+
except OperationalException as e:
|
|
214
|
+
logger.warning(f"total_loss failed: {e}")
|
|
215
|
+
|
|
216
|
+
if ("average_trade_return" in metrics
|
|
217
|
+
or "average_trade_return_percentage" in metrics):
|
|
218
|
+
try:
|
|
219
|
+
avg_return = get_average_trade_return(backtest_run.trades)
|
|
220
|
+
if "average_trade_return" in metrics:
|
|
221
|
+
backtest_metrics.average_trade_return = avg_return[0]
|
|
222
|
+
if "average_trade_return_percentage" in metrics:
|
|
223
|
+
backtest_metrics.average_trade_return_percentage = \
|
|
224
|
+
avg_return[1]
|
|
225
|
+
except OperationalException as e:
|
|
226
|
+
logger.warning(f"average_trade_return failed: {e}")
|
|
227
|
+
|
|
228
|
+
if ("average_trade_gain" in metrics
|
|
229
|
+
or "average_trade_gain_percentage" in metrics):
|
|
230
|
+
try:
|
|
231
|
+
avg_gain = get_average_trade_gain(backtest_run.trades)
|
|
232
|
+
if "average_trade_gain" in metrics:
|
|
233
|
+
backtest_metrics.average_trade_gain = avg_gain[0]
|
|
234
|
+
if "average_trade_gain_percentage" in metrics:
|
|
235
|
+
backtest_metrics.average_trade_gain_percentage = avg_gain[1]
|
|
236
|
+
except OperationalException as e:
|
|
237
|
+
logger.warning(f"average_trade_gain failed: {e}")
|
|
238
|
+
|
|
239
|
+
if ("average_trade_loss" in metrics
|
|
240
|
+
or "average_trade_loss_percentage" in metrics):
|
|
241
|
+
try:
|
|
242
|
+
avg_loss = get_average_trade_loss(backtest_run.trades)
|
|
243
|
+
if "average_trade_loss" in metrics:
|
|
244
|
+
backtest_metrics.average_trade_loss = avg_loss[0]
|
|
245
|
+
if "average_trade_loss_percentage" in metrics:
|
|
246
|
+
backtest_metrics.average_trade_loss_percentage = avg_loss[1]
|
|
247
|
+
except OperationalException as e:
|
|
248
|
+
logger.warning(f"average_trade_loss failed: {e}")
|
|
249
|
+
|
|
250
|
+
if ("current_average_trade_gain" in metrics
|
|
251
|
+
or "get_current_average_trade_gain_percentage" in metrics):
|
|
252
|
+
try:
|
|
253
|
+
current_avg_gain = get_current_average_trade_gain(
|
|
254
|
+
backtest_run.trades
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if "current_average_trade_gain" in metrics:
|
|
258
|
+
backtest_metrics.current_average_trade_gain = \
|
|
259
|
+
current_avg_gain[0]
|
|
260
|
+
|
|
261
|
+
if "current_average_trade_gain_percentage" in metrics:
|
|
262
|
+
backtest_metrics.current_average_trade_gain_percentage = \
|
|
263
|
+
current_avg_gain[1]
|
|
264
|
+
except OperationalException as e:
|
|
265
|
+
logger.warning(f"current_average_trade_gain failed: {e}")
|
|
266
|
+
|
|
267
|
+
if ("current_average_trade_return" in metrics
|
|
268
|
+
or "current_average_trade_return_percentage" in metrics):
|
|
269
|
+
try:
|
|
270
|
+
current_avg_return = get_current_average_trade_return(
|
|
271
|
+
backtest_run.trades
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if "current_average_trade_return" in metrics:
|
|
275
|
+
backtest_metrics.current_average_trade_return = \
|
|
276
|
+
current_avg_return[0]
|
|
277
|
+
if "current_average_trade_return_percentage" in metrics:
|
|
278
|
+
backtest_metrics.current_average_trade_return_percentage =\
|
|
279
|
+
current_avg_return[1]
|
|
280
|
+
except OperationalException as e:
|
|
281
|
+
logger.warning(f"current_average_trade_return failed: {e}")
|
|
282
|
+
|
|
283
|
+
if "current_average_trade_duration" in metrics:
|
|
284
|
+
try:
|
|
285
|
+
current_avg_duration = get_current_average_trade_duration(
|
|
286
|
+
backtest_run.trades, backtest_run
|
|
287
|
+
)
|
|
288
|
+
backtest_metrics.current_average_trade_duration = \
|
|
289
|
+
current_avg_duration
|
|
290
|
+
except OperationalException as e:
|
|
291
|
+
logger.warning(f"current_average_trade_duration failed: {e}")
|
|
292
|
+
|
|
293
|
+
if ("current_average_trade_loss" in metrics
|
|
294
|
+
or "current_average_trade_loss_percentage" in metrics):
|
|
295
|
+
try:
|
|
296
|
+
current_avg_loss = get_current_average_trade_loss(
|
|
297
|
+
backtest_run.trades
|
|
298
|
+
)
|
|
299
|
+
if "current_average_trade_loss" in metrics:
|
|
300
|
+
backtest_metrics.current_average_trade_loss = \
|
|
301
|
+
current_avg_loss[0]
|
|
302
|
+
if "current_average_trade_loss_percentage" in metrics:
|
|
303
|
+
backtest_metrics.current_average_trade_loss_percentage = \
|
|
304
|
+
current_avg_loss[1]
|
|
305
|
+
except OperationalException as e:
|
|
306
|
+
logger.warning(f"current_average_trade_loss failed: {e}")
|
|
307
|
+
|
|
308
|
+
safe_set("number_of_positive_trades", get_positive_trades, backtest_run.trades)
|
|
309
|
+
safe_set("percentage_positive_trades", get_positive_trades, backtest_run.trades, index=1)
|
|
310
|
+
safe_set("number_of_negative_trades", get_negative_trades, backtest_run.trades)
|
|
311
|
+
safe_set("percentage_negative_trades", get_negative_trades, backtest_run.trades, index=1)
|
|
312
|
+
safe_set("median_trade_return", get_median_trade_return, backtest_run.trades, index=0)
|
|
313
|
+
safe_set("median_trade_return_percentage", get_median_trade_return, backtest_run.trades, index=1)
|
|
314
|
+
safe_set("number_of_trades", get_number_of_trades, backtest_run.trades)
|
|
315
|
+
safe_set("number_of_trades_closed", get_number_of_closed_trades, backtest_run.trades)
|
|
316
|
+
safe_set("number_of_trades_opened", get_number_of_open_trades, backtest_run.trades)
|
|
317
|
+
safe_set("average_trade_duration", get_average_trade_duration, backtest_run.trades)
|
|
318
|
+
safe_set("average_trade_size", get_average_trade_size, backtest_run.trades)
|
|
319
|
+
safe_set("equity_curve", get_equity_curve, backtest_run.portfolio_snapshots)
|
|
320
|
+
safe_set("final_value", get_final_value, backtest_run.portfolio_snapshots)
|
|
321
|
+
safe_set("cagr", get_cagr, backtest_run.portfolio_snapshots)
|
|
322
|
+
safe_set("sharpe_ratio", get_sharpe_ratio, backtest_run.portfolio_snapshots, risk_free_rate)
|
|
323
|
+
safe_set("rolling_sharpe_ratio", get_rolling_sharpe_ratio, backtest_run.portfolio_snapshots, risk_free_rate)
|
|
324
|
+
safe_set("sortino_ratio", get_sortino_ratio, backtest_run.portfolio_snapshots, risk_free_rate)
|
|
325
|
+
safe_set("profit_factor", get_profit_factor, backtest_run.trades)
|
|
326
|
+
safe_set("calmar_ratio", get_calmar_ratio, backtest_run.portfolio_snapshots)
|
|
327
|
+
safe_set("annual_volatility", get_annual_volatility, backtest_run.portfolio_snapshots)
|
|
328
|
+
safe_set("monthly_returns", get_monthly_returns, backtest_run.portfolio_snapshots)
|
|
329
|
+
safe_set("yearly_returns", get_yearly_returns, backtest_run.portfolio_snapshots)
|
|
330
|
+
safe_set("drawdown_series", get_drawdown_series, backtest_run.portfolio_snapshots)
|
|
331
|
+
safe_set("max_drawdown", get_max_drawdown, backtest_run.portfolio_snapshots)
|
|
332
|
+
safe_set("max_drawdown_absolute", get_max_drawdown_absolute, backtest_run.portfolio_snapshots)
|
|
333
|
+
safe_set("max_daily_drawdown", get_max_daily_drawdown, backtest_run.portfolio_snapshots)
|
|
334
|
+
safe_set("max_drawdown_duration", get_max_drawdown_duration, backtest_run.portfolio_snapshots)
|
|
335
|
+
safe_set("trades_per_year", get_trades_per_year, backtest_run.trades, backtest_run.backtest_start_date, backtest_run.backtest_end_date)
|
|
336
|
+
safe_set("trades_per_day", get_trades_per_day, backtest_run.trades, backtest_run.backtest_start_date, backtest_run.backtest_end_date)
|
|
337
|
+
safe_set("exposure_ratio", get_exposure_ratio, backtest_run.trades, backtest_run.backtest_start_date, backtest_run.backtest_end_date)
|
|
338
|
+
safe_set("cumulative_exposure", get_cumulative_exposure, backtest_run.trades, backtest_run.backtest_start_date, backtest_run.backtest_end_date)
|
|
339
|
+
safe_set("best_trade", get_best_trade, backtest_run.trades)
|
|
340
|
+
safe_set("worst_trade", get_worst_trade, backtest_run.trades)
|
|
341
|
+
safe_set("win_rate", get_win_rate, backtest_run.trades)
|
|
342
|
+
safe_set("current_win_rate", get_current_win_rate, backtest_run.trades)
|
|
343
|
+
safe_set("win_loss_ratio", get_win_loss_ratio, backtest_run.trades)
|
|
344
|
+
safe_set("current_win_loss_ratio", get_current_win_loss_ratio, backtest_run.trades)
|
|
345
|
+
safe_set("percentage_winning_months", get_percentage_winning_months, backtest_run.portfolio_snapshots)
|
|
346
|
+
safe_set("percentage_winning_years", get_percentage_winning_years, backtest_run.portfolio_snapshots)
|
|
347
|
+
safe_set("average_monthly_return", get_average_monthly_return, backtest_run.portfolio_snapshots)
|
|
348
|
+
safe_set("average_monthly_return_winning_months", get_average_monthly_return_winning_months, backtest_run.portfolio_snapshots)
|
|
349
|
+
safe_set("average_monthly_return_losing_months", get_average_monthly_return_losing_months, backtest_run.portfolio_snapshots)
|
|
350
|
+
safe_set("best_month", get_best_month, backtest_run.portfolio_snapshots)
|
|
351
|
+
safe_set("best_year", get_best_year, backtest_run.portfolio_snapshots)
|
|
352
|
+
safe_set("worst_month", get_worst_month, backtest_run.portfolio_snapshots)
|
|
353
|
+
safe_set("worst_year", get_worst_year, backtest_run.portfolio_snapshots)
|
|
354
|
+
safe_set("gross_loss", get_gross_loss, backtest_run.trades)
|
|
355
|
+
safe_set("gross_profit", get_gross_profit, backtest_run.trades)
|
|
356
|
+
safe_set("cumulative_return_series", get_cumulative_return_series, backtest_run.portfolio_snapshots)
|
|
357
|
+
safe_set("cumulative_return", get_cumulative_return, backtest_run.portfolio_snapshots)
|
|
358
|
+
return backtest_metrics
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from .cagr import get_cagr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_mean_daily_return(snapshots):
|
|
8
|
+
"""
|
|
9
|
+
Calculate the mean daily return from the total value of the snapshots.
|
|
10
|
+
|
|
11
|
+
This function computes the mean daily return based on the list of
|
|
12
|
+
snapshots in the report. If the snapshots have a granularity of less
|
|
13
|
+
than a day, the function will resample to daily frequency and compute
|
|
14
|
+
average daily returns.
|
|
15
|
+
|
|
16
|
+
If there is less data then for a year, it will use cagr to
|
|
17
|
+
calculate the mean daily return.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
float: The mean daily return.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if len(snapshots) < 2:
|
|
27
|
+
return 0.0 # Not enough data
|
|
28
|
+
|
|
29
|
+
# Create DataFrame from snapshots
|
|
30
|
+
data = [(s.created_at, s.total_value) for s in snapshots]
|
|
31
|
+
df = pd.DataFrame(data, columns=["created_at", "total_value"])
|
|
32
|
+
df['created_at'] = pd.to_datetime(df['created_at'])
|
|
33
|
+
df = df.sort_values('created_at').drop_duplicates('created_at')\
|
|
34
|
+
.set_index('created_at')
|
|
35
|
+
|
|
36
|
+
start_date = df.iloc[0].name
|
|
37
|
+
end_date = df.iloc[-1].name
|
|
38
|
+
|
|
39
|
+
# Check if the period is less than a year
|
|
40
|
+
if (end_date - start_date).days < 365:
|
|
41
|
+
# Use CAGR to calculate mean daily return
|
|
42
|
+
cagr = get_cagr(snapshots)
|
|
43
|
+
if cagr == 0.0:
|
|
44
|
+
return 0.0
|
|
45
|
+
|
|
46
|
+
return (1 + cagr) ** (1 / 365) - 1
|
|
47
|
+
|
|
48
|
+
# Resample to daily frequency using last value of the day
|
|
49
|
+
daily_df = df.resample('1D').last().dropna()
|
|
50
|
+
|
|
51
|
+
# Calculate daily returns
|
|
52
|
+
daily_df['return'] = daily_df['total_value'].pct_change()
|
|
53
|
+
daily_df = daily_df.dropna()
|
|
54
|
+
|
|
55
|
+
if daily_df.empty:
|
|
56
|
+
return 0.0
|
|
57
|
+
|
|
58
|
+
mean_return = daily_df['return'].mean()
|
|
59
|
+
|
|
60
|
+
if np.isnan(mean_return):
|
|
61
|
+
return 0.0
|
|
62
|
+
|
|
63
|
+
return mean_return
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_mean_yearly_return(report, periods_per_year=365):
|
|
67
|
+
"""
|
|
68
|
+
Calculate the mean yearly return from a backtest report by
|
|
69
|
+
annualizing the mean daily return.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
report (BacktestReport): The report containing the snapshots.
|
|
73
|
+
periods_per_year (int): Number of periods in a year (e.g., 365 for daily data).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
float: The mean yearly return (annualized).
|
|
77
|
+
"""
|
|
78
|
+
mean_daily_return = get_mean_daily_return(report)
|
|
79
|
+
|
|
80
|
+
if mean_daily_return == 0.0:
|
|
81
|
+
return 0.0
|
|
82
|
+
|
|
83
|
+
return (1 + mean_daily_return) ** periods_per_year - 1
|
|
84
|
+
|