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,548 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from typing import Dict, Union, List
|
|
7
|
+
|
|
8
|
+
from investing_algorithm_framework.domain.exceptions \
|
|
9
|
+
import OperationalException
|
|
10
|
+
|
|
11
|
+
from .backtest_metrics import BacktestMetrics
|
|
12
|
+
from .backtest_run import BacktestRun
|
|
13
|
+
from .backtest_permutation_test import BacktestPermutationTest
|
|
14
|
+
from .backtest_date_range import BacktestDateRange
|
|
15
|
+
from .backtest_summary_metrics import BacktestSummaryMetrics
|
|
16
|
+
from .combine_backtests import generate_backtest_summary_metrics
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Backtest:
|
|
24
|
+
"""
|
|
25
|
+
Represents a backtest of an algorithm. It contains the backtest metrics,
|
|
26
|
+
backtest results, and paths to strategy and data files.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
backtest_runs (List[BacktestRun]): A list of backtest runs,
|
|
30
|
+
each representing the performance metrics of a single
|
|
31
|
+
backtest run.
|
|
32
|
+
backtest_summary (BacktestSummaryMetrics): An aggregated view of
|
|
33
|
+
the backtest metrics, combining results from multiple backtests
|
|
34
|
+
metrics into a single summary.
|
|
35
|
+
backtest_permutation_tests (List[BacktestPermutationTestMetrics]): A
|
|
36
|
+
list of backtest permutation tests,
|
|
37
|
+
each representing the performance metrics of a single
|
|
38
|
+
backtest permutation test.
|
|
39
|
+
metadata (Dict[str, str]): Metadata related to the backtest, such as
|
|
40
|
+
configuration parameters or additional information about the
|
|
41
|
+
strategy that was backtested. This can be used for later
|
|
42
|
+
reference or analysis.
|
|
43
|
+
risk_free_rate (float): The risk-free rate used in the backtest,
|
|
44
|
+
typically expressed as a decimal (e.g., 0.03 for 3%). This
|
|
45
|
+
strategy_ids (List[int]): List of strategy IDs associated with
|
|
46
|
+
this backtest.
|
|
47
|
+
algorithm_id (int): The ID of the algorithm associated with this
|
|
48
|
+
backtest.
|
|
49
|
+
"""
|
|
50
|
+
algorithm_id: str
|
|
51
|
+
backtest_runs: List[BacktestRun] = field(default_factory=list)
|
|
52
|
+
backtest_summary: BacktestSummaryMetrics = field(default=None)
|
|
53
|
+
backtest_permutation_tests: List[BacktestPermutationTest] = \
|
|
54
|
+
field(default_factory=list)
|
|
55
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
|
56
|
+
risk_free_rate: float = None
|
|
57
|
+
strategy_ids: List[int] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
def get_all_backtest_runs(
|
|
60
|
+
self, backtest_date_ranges=None
|
|
61
|
+
) -> List[BacktestRun]:
|
|
62
|
+
"""
|
|
63
|
+
Retrieve all BacktestRun instances from the backtest.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
backtest_date_ranges (List[BacktestDateRange], optional): A list of
|
|
67
|
+
date ranges to filter the backtest runs. If provided, only
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List[BacktestRun]: A list of all BacktestRun instances.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if backtest_date_ranges is not None:
|
|
74
|
+
filtered_runs = []
|
|
75
|
+
for date_range in backtest_date_ranges:
|
|
76
|
+
run = self.get_backtest_run(date_range)
|
|
77
|
+
if run:
|
|
78
|
+
filtered_runs.append(run)
|
|
79
|
+
return filtered_runs
|
|
80
|
+
|
|
81
|
+
return self.backtest_runs
|
|
82
|
+
|
|
83
|
+
def get_backtest_run(
|
|
84
|
+
self, date_range: BacktestDateRange
|
|
85
|
+
) -> Union[BacktestRun, None]:
|
|
86
|
+
"""
|
|
87
|
+
Retrieve a specific BacktestRun based on the provided date range.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
date_range (BacktestDateRange): The date range to search for.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Union[BacktestRun, None]: The matching BacktestRun if found,
|
|
94
|
+
otherwise None.
|
|
95
|
+
"""
|
|
96
|
+
for run in self.backtest_runs:
|
|
97
|
+
if (run.backtest_start_date == date_range.start_date and
|
|
98
|
+
run.backtest_end_date == date_range.end_date):
|
|
99
|
+
return run
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def get_all_backtest_permutation_tests(
|
|
103
|
+
self
|
|
104
|
+
) -> List[BacktestPermutationTest]:
|
|
105
|
+
"""
|
|
106
|
+
Retrieve all BacktestPermutationTest instances from the backtest.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List[BacktestPermutationTest]: A list of all
|
|
110
|
+
BacktestPermutationTest instances.
|
|
111
|
+
"""
|
|
112
|
+
return self.backtest_permutation_tests
|
|
113
|
+
|
|
114
|
+
def get_backtest_permutation_test(
|
|
115
|
+
self, date_range: BacktestDateRange
|
|
116
|
+
) -> Union[BacktestPermutationTest, None]:
|
|
117
|
+
"""
|
|
118
|
+
Retrieve a specific BacktestPermutationTest based on
|
|
119
|
+
the provided date range.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
date_range (BacktestDateRange): The date range to search for.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Union[BacktestPermutationTest, None]: The
|
|
126
|
+
matching BacktestPermutationTest if found,
|
|
127
|
+
otherwise None.
|
|
128
|
+
"""
|
|
129
|
+
for perm_test in self.backtest_permutation_tests:
|
|
130
|
+
if (perm_test.backtest_start_date == date_range.start_date and
|
|
131
|
+
perm_test.backtest_end_date == date_range.end_date):
|
|
132
|
+
return perm_test
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def get_all_backtest_metrics(self) -> List[BacktestMetrics]:
|
|
136
|
+
"""
|
|
137
|
+
Retrieve all BacktestMetrics from the backtest runs.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List[BacktestMetrics]: A list of BacktestMetrics from
|
|
141
|
+
all backtest runs.
|
|
142
|
+
"""
|
|
143
|
+
return [
|
|
144
|
+
run.backtest_metrics for run in self.backtest_runs
|
|
145
|
+
if run.backtest_metrics
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
def get_backtest_metrics(
|
|
149
|
+
self, date_range: BacktestDateRange
|
|
150
|
+
) -> Union[BacktestMetrics, None]:
|
|
151
|
+
"""
|
|
152
|
+
Retrieve the BacktestMetrics for a specific BacktestRun based on
|
|
153
|
+
the provided date range.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
date_range (Optional[BacktestDateRange]): The date range to
|
|
157
|
+
search for.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Union[BacktestMetrics, None]: The BacktestMetrics of the matching
|
|
161
|
+
BacktestRun if found, otherwise None.
|
|
162
|
+
"""
|
|
163
|
+
run = self.get_backtest_run(date_range)
|
|
164
|
+
if run:
|
|
165
|
+
return run.backtest_metrics
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def to_dict(self) -> dict:
|
|
169
|
+
"""
|
|
170
|
+
Convert the Backtest instance to a dictionary.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
dict: A dictionary representation of the Backtest instance.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
backtest_summary = self.backtest_summary.to_dict() \
|
|
177
|
+
if self.backtest_summary else None
|
|
178
|
+
return {
|
|
179
|
+
"backtest_runs": [
|
|
180
|
+
br.to_dict() for br in self.backtest_runs
|
|
181
|
+
] if self.backtest_runs else None,
|
|
182
|
+
"backtest_summary": backtest_summary,
|
|
183
|
+
"backtest_permutation_tests":
|
|
184
|
+
[
|
|
185
|
+
bpt.to_dict() for bpt in self.backtest_permutation_tests
|
|
186
|
+
] if self.backtest_permutation_tests else None,
|
|
187
|
+
"metadata": self.metadata,
|
|
188
|
+
"risk_free_rate": self.risk_free_rate,
|
|
189
|
+
"strategy_ids": self.strategy_ids,
|
|
190
|
+
"algorithm_id": self.algorithm_id
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def open(
|
|
195
|
+
directory_path: Union[str, Path],
|
|
196
|
+
backtest_date_ranges: List[BacktestDateRange] = None,
|
|
197
|
+
) -> 'Backtest':
|
|
198
|
+
"""
|
|
199
|
+
Open a backtest report from a directory and return a Backtest instance.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
directory_path (str): The path to the directory containing the
|
|
203
|
+
backtest report files.
|
|
204
|
+
backtest_date_ranges (List[BacktestDateRange], optional): A list of
|
|
205
|
+
date ranges to filter the backtest runs. If provided, only
|
|
206
|
+
backtest runs matching these date ranges will be loaded.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Backtest: An instance of Backtest with the loaded metrics
|
|
210
|
+
and results.
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
OperationalException: If the directory does not exist or if
|
|
214
|
+
there is an error loading the files.
|
|
215
|
+
"""
|
|
216
|
+
algorithm_id = None
|
|
217
|
+
backtest_runs = []
|
|
218
|
+
backtest_summary_metrics = None
|
|
219
|
+
permutation_metrics = []
|
|
220
|
+
metadata = {}
|
|
221
|
+
risk_free_rate = None
|
|
222
|
+
|
|
223
|
+
if not os.path.exists(directory_path):
|
|
224
|
+
raise OperationalException(
|
|
225
|
+
f"The directory {directory_path} does not exist."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if not os.path.isdir(directory_path):
|
|
229
|
+
raise OperationalException(
|
|
230
|
+
f"Backtest path {directory_path} is not a directory."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Load algorithm_id if available
|
|
234
|
+
id_file = os.path.join(directory_path, "algorithm_id.json")
|
|
235
|
+
|
|
236
|
+
if os.path.isfile(id_file):
|
|
237
|
+
with open(id_file, 'r') as f:
|
|
238
|
+
try:
|
|
239
|
+
algorithm_id = json.load(f).get('algorithm_id', None)
|
|
240
|
+
except json.JSONDecodeError as e:
|
|
241
|
+
logger.error(f"Error decoding algorithm_id JSON: {e}")
|
|
242
|
+
algorithm_id = None
|
|
243
|
+
|
|
244
|
+
# Load all backtest runs
|
|
245
|
+
runs_dir = os.path.join(directory_path, "runs")
|
|
246
|
+
|
|
247
|
+
if os.path.isdir(runs_dir):
|
|
248
|
+
for dir_name in os.listdir(runs_dir):
|
|
249
|
+
run_path = os.path.join(runs_dir, dir_name)
|
|
250
|
+
if os.path.isdir(run_path):
|
|
251
|
+
|
|
252
|
+
if backtest_date_ranges is not None:
|
|
253
|
+
temp_run = BacktestRun.open(run_path)
|
|
254
|
+
match_found = False
|
|
255
|
+
|
|
256
|
+
for date_range in backtest_date_ranges:
|
|
257
|
+
if (
|
|
258
|
+
temp_run.backtest_start_date ==
|
|
259
|
+
date_range.start_date and
|
|
260
|
+
temp_run.backtest_end_date ==
|
|
261
|
+
date_range.end_date
|
|
262
|
+
):
|
|
263
|
+
|
|
264
|
+
if date_range.name is not None:
|
|
265
|
+
if (
|
|
266
|
+
temp_run.backtest_date_range_name ==
|
|
267
|
+
date_range.name
|
|
268
|
+
):
|
|
269
|
+
match_found = True
|
|
270
|
+
break
|
|
271
|
+
else:
|
|
272
|
+
match_found = True
|
|
273
|
+
break
|
|
274
|
+
|
|
275
|
+
if not match_found:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
backtest_runs.append(BacktestRun.open(run_path))
|
|
279
|
+
|
|
280
|
+
# Load combined backtests summary
|
|
281
|
+
if backtest_date_ranges is not None:
|
|
282
|
+
summary_file = os.path.join(directory_path, "summary.json")
|
|
283
|
+
|
|
284
|
+
if os.path.isfile(summary_file):
|
|
285
|
+
backtest_summary_metrics = \
|
|
286
|
+
BacktestSummaryMetrics.open(summary_file)
|
|
287
|
+
else:
|
|
288
|
+
# Generate new summary from loaded backtest runs
|
|
289
|
+
temp_metrics = []
|
|
290
|
+
for br in backtest_runs:
|
|
291
|
+
if br.backtest_metrics:
|
|
292
|
+
temp_metrics.append(br.backtest_metrics)
|
|
293
|
+
|
|
294
|
+
backtest_summary_metrics = \
|
|
295
|
+
generate_backtest_summary_metrics(temp_metrics)
|
|
296
|
+
|
|
297
|
+
# Load backtest permutation test metrics
|
|
298
|
+
perm_test_dir = os.path.join(directory_path, "permutation_tests")
|
|
299
|
+
|
|
300
|
+
if os.path.isdir(perm_test_dir):
|
|
301
|
+
for dir_name in os.listdir(perm_test_dir):
|
|
302
|
+
perm_test_file = os.path.join(perm_test_dir, dir_name)
|
|
303
|
+
if os.path.isdir(perm_test_file):
|
|
304
|
+
permutation_metrics.append(
|
|
305
|
+
BacktestPermutationTest.open(perm_test_file)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Load metadata if available
|
|
309
|
+
meta_file = os.path.join(directory_path, "metadata.json")
|
|
310
|
+
|
|
311
|
+
if os.path.isfile(meta_file):
|
|
312
|
+
with open(meta_file, 'r') as f:
|
|
313
|
+
metadata = json.load(f)
|
|
314
|
+
|
|
315
|
+
# Load risk-free rate if available
|
|
316
|
+
risk_free_rate_file = os.path.join(
|
|
317
|
+
directory_path, "risk_free_rate.json"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if os.path.isfile(risk_free_rate_file):
|
|
321
|
+
with open(risk_free_rate_file, 'r') as f:
|
|
322
|
+
try:
|
|
323
|
+
risk_free_rate = json.load(f).get(
|
|
324
|
+
'risk_free_rate', None
|
|
325
|
+
)
|
|
326
|
+
except json.JSONDecodeError as e:
|
|
327
|
+
logger.error(f"Error decoding risk-free rate JSON: {e}")
|
|
328
|
+
risk_free_rate = None
|
|
329
|
+
|
|
330
|
+
return Backtest(
|
|
331
|
+
algorithm_id=algorithm_id,
|
|
332
|
+
backtest_runs=backtest_runs,
|
|
333
|
+
backtest_summary=backtest_summary_metrics,
|
|
334
|
+
backtest_permutation_tests=permutation_metrics,
|
|
335
|
+
metadata=metadata,
|
|
336
|
+
risk_free_rate=risk_free_rate
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def save(
|
|
340
|
+
self,
|
|
341
|
+
directory_path: Union[str, Path],
|
|
342
|
+
backtest_date_ranges: List[BacktestDateRange] = None,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""
|
|
345
|
+
Save the backtest metrics to a file in JSON format. The metrics will
|
|
346
|
+
always be saved in a file named `metrics.json`
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
directory_path (str): The directory where the metrics
|
|
350
|
+
file will be saved.
|
|
351
|
+
backtest_date_ranges (List[BacktestDateRange], optional): A list
|
|
352
|
+
of date ranges to filter the backtest runs. If provided, only
|
|
353
|
+
backtest runs matching these date ranges will be saved.
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
OperationalException: If the directory does not exist or if
|
|
357
|
+
there is an error saving the files.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
None: This method does not return anything, it saves the
|
|
361
|
+
metrics to a file.
|
|
362
|
+
"""
|
|
363
|
+
if not os.path.exists(directory_path):
|
|
364
|
+
os.makedirs(directory_path)
|
|
365
|
+
|
|
366
|
+
# Call the save method of all backtest runs
|
|
367
|
+
if self.backtest_runs:
|
|
368
|
+
run_path = os.path.join(directory_path, "runs")
|
|
369
|
+
os.makedirs(run_path, exist_ok=True)
|
|
370
|
+
|
|
371
|
+
if backtest_date_ranges is not None:
|
|
372
|
+
runs = self.get_all_backtest_runs()
|
|
373
|
+
else:
|
|
374
|
+
runs = self.backtest_runs
|
|
375
|
+
|
|
376
|
+
for br in runs:
|
|
377
|
+
dir_name = br.create_directory_name()
|
|
378
|
+
destination_run_path = os.path.join(run_path, dir_name)
|
|
379
|
+
os.makedirs(destination_run_path, exist_ok=True)
|
|
380
|
+
br.save(destination_run_path)
|
|
381
|
+
|
|
382
|
+
# Save combined backtest metrics if available
|
|
383
|
+
if self.backtest_summary:
|
|
384
|
+
summary_file = os.path.join(
|
|
385
|
+
directory_path, "summary.json"
|
|
386
|
+
)
|
|
387
|
+
self.backtest_summary.save(summary_file)
|
|
388
|
+
|
|
389
|
+
if self.backtest_permutation_tests:
|
|
390
|
+
permutation_dir_path = os.path.join(
|
|
391
|
+
directory_path, "permutation_tests"
|
|
392
|
+
)
|
|
393
|
+
os.makedirs(permutation_dir_path, exist_ok=True)
|
|
394
|
+
|
|
395
|
+
for pm in self.backtest_permutation_tests:
|
|
396
|
+
dir_name = pm.create_directory_name()
|
|
397
|
+
pm_path = os.path.join(permutation_dir_path, dir_name)
|
|
398
|
+
pm.save(pm_path)
|
|
399
|
+
|
|
400
|
+
# Save metadata if available
|
|
401
|
+
if self.metadata:
|
|
402
|
+
meta_file = os.path.join(directory_path, "metadata.json")
|
|
403
|
+
with open(meta_file, 'w') as f:
|
|
404
|
+
json.dump(self.metadata, f, indent=4)
|
|
405
|
+
|
|
406
|
+
# Save risk-free rate if available
|
|
407
|
+
if self.risk_free_rate is not None:
|
|
408
|
+
risk_free_rate_file = os.path.join(
|
|
409
|
+
directory_path, "risk_free_rate.json"
|
|
410
|
+
)
|
|
411
|
+
with open(risk_free_rate_file, 'w') as f:
|
|
412
|
+
json.dump(
|
|
413
|
+
{'risk_free_rate': self.risk_free_rate}, f, indent=4
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Save strategy IDs if available
|
|
417
|
+
if self.strategy_ids:
|
|
418
|
+
strategy_ids_file = os.path.join(
|
|
419
|
+
directory_path, "strategy_ids.json"
|
|
420
|
+
)
|
|
421
|
+
with open(strategy_ids_file, 'w') as f:
|
|
422
|
+
json.dump({'strategy_ids': self.strategy_ids}, f, indent=4)
|
|
423
|
+
|
|
424
|
+
# Save algorithm ID if available
|
|
425
|
+
if self.algorithm_id is not None:
|
|
426
|
+
algorithm_id_file = os.path.join(
|
|
427
|
+
directory_path, "algorithm_id.json"
|
|
428
|
+
)
|
|
429
|
+
with open(algorithm_id_file, 'w') as f:
|
|
430
|
+
json.dump(
|
|
431
|
+
{'algorithm_id': self.algorithm_id}, f, indent=4
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Save the permutation tests if available
|
|
435
|
+
if self.backtest_permutation_tests:
|
|
436
|
+
permutation_tests_path = os.path.join(
|
|
437
|
+
directory_path, "permutation_tests"
|
|
438
|
+
)
|
|
439
|
+
os.makedirs(permutation_tests_path, exist_ok=True)
|
|
440
|
+
|
|
441
|
+
for bpt in self.backtest_permutation_tests:
|
|
442
|
+
dir_name = bpt.create_directory_name()
|
|
443
|
+
bpt_path = os.path.join(permutation_tests_path, dir_name)
|
|
444
|
+
os.makedirs(bpt_path, exist_ok=True)
|
|
445
|
+
bpt.save(bpt_path)
|
|
446
|
+
|
|
447
|
+
def __repr__(self):
|
|
448
|
+
"""
|
|
449
|
+
Return a string representation of the Backtest instance.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
str: A string representation of the Backtest instance.
|
|
453
|
+
"""
|
|
454
|
+
return json.dumps(
|
|
455
|
+
self.to_dict(), indent=4, sort_keys=True, default=str
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def merge(self, other: 'Backtest') -> 'Backtest':
|
|
459
|
+
"""
|
|
460
|
+
Function to merge another Backtest instance into this one.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
other (Backtest): The other Backtest instance to merge.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Backtest: The merged Backtest instance.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
merged = Backtest()
|
|
470
|
+
merged.backtest_runs = self.backtest_runs + other.backtest_runs
|
|
471
|
+
|
|
472
|
+
summary = BacktestSummaryMetrics()
|
|
473
|
+
|
|
474
|
+
for bt_run in merged.get_all_backtest_metrics():
|
|
475
|
+
summary.add(bt_run)
|
|
476
|
+
|
|
477
|
+
merged.backtest_summary = summary
|
|
478
|
+
merged.backtest_permutation_tests = \
|
|
479
|
+
self.backtest_permutation_tests + other.backtest_permutation_tests
|
|
480
|
+
|
|
481
|
+
# Merge metadata
|
|
482
|
+
merged.metadata = {**self.metadata, **other.metadata}
|
|
483
|
+
|
|
484
|
+
if self.risk_free_rate is None:
|
|
485
|
+
merged.risk_free_rate = other.risk_free_rate
|
|
486
|
+
|
|
487
|
+
if self.strategy_ids is None:
|
|
488
|
+
merged.strategy_ids = other.strategy_ids
|
|
489
|
+
|
|
490
|
+
if self.algorithm_id is None:
|
|
491
|
+
merged.algorithm_id = other.algorithm_id
|
|
492
|
+
|
|
493
|
+
return merged
|
|
494
|
+
|
|
495
|
+
def get_metadata(self) -> Dict[str, str]:
|
|
496
|
+
"""
|
|
497
|
+
Get the metadata of the backtest.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Dict[str, str]: A dictionary containing the metadata
|
|
501
|
+
of the backtest.
|
|
502
|
+
"""
|
|
503
|
+
return self.metadata
|
|
504
|
+
|
|
505
|
+
def get_backtest_date_ranges(self):
|
|
506
|
+
"""
|
|
507
|
+
Get the date ranges for the backtest.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
List[BacktestDateRange]: A list of BacktestDateRange objects
|
|
511
|
+
representing the date ranges for each backtest run.
|
|
512
|
+
"""
|
|
513
|
+
return [
|
|
514
|
+
BacktestDateRange(
|
|
515
|
+
start_date=run.backtest_start_date,
|
|
516
|
+
end_date=run.backtest_end_date,
|
|
517
|
+
name=run.backtest_date_range_name
|
|
518
|
+
)
|
|
519
|
+
for run in self.backtest_runs
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
def add_permutation_test(
|
|
523
|
+
self, permutation_test: BacktestPermutationTest
|
|
524
|
+
) -> None:
|
|
525
|
+
"""
|
|
526
|
+
Add a permutation test to the backtest.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
permutation_test (BacktestPermutationTest): The permutation test
|
|
530
|
+
to add.
|
|
531
|
+
"""
|
|
532
|
+
self.backtest_permutation_tests.append(permutation_test)
|
|
533
|
+
|
|
534
|
+
def __hash__(self):
|
|
535
|
+
if self.algorithm_id is None:
|
|
536
|
+
raise ValueError(
|
|
537
|
+
"Cannot hash Backtest without an algorithm_id value, Please "
|
|
538
|
+
"make sure the Backtest instance has an algorithm_id set."
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
meta_id = self.metadata.get("algorithm_id")
|
|
542
|
+
return hash(meta_id)
|
|
543
|
+
|
|
544
|
+
def __eq__(self, other):
|
|
545
|
+
if not isinstance(other, Backtest):
|
|
546
|
+
return False
|
|
547
|
+
|
|
548
|
+
return self.algorithm_id == other.algorithm_id
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from dateutil.parser import parse
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
|
|
5
|
+
from investing_algorithm_framework.domain.exceptions import \
|
|
6
|
+
OperationalException
|
|
7
|
+
|
|
8
|
+
logger = getLogger("investing_algorithm_framework")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BacktestDateRange:
|
|
12
|
+
"""
|
|
13
|
+
Represents a date range for a backtest. This class
|
|
14
|
+
will check that the start and end dates are valid for a backtest.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
_start_date (datetime): The start date of the backtest.
|
|
18
|
+
_end_date (datetime): The end date of the backtest. If not provided,
|
|
19
|
+
it defaults to the current UTC time.
|
|
20
|
+
_name (str): An optional name for the backtest date range.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, start_date, end_date=None, name=None):
|
|
23
|
+
|
|
24
|
+
if isinstance(start_date, str):
|
|
25
|
+
start_date = parse(start_date)
|
|
26
|
+
|
|
27
|
+
if end_date is not None and isinstance(end_date, str):
|
|
28
|
+
end_date = parse(end_date)
|
|
29
|
+
|
|
30
|
+
if end_date is None:
|
|
31
|
+
self._end_date = datetime.now(tz=timezone.utc)
|
|
32
|
+
|
|
33
|
+
# Check if start_date end end_date are utc datetime objects
|
|
34
|
+
time_zone_info = start_date.tzinfo
|
|
35
|
+
|
|
36
|
+
if time_zone_info is None or time_zone_info is not timezone.utc:
|
|
37
|
+
logger.warning(
|
|
38
|
+
"Start date must be a UTC datetime object. "
|
|
39
|
+
f"Received: {start_date}"
|
|
40
|
+
)
|
|
41
|
+
# Convert to UTC if not already
|
|
42
|
+
start_date = start_date.astimezone(timezone.utc)
|
|
43
|
+
|
|
44
|
+
time_zone_info = end_date.tzinfo
|
|
45
|
+
|
|
46
|
+
if time_zone_info is None or time_zone_info is not timezone.utc:
|
|
47
|
+
logger.warning(
|
|
48
|
+
"End date must be a UTC datetime object. "
|
|
49
|
+
f"Received: {end_date}"
|
|
50
|
+
)
|
|
51
|
+
# Convert to UTC if not already
|
|
52
|
+
end_date = end_date.astimezone(timezone.utc)
|
|
53
|
+
|
|
54
|
+
self._start_date = start_date
|
|
55
|
+
self._end_date = end_date
|
|
56
|
+
self._name = name
|
|
57
|
+
|
|
58
|
+
if end_date < start_date:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"End date cannot be before start date for a backtest "
|
|
61
|
+
"date range. " +
|
|
62
|
+
f"(start_date: {start_date}, end_date: {end_date})"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Check if the start date is rounded to the nearest hour
|
|
66
|
+
if start_date.minute != 0 or start_date.second != 0 \
|
|
67
|
+
or start_date.microsecond != 0:
|
|
68
|
+
raise OperationalException(
|
|
69
|
+
"Start date must be rounded to the nearest hour. "
|
|
70
|
+
f"Received: {start_date}"
|
|
71
|
+
)
|
|
72
|
+
# Check if the end date is rounded to the nearest hour
|
|
73
|
+
if end_date.minute != 0 or end_date.second != 0 \
|
|
74
|
+
or end_date.microsecond != 0:
|
|
75
|
+
raise OperationalException(
|
|
76
|
+
"End date must be rounded to the nearest hour. "
|
|
77
|
+
f"Received: {end_date}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def start_date(self):
|
|
82
|
+
return self._start_date
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def end_date(self):
|
|
86
|
+
return self._end_date
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def name(self):
|
|
90
|
+
return self._name
|
|
91
|
+
|
|
92
|
+
def __eq__(self, other):
|
|
93
|
+
"""
|
|
94
|
+
Two BacktestDateRange objects are equal if they have the same
|
|
95
|
+
start and end dates, regardless of their names.
|
|
96
|
+
"""
|
|
97
|
+
if not isinstance(other, BacktestDateRange):
|
|
98
|
+
return False
|
|
99
|
+
return (self._start_date == other._start_date and
|
|
100
|
+
self._end_date == other._end_date)
|
|
101
|
+
|
|
102
|
+
def __hash__(self):
|
|
103
|
+
"""
|
|
104
|
+
Hash based on start and end dates to make the object hashable
|
|
105
|
+
for use in sets and as dictionary keys.
|
|
106
|
+
"""
|
|
107
|
+
return hash((self._start_date, self._end_date))
|
|
108
|
+
|
|
109
|
+
def __repr__(self):
|
|
110
|
+
return f"{self.name}: {self._start_date} - {self._end_date}"
|
|
111
|
+
|
|
112
|
+
def __str__(self):
|
|
113
|
+
return self.__repr__()
|