investing-algorithm-framework 6.9.1__py3-none-any.whl → 7.19.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of investing-algorithm-framework might be problematic. Click here for more details.
- investing_algorithm_framework/__init__.py +147 -44
- investing_algorithm_framework/app/__init__.py +23 -6
- investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +1322 -707
- investing_algorithm_framework/app/context.py +196 -88
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +16 -5
- investing_algorithm_framework/app/reporting/ascii.py +57 -202
- investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
- investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
- investing_algorithm_framework/app/reporting/generate.py +100 -114
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
- investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
- investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
- investing_algorithm_framework/app/strategy.py +315 -175
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/cli/cli.py +30 -12
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
- investing_algorithm_framework/cli/initialize_app.py +20 -1
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
- investing_algorithm_framework/create_app.py +3 -5
- investing_algorithm_framework/dependency_container.py +25 -39
- investing_algorithm_framework/domain/__init__.py +45 -38
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +27 -0
- investing_algorithm_framework/domain/constants.py +6 -34
- investing_algorithm_framework/domain/data_provider.py +200 -56
- investing_algorithm_framework/domain/exceptions.py +34 -1
- investing_algorithm_framework/domain/models/__init__.py +10 -19
- investing_algorithm_framework/domain/models/base_model.py +0 -6
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
- investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +34 -13
- investing_algorithm_framework/domain/models/order/order_status.py +1 -1
- investing_algorithm_framework/domain/models/order/order_type.py +1 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +9 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
- investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
- investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
- investing_algorithm_framework/domain/models/time_frame.py +7 -0
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +63 -1
- investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
- investing_algorithm_framework/domain/models/trade/trade.py +56 -32
- investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
- investing_algorithm_framework/domain/order_executor.py +19 -0
- investing_algorithm_framework/domain/portfolio_provider.py +20 -1
- investing_algorithm_framework/domain/services/__init__.py +0 -13
- investing_algorithm_framework/domain/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +5 -1
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +17 -14
- investing_algorithm_framework/download_data.py +40 -10
- investing_algorithm_framework/infrastructure/__init__.py +13 -25
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
- investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
- investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
- investing_algorithm_framework/services/__init__.py +105 -8
- investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
- investing_algorithm_framework/services/configuration_service.py +14 -4
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
- investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
- investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
- investing_algorithm_framework/services/order_service/order_service.py +9 -71
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
- investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
- investing_algorithm_framework/services/repository_service.py +5 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
- investing_algorithm_framework/services/trade_service/__init__.py +7 -1
- investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
- investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
- investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
- investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
- investing_algorithm_framework/app/reporting/evaluation.py +0 -243
- investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
- investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
- investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
- investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
- investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
- investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
- investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
- investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
- investing_algorithm_framework/domain/models/data_source.py +0 -21
- investing_algorithm_framework/domain/models/date_range.py +0 -64
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
- investing_algorithm_framework/domain/services/market_service.py +0 -153
- investing_algorithm_framework/domain/services/observable.py +0 -51
- investing_algorithm_framework/domain/services/observer.py +0 -19
- investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
- investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
- investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
- investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
- investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
- investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
- investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
- investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
- investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
- investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
- investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
- investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
- investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
- investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
- /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from typing import Union, List, Optional, Dict
|
|
8
|
+
|
|
9
|
+
from investing_algorithm_framework.domain.exceptions \
|
|
10
|
+
import OperationalException
|
|
11
|
+
from investing_algorithm_framework.domain.models.order import Order, \
|
|
12
|
+
OrderSide, OrderStatus
|
|
13
|
+
from investing_algorithm_framework.domain.models.position import Position
|
|
14
|
+
from investing_algorithm_framework.domain.models.trade import Trade
|
|
15
|
+
from investing_algorithm_framework.domain.models.portfolio import \
|
|
16
|
+
PortfolioSnapshot
|
|
17
|
+
from investing_algorithm_framework.domain.models.trade.trade_status import \
|
|
18
|
+
TradeStatus
|
|
19
|
+
from investing_algorithm_framework.domain.models.trade.trade_stop_loss import \
|
|
20
|
+
TradeStopLoss
|
|
21
|
+
from investing_algorithm_framework.domain.models.trade.trade_take_profit \
|
|
22
|
+
import TradeTakeProfit
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from .backtest_metrics import BacktestMetrics
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
logger = getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class BacktestRun:
|
|
33
|
+
"""
|
|
34
|
+
Represents a backtest of an algorithm. It contains the backtest metrics,
|
|
35
|
+
backtest results, and paths to strategy and data files.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
backtest_metrics (Optional[List[BacktestMetrics]]): A list of
|
|
39
|
+
backtest metrics objects, each representing the performance
|
|
40
|
+
metrics of a single backtest run.
|
|
41
|
+
backtest_start_date (datetime): The start date of the backtest.
|
|
42
|
+
backtest_end_date (datetime): The end date of the backtest.
|
|
43
|
+
backtest_date_range_name (str): The name of the date range used for
|
|
44
|
+
the backtest.
|
|
45
|
+
trading_symbol (str): The trading symbol used in the backtest.
|
|
46
|
+
initial_unallocated (float): The initial unallocated amount in the
|
|
47
|
+
backtest.
|
|
48
|
+
number_of_runs (int): The number of runs in the backtest.
|
|
49
|
+
portfolio_snapshots (List[PortfolioSnapshot]): A list of portfolio
|
|
50
|
+
snapshots taken during the backtest.
|
|
51
|
+
trades (List[Trade]): A list of trades executed during the backtest.
|
|
52
|
+
orders (List[Order]): A list of orders placed during the backtest.
|
|
53
|
+
positions (List[Position]): A list of positions held during the
|
|
54
|
+
backtest.
|
|
55
|
+
created_at (datetime): The date and time when the backtest was created.
|
|
56
|
+
symbols (List[str]): A list of trading symbols involved in
|
|
57
|
+
the backtest.
|
|
58
|
+
number_of_days (int): The total number of days the backtest ran.
|
|
59
|
+
number_of_trades (int): The total number of trades executed during
|
|
60
|
+
the backtest.
|
|
61
|
+
number_of_trades_closed (int): The total number of trades that were
|
|
62
|
+
closed during the backtest.
|
|
63
|
+
number_of_trades_open (int): The total number of trades that are
|
|
64
|
+
still open at the end of the backtest.
|
|
65
|
+
number_of_orders (int): The total number of orders placed during
|
|
66
|
+
the backtest.
|
|
67
|
+
number_of_positions (int): The total number of positions held
|
|
68
|
+
during the backtest.
|
|
69
|
+
"""
|
|
70
|
+
backtest_start_date: datetime
|
|
71
|
+
backtest_end_date: datetime
|
|
72
|
+
trading_symbol: str
|
|
73
|
+
initial_unallocated: float = 0.0
|
|
74
|
+
number_of_runs: int = 0
|
|
75
|
+
portfolio_snapshots: List[PortfolioSnapshot] = field(default_factory=list)
|
|
76
|
+
trades: List[Trade] = field(default_factory=list)
|
|
77
|
+
orders: List[Order] = field(default_factory=list)
|
|
78
|
+
positions: List[Position] = field(default_factory=list)
|
|
79
|
+
created_at: datetime = None,
|
|
80
|
+
symbols: List[str] = field(default_factory=list)
|
|
81
|
+
number_of_days: int = 0
|
|
82
|
+
number_of_trades: int = 0
|
|
83
|
+
number_of_trades_closed: int = 0
|
|
84
|
+
number_of_trades_open: int = 0
|
|
85
|
+
number_of_orders: int = 0
|
|
86
|
+
number_of_positions: int = 0
|
|
87
|
+
backtest_metrics: BacktestMetrics = None
|
|
88
|
+
backtest_date_range_name: str = None
|
|
89
|
+
data_sources: List[Dict] = field(default_factory=list)
|
|
90
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict:
|
|
93
|
+
"""
|
|
94
|
+
Convert the Backtest instance to a dictionary with all
|
|
95
|
+
date/datetime fields as ISO strings (always UTC).
|
|
96
|
+
"""
|
|
97
|
+
def ensure_iso(value):
|
|
98
|
+
if hasattr(value, "isoformat"):
|
|
99
|
+
if value.tzinfo is None:
|
|
100
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
101
|
+
return value.isoformat()
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
backtest_metrics = self.backtest_metrics.to_dict() \
|
|
105
|
+
if self.backtest_metrics else None
|
|
106
|
+
return {
|
|
107
|
+
"backtest_metrics": backtest_metrics,
|
|
108
|
+
"backtest_start_date": ensure_iso(self.backtest_start_date),
|
|
109
|
+
"backtest_date_range_name": self.backtest_date_range_name,
|
|
110
|
+
"backtest_end_date": ensure_iso(self.backtest_end_date),
|
|
111
|
+
"trading_symbol": self.trading_symbol,
|
|
112
|
+
"initial_unallocated": self.initial_unallocated,
|
|
113
|
+
"number_of_runs": self.number_of_runs,
|
|
114
|
+
"portfolio_snapshots": [
|
|
115
|
+
ps.to_dict() for ps in self.portfolio_snapshots
|
|
116
|
+
],
|
|
117
|
+
"trades": [trade.to_dict() for trade in self.trades],
|
|
118
|
+
"orders": [order.to_dict() for order in self.orders],
|
|
119
|
+
"positions": [position.to_dict() for position in self.positions],
|
|
120
|
+
"created_at": ensure_iso(self.created_at),
|
|
121
|
+
"symbols": self.symbols,
|
|
122
|
+
"number_of_days": self.number_of_days,
|
|
123
|
+
"number_of_trades": self.number_of_trades,
|
|
124
|
+
"number_of_trades_closed": self.number_of_trades_closed,
|
|
125
|
+
"number_of_trades_open": self.number_of_trades_open,
|
|
126
|
+
"number_of_orders": self.number_of_orders,
|
|
127
|
+
"number_of_positions": self.number_of_positions,
|
|
128
|
+
"metadata": self.metadata,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def open(directory_path: Union[str, Path]) -> 'BacktestRun':
|
|
133
|
+
"""
|
|
134
|
+
Open a backtest report from a directory and return a Backtest instance.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
directory_path (str): The path to the directory containing the
|
|
138
|
+
backtest report files.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Backtest: An instance of Backtest with the loaded metrics
|
|
142
|
+
and results.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
OperationalException: If the directory does not exist or if
|
|
146
|
+
there is an error loading the files.
|
|
147
|
+
"""
|
|
148
|
+
backtest_metrics = None
|
|
149
|
+
|
|
150
|
+
if not os.path.exists(directory_path):
|
|
151
|
+
raise OperationalException(
|
|
152
|
+
f"The directory {directory_path} does not exist."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Load combined backtest metrics
|
|
156
|
+
metrics_file = os.path.join(directory_path, "metrics.json")
|
|
157
|
+
|
|
158
|
+
if os.path.isfile(metrics_file):
|
|
159
|
+
backtest_metrics = BacktestMetrics.open(metrics_file)
|
|
160
|
+
|
|
161
|
+
# Load backtest results
|
|
162
|
+
run_file = os.path.join(directory_path, "run.json")
|
|
163
|
+
|
|
164
|
+
if os.path.isfile(run_file):
|
|
165
|
+
data = json.load(open(run_file, 'r'))
|
|
166
|
+
else:
|
|
167
|
+
raise OperationalException(
|
|
168
|
+
f"The run file {run_file} does not exist."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Parse datetime fields
|
|
172
|
+
data["backtest_start_date"] = datetime.strptime(
|
|
173
|
+
data["backtest_start_date"], "%Y-%m-%d %H:%M:%S"
|
|
174
|
+
)
|
|
175
|
+
data["backtest_end_date"] = datetime.strptime(
|
|
176
|
+
data["backtest_end_date"], "%Y-%m-%d %H:%M:%S"
|
|
177
|
+
)
|
|
178
|
+
data["created_at"] = datetime.strptime(
|
|
179
|
+
data["created_at"], "%Y-%m-%d %H:%M:%S"
|
|
180
|
+
)
|
|
181
|
+
# Convert all to utc timezone
|
|
182
|
+
data["backtest_start_date"] = data[
|
|
183
|
+
"backtest_start_date"].replace(tzinfo=timezone.utc)
|
|
184
|
+
data["backtest_end_date"] = data[
|
|
185
|
+
"backtest_end_date"].replace(tzinfo=timezone.utc)
|
|
186
|
+
data["created_at"] = data["created_at"].replace(tzinfo=timezone.utc)
|
|
187
|
+
|
|
188
|
+
# Parse orders
|
|
189
|
+
data["orders"] = [
|
|
190
|
+
Order.from_dict(order) for order in data.get("orders", [])
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
# Parse positions
|
|
194
|
+
data["positions"] = [
|
|
195
|
+
Position.from_dict(position)
|
|
196
|
+
for position in data.get("positions", [])
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Parse trades
|
|
200
|
+
data["trades"] = [
|
|
201
|
+
Trade.from_dict(trade) for trade in data.get("trades", [])
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
# Parse portfolio snapshots
|
|
205
|
+
data["portfolio_snapshots"] = [
|
|
206
|
+
PortfolioSnapshot.from_dict(ps)
|
|
207
|
+
for ps in data.get("portfolio_snapshots", [])
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
return BacktestRun(
|
|
211
|
+
backtest_metrics=backtest_metrics,
|
|
212
|
+
**data
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def create_directory_name(self) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Create a directory name for the backtest run based on its attributes.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
str: A string representing the directory name.
|
|
221
|
+
"""
|
|
222
|
+
start_str = self.backtest_start_date.strftime("%Y%m%d")
|
|
223
|
+
end_str = self.backtest_end_date.strftime("%Y%m%d")
|
|
224
|
+
dir_name = f"backtest_{self.trading_symbol}_{start_str}_{end_str}"
|
|
225
|
+
return dir_name
|
|
226
|
+
|
|
227
|
+
def save(self, directory_path: Union[str, Path]) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Save the backtest run to a directory.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
directory_path (str): The directory where the metrics
|
|
233
|
+
file will be saved.
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
OperationalException: If the directory does not exist or if
|
|
237
|
+
there is an error saving the files.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
None: This method does not return anything, it saves the
|
|
241
|
+
metrics to a file.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
metrics_path = os.path.join(directory_path, "metrics.json")
|
|
245
|
+
run_path = os.path.join(directory_path, "run.json")
|
|
246
|
+
|
|
247
|
+
if not os.path.exists(directory_path):
|
|
248
|
+
os.makedirs(directory_path)
|
|
249
|
+
|
|
250
|
+
# Call the save method of BacktestMetrics
|
|
251
|
+
if self.backtest_metrics:
|
|
252
|
+
self.backtest_metrics.save(metrics_path)
|
|
253
|
+
|
|
254
|
+
# Save the run data
|
|
255
|
+
with open(run_path, 'w') as f:
|
|
256
|
+
# string format datetime objects
|
|
257
|
+
data = self.to_dict()
|
|
258
|
+
|
|
259
|
+
# Remove backtest_metrics to avoid redundancy
|
|
260
|
+
data.pop("backtest_metrics", None)
|
|
261
|
+
|
|
262
|
+
# Ensure datetime objects are in UTC before formatting
|
|
263
|
+
backtest_start_date = self.backtest_start_date
|
|
264
|
+
|
|
265
|
+
if backtest_start_date.tzinfo is None:
|
|
266
|
+
# Naive datetime - treat as UTC
|
|
267
|
+
backtest_start_date = backtest_start_date.replace(
|
|
268
|
+
tzinfo=timezone.utc
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
# Timezone-aware - convert to UTC
|
|
272
|
+
backtest_start_date = backtest_start_date.astimezone(
|
|
273
|
+
timezone.utc
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
backtest_end_date = self.backtest_end_date
|
|
277
|
+
if backtest_end_date.tzinfo is None:
|
|
278
|
+
backtest_end_date = backtest_end_date.replace(
|
|
279
|
+
tzinfo=timezone.utc
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
backtest_end_date = backtest_end_date.astimezone(timezone.utc)
|
|
283
|
+
|
|
284
|
+
created_at = self.created_at
|
|
285
|
+
if created_at.tzinfo is None:
|
|
286
|
+
created_at = created_at.replace(tzinfo=timezone.utc)
|
|
287
|
+
else:
|
|
288
|
+
created_at = created_at.astimezone(timezone.utc)
|
|
289
|
+
|
|
290
|
+
data["backtest_start_date"] = backtest_start_date.strftime(
|
|
291
|
+
"%Y-%m-%d %H:%M:%S"
|
|
292
|
+
)
|
|
293
|
+
data["backtest_end_date"] = backtest_end_date.strftime(
|
|
294
|
+
"%Y-%m-%d %H:%M:%S"
|
|
295
|
+
)
|
|
296
|
+
data["created_at"] = created_at.strftime(
|
|
297
|
+
"%Y-%m-%d %H:%M:%S"
|
|
298
|
+
)
|
|
299
|
+
json.dump(data, f, default=str)
|
|
300
|
+
|
|
301
|
+
def get_trade(self, trade_id: str) -> Optional[Trade]:
|
|
302
|
+
"""
|
|
303
|
+
Get a trade by its ID from the backtest report
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
trade_id (str): The trade ID
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Trade: The trade with the given ID, or None if not found
|
|
310
|
+
"""
|
|
311
|
+
for trade in self.trades:
|
|
312
|
+
if trade.trade_id == trade_id:
|
|
313
|
+
return trade
|
|
314
|
+
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
def get_trades(
|
|
318
|
+
self,
|
|
319
|
+
target_symbol: str = None,
|
|
320
|
+
trade_status: Union[TradeStatus, str] = None,
|
|
321
|
+
opened_at: datetime = None,
|
|
322
|
+
opened_at_lt: datetime = None,
|
|
323
|
+
opened_at_lte: datetime = None,
|
|
324
|
+
opened_at_gt: datetime = None,
|
|
325
|
+
opened_at_gte: datetime = None,
|
|
326
|
+
order_id: str = None
|
|
327
|
+
) -> List[Trade]:
|
|
328
|
+
"""
|
|
329
|
+
Get the trades of a backtest report
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
target_symbol (str): The target_symbol
|
|
333
|
+
trade_status (Union[TradeStatus, str]): The trade status
|
|
334
|
+
opened_at (datetime): The created_at date to filter the trades
|
|
335
|
+
opened_at_lt (datetime): The created_at date to filter the trades
|
|
336
|
+
opened_at_lte (datetime): The created_at date to filter the trades
|
|
337
|
+
opened_at_gt (datetime): The created_at date to filter the trades
|
|
338
|
+
opened_at_gte (datetime): The created_at date to filter the trades
|
|
339
|
+
order_id (str): The order ID to filter the trades
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
list: The trades of the backtest report
|
|
343
|
+
"""
|
|
344
|
+
selection = self.trades
|
|
345
|
+
|
|
346
|
+
if target_symbol is not None:
|
|
347
|
+
selection = [
|
|
348
|
+
trade for trade in selection
|
|
349
|
+
if trade.target_symbol.lower() == target_symbol.lower()
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
if trade_status is not None:
|
|
353
|
+
trade_status = TradeStatus.from_value(trade_status)
|
|
354
|
+
selection = [
|
|
355
|
+
trade for trade in selection
|
|
356
|
+
if trade.status == trade_status.value
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
if opened_at is not None:
|
|
360
|
+
selection = [
|
|
361
|
+
trade for trade in selection
|
|
362
|
+
if trade.opened_at == opened_at
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
if opened_at_lt is not None:
|
|
366
|
+
selection = [
|
|
367
|
+
trade for trade in selection
|
|
368
|
+
if trade.opened_at < opened_at_lt
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
if opened_at_lte is not None:
|
|
372
|
+
selection = [
|
|
373
|
+
trade for trade in selection
|
|
374
|
+
if trade.opened_at <= opened_at_lte
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
if opened_at_gt is not None:
|
|
378
|
+
selection = [
|
|
379
|
+
trade for trade in selection
|
|
380
|
+
if trade.opened_at > opened_at_gt
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
if opened_at_gte is not None:
|
|
384
|
+
selection = [
|
|
385
|
+
trade for trade in selection
|
|
386
|
+
if trade.opened_at >= opened_at_gte
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
if order_id is not None:
|
|
390
|
+
new_selection = []
|
|
391
|
+
for trade in selection:
|
|
392
|
+
|
|
393
|
+
for order in trade.orders:
|
|
394
|
+
if order.order_id == order_id:
|
|
395
|
+
new_selection.append(trade)
|
|
396
|
+
break
|
|
397
|
+
|
|
398
|
+
selection = new_selection
|
|
399
|
+
|
|
400
|
+
return selection
|
|
401
|
+
|
|
402
|
+
def get_stop_losses(
|
|
403
|
+
self,
|
|
404
|
+
trade_id: str = None,
|
|
405
|
+
triggered: bool = None
|
|
406
|
+
) -> List[TradeStopLoss]:
|
|
407
|
+
"""
|
|
408
|
+
Get the stop losses of the backtest report
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
trade_id (str): The trade ID to filter the stop losses
|
|
412
|
+
triggered (bool): Whether to filter by triggered stop losses
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
list: The stop losses of the backtest report
|
|
416
|
+
"""
|
|
417
|
+
stop_losses = []
|
|
418
|
+
|
|
419
|
+
for trade in self.trades:
|
|
420
|
+
if trade_id is not None and trade.id != trade_id:
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
for sl in trade.stop_losses:
|
|
424
|
+
if isinstance(sl, TradeStopLoss):
|
|
425
|
+
if triggered is not None:
|
|
426
|
+
if sl.triggered == triggered:
|
|
427
|
+
stop_losses.append(sl)
|
|
428
|
+
else:
|
|
429
|
+
stop_losses.append(sl)
|
|
430
|
+
|
|
431
|
+
return stop_losses
|
|
432
|
+
|
|
433
|
+
def get_take_profits(
|
|
434
|
+
self,
|
|
435
|
+
trade_id: str = None,
|
|
436
|
+
triggered: bool = None
|
|
437
|
+
) -> List[TradeStopLoss]:
|
|
438
|
+
"""
|
|
439
|
+
Get the take profits of the backtest report
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
trade_id (str): The trade ID to filter the take profits
|
|
443
|
+
triggered (bool): Whether to filter by triggered take profits
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
list: The take profits of the backtest report
|
|
447
|
+
"""
|
|
448
|
+
take_profits = []
|
|
449
|
+
|
|
450
|
+
for trade in self.trades:
|
|
451
|
+
if trade_id is not None and trade.id != trade_id:
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
for tp in trade.take_profits:
|
|
455
|
+
if isinstance(tp, TradeTakeProfit):
|
|
456
|
+
if triggered is not None:
|
|
457
|
+
if tp.triggered == triggered:
|
|
458
|
+
take_profits.append(tp)
|
|
459
|
+
else:
|
|
460
|
+
take_profits.append(tp)
|
|
461
|
+
|
|
462
|
+
return take_profits
|
|
463
|
+
|
|
464
|
+
def get_portfolio_snapshots(
|
|
465
|
+
self,
|
|
466
|
+
created_at_lt: Optional[datetime] = None,
|
|
467
|
+
created_at_lte: Optional[datetime] = None,
|
|
468
|
+
created_at_gt: Optional[datetime] = None,
|
|
469
|
+
created_at_gte: Optional[datetime] = None
|
|
470
|
+
) -> List[PortfolioSnapshot]:
|
|
471
|
+
"""
|
|
472
|
+
Get the portfolio snapshots of the backtest report
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
created_at_lt (datetime): The created_at date to filter
|
|
476
|
+
the snapshots
|
|
477
|
+
created_at_lte (datetime): The created_at date to filter
|
|
478
|
+
the snapshots
|
|
479
|
+
created_at_gt (datetime): The created_at date to filter
|
|
480
|
+
the snapshots
|
|
481
|
+
created_at_gte (datetime): The created_at date to filter
|
|
482
|
+
the snapshots
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
list: The portfolio snapshots of the backtest report
|
|
486
|
+
"""
|
|
487
|
+
selection = self.portfolio_snapshots
|
|
488
|
+
|
|
489
|
+
if created_at_lt is not None:
|
|
490
|
+
selection = [
|
|
491
|
+
snapshot for snapshot in selection
|
|
492
|
+
if snapshot.created_at < created_at_lt
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
if created_at_lte is not None:
|
|
496
|
+
selection = [
|
|
497
|
+
snapshot for snapshot in selection
|
|
498
|
+
if snapshot.created_at <= created_at_lte
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
if created_at_gt is not None:
|
|
502
|
+
selection = [
|
|
503
|
+
snapshot for snapshot in selection
|
|
504
|
+
if snapshot.created_at > created_at_gt
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
if created_at_gte is not None:
|
|
508
|
+
selection = [
|
|
509
|
+
snapshot for snapshot in selection
|
|
510
|
+
if snapshot.created_at >= created_at_gte
|
|
511
|
+
]
|
|
512
|
+
|
|
513
|
+
return selection
|
|
514
|
+
|
|
515
|
+
def get_orders(
|
|
516
|
+
self,
|
|
517
|
+
target_symbol: str = None,
|
|
518
|
+
order_side: str = None,
|
|
519
|
+
order_status: Union[OrderStatus, str] = None,
|
|
520
|
+
created_at: datetime = None,
|
|
521
|
+
created_at_lt: datetime = None,
|
|
522
|
+
created_at_lte: datetime = None,
|
|
523
|
+
created_at_gt: datetime = None,
|
|
524
|
+
created_at_gte: datetime = None
|
|
525
|
+
) -> List[Order]:
|
|
526
|
+
"""
|
|
527
|
+
Get the orders of a backtest report
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
target_symbol (str): The target_symbol
|
|
531
|
+
order_side (str): The order side
|
|
532
|
+
order_status (Union[OrderStatus, str]): The order status
|
|
533
|
+
created_at (datetime): The created_at date to filter the orders
|
|
534
|
+
created_at_lt (datetime): The created_at date to filter the orders
|
|
535
|
+
created_at_lte (datetime): The created_at date to filter the orders
|
|
536
|
+
created_at_gt (datetime): The created_at date to filter the orders
|
|
537
|
+
created_at_gte (datetime): The created_at date to filter the orders
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
list: The orders of the backtest report
|
|
541
|
+
"""
|
|
542
|
+
selection = self.orders
|
|
543
|
+
|
|
544
|
+
if created_at is not None:
|
|
545
|
+
selection = [
|
|
546
|
+
order for order in selection
|
|
547
|
+
if order.created_at == created_at
|
|
548
|
+
]
|
|
549
|
+
|
|
550
|
+
if created_at_lt is not None:
|
|
551
|
+
selection = [
|
|
552
|
+
order for order in selection
|
|
553
|
+
if order.created_at < created_at_lt
|
|
554
|
+
]
|
|
555
|
+
|
|
556
|
+
if created_at_lte is not None:
|
|
557
|
+
selection = [
|
|
558
|
+
order for order in selection
|
|
559
|
+
if order.created_at <= created_at_lte
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
if created_at_gt is not None:
|
|
563
|
+
selection = [
|
|
564
|
+
order for order in selection
|
|
565
|
+
if order.created_at > created_at_gt
|
|
566
|
+
]
|
|
567
|
+
|
|
568
|
+
if created_at_gte is not None:
|
|
569
|
+
selection = [
|
|
570
|
+
order for order in selection
|
|
571
|
+
if order.created_at >= created_at_gte
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
if target_symbol is not None:
|
|
575
|
+
selection = [
|
|
576
|
+
order for order in selection
|
|
577
|
+
if order.target_symbol == target_symbol
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
if order_side is not None:
|
|
581
|
+
order_side = OrderSide.from_value(order_side)
|
|
582
|
+
selection = [
|
|
583
|
+
order for order in selection
|
|
584
|
+
if order.order_side == order_side.value
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
if order_status is not None:
|
|
588
|
+
status = OrderStatus.from_value(order_status)
|
|
589
|
+
selection = [
|
|
590
|
+
order for order in selection
|
|
591
|
+
if order.status == status.value
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
return selection
|
|
595
|
+
|
|
596
|
+
def __repr__(self):
|
|
597
|
+
"""
|
|
598
|
+
Return a string representation of the Backtest instance.
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
str: A string representation of the Backtest instance.
|
|
602
|
+
"""
|
|
603
|
+
return json.dumps(
|
|
604
|
+
self.to_dict(), indent=4, sort_keys=True, default=str
|
|
605
|
+
)
|