investing-algorithm-framework 7.19.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of investing-algorithm-framework might be problematic. Click here for more details.
- investing_algorithm_framework/__init__.py +197 -0
- investing_algorithm_framework/app/__init__.py +47 -0
- investing_algorithm_framework/app/algorithm/__init__.py +7 -0
- investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +2204 -0
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1667 -0
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +27 -0
- investing_algorithm_framework/app/reporting/ascii.py +921 -0
- investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
- investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
- investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
- investing_algorithm_framework/app/reporting/generate.py +185 -0
- investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
- investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
- investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
- investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
- investing_algorithm_framework/app/stateless/__init__.py +35 -0
- investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
- investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
- investing_algorithm_framework/app/strategy.py +675 -0
- investing_algorithm_framework/app/task.py +41 -0
- investing_algorithm_framework/app/web/__init__.py +5 -0
- investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
- investing_algorithm_framework/app/web/controllers/orders.py +20 -0
- investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
- investing_algorithm_framework/app/web/controllers/positions.py +18 -0
- investing_algorithm_framework/app/web/create_app.py +20 -0
- investing_algorithm_framework/app/web/error_handler.py +59 -0
- investing_algorithm_framework/app/web/responses.py +20 -0
- investing_algorithm_framework/app/web/run_strategies.py +4 -0
- investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
- investing_algorithm_framework/app/web/schemas/order.py +12 -0
- investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
- investing_algorithm_framework/app/web/schemas/position.py +15 -0
- investing_algorithm_framework/app/web/setup_cors.py +6 -0
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +207 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
- investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
- investing_algorithm_framework/cli/initialize_app.py +603 -0
- investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
- investing_algorithm_framework/cli/templates/app.py.template +18 -0
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
- investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
- investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
- investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
- investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
- investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
- investing_algorithm_framework/cli/templates/env.example.template +2 -0
- investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
- investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
- investing_algorithm_framework/cli/templates/readme.md.template +135 -0
- investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
- investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
- investing_algorithm_framework/create_app.py +54 -0
- investing_algorithm_framework/dependency_container.py +155 -0
- investing_algorithm_framework/domain/__init__.py +148 -0
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +435 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +111 -0
- investing_algorithm_framework/domain/constants.py +83 -0
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +42 -0
- investing_algorithm_framework/domain/decimal_parsing.py +40 -0
- investing_algorithm_framework/domain/exceptions.py +112 -0
- investing_algorithm_framework/domain/models/__init__.py +43 -0
- investing_algorithm_framework/domain/models/app_mode.py +34 -0
- investing_algorithm_framework/domain/models/base_model.py +25 -0
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/data/data_type.py +46 -0
- investing_algorithm_framework/domain/models/event.py +35 -0
- investing_algorithm_framework/domain/models/market/__init__.py +5 -0
- investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
- investing_algorithm_framework/domain/models/order/__init__.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +384 -0
- investing_algorithm_framework/domain/models/order/order_side.py +36 -0
- investing_algorithm_framework/domain/models/order/order_status.py +37 -0
- investing_algorithm_framework/domain/models/order/order_type.py +30 -0
- investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
- investing_algorithm_framework/domain/models/position/__init__.py +4 -0
- investing_algorithm_framework/domain/models/position/position.py +68 -0
- investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
- investing_algorithm_framework/domain/models/time_frame.py +153 -0
- investing_algorithm_framework/domain/models/time_interval.py +124 -0
- investing_algorithm_framework/domain/models/time_unit.py +149 -0
- investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
- investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
- investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
- investing_algorithm_framework/domain/models/trade/trade.py +388 -0
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
- investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -0
- investing_algorithm_framework/domain/positions/__init__.py +4 -0
- investing_algorithm_framework/domain/positions/position_size.py +41 -0
- investing_algorithm_framework/domain/services/__init__.py +11 -0
- investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
- investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
- investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
- investing_algorithm_framework/domain/services/rounding_service.py +27 -0
- investing_algorithm_framework/domain/services/state_handler.py +38 -0
- investing_algorithm_framework/domain/stateless_actions.py +7 -0
- investing_algorithm_framework/domain/strategy.py +44 -0
- investing_algorithm_framework/domain/utils/__init__.py +27 -0
- investing_algorithm_framework/domain/utils/csv.py +104 -0
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/dates.py +57 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +53 -0
- investing_algorithm_framework/domain/utils/random.py +41 -0
- investing_algorithm_framework/domain/utils/signatures.py +17 -0
- investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
- investing_algorithm_framework/domain/utils/synchronized.py +12 -0
- investing_algorithm_framework/download_data.py +108 -0
- investing_algorithm_framework/infrastructure/__init__.py +50 -0
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
- investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
- investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
- investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
- investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
- investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
- investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
- investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
- investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
- investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
- investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
- investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
- investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
- investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
- investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
- investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
- investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
- investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
- investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
- investing_algorithm_framework/services/__init__.py +132 -0
- investing_algorithm_framework/services/backtesting/__init__.py +5 -0
- investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
- investing_algorithm_framework/services/configuration_service.py +96 -0
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/services/market_credential_service.py +40 -0
- investing_algorithm_framework/services/metrics/__init__.py +114 -0
- investing_algorithm_framework/services/metrics/alpha.py +0 -0
- investing_algorithm_framework/services/metrics/beta.py +0 -0
- investing_algorithm_framework/services/metrics/cagr.py +60 -0
- investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
- investing_algorithm_framework/services/metrics/drawdown.py +181 -0
- investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
- investing_algorithm_framework/services/metrics/exposure.py +210 -0
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
- investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
- investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
- investing_algorithm_framework/services/metrics/recovery.py +113 -0
- investing_algorithm_framework/services/metrics/returns.py +452 -0
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
- investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
- investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
- investing_algorithm_framework/services/metrics/ulcer.py +0 -0
- investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/services/metrics/win_rate.py +177 -0
- investing_algorithm_framework/services/order_service/__init__.py +9 -0
- investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
- investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
- investing_algorithm_framework/services/order_service/order_service.py +826 -0
- investing_algorithm_framework/services/portfolios/__init__.py +16 -0
- investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
- investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
- investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
- investing_algorithm_framework/services/positions/__init__.py +7 -0
- investing_algorithm_framework/services/positions/position_service.py +210 -0
- investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
- investing_algorithm_framework/services/repository_service.py +40 -0
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
- investing_algorithm_framework/services/trade_service/__init__.py +3 -0
- investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
- investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
- investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
- investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
- investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
- investing_algorithm_framework-7.19.14.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from datetime import timezone
|
|
9
|
+
|
|
10
|
+
from .backtest_metrics import BacktestMetrics
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BacktestPermutationTest:
|
|
15
|
+
"""
|
|
16
|
+
Represents the result of a permutation test on backtest metrics.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
real_metrics (BacktestMetrics): The real backtest metrics.
|
|
20
|
+
permutated_metrics (List[BacktestMetrics]): A list of backtest
|
|
21
|
+
metrics objects from permuted backtests.
|
|
22
|
+
p_values (Dict[str, float]): A dictionary mapping metric names
|
|
23
|
+
to their permutation test p-values.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Default set of metrics for permutation testing
|
|
27
|
+
DEFAULT_METRICS: List[str] = field(default_factory=lambda: [
|
|
28
|
+
"cagr",
|
|
29
|
+
"sharpe_ratio",
|
|
30
|
+
"sortino_ratio",
|
|
31
|
+
"calmar_ratio",
|
|
32
|
+
"profit_factor",
|
|
33
|
+
"annual_volatility",
|
|
34
|
+
"max_drawdown",
|
|
35
|
+
"win_rate",
|
|
36
|
+
"win_loss_ratio",
|
|
37
|
+
"average_monthly_return"
|
|
38
|
+
])
|
|
39
|
+
real_metrics: BacktestMetrics = None
|
|
40
|
+
permutated_metrics: List[BacktestMetrics] = field(default_factory=list)
|
|
41
|
+
p_values: Dict[str, float] = field(default_factory=dict)
|
|
42
|
+
ohlcv_permutated_datasets: Dict[str, List[pd.DataFrame]] = \
|
|
43
|
+
field(default_factory=dict)
|
|
44
|
+
ohlcv_original_datasets: Dict[str, pd.DataFrame] = \
|
|
45
|
+
field(default_factory=dict)
|
|
46
|
+
backtest_start_date: pd.Timestamp = None
|
|
47
|
+
backtest_end_date: pd.Timestamp = None
|
|
48
|
+
backtest_date_range_name: str = None
|
|
49
|
+
|
|
50
|
+
def compute_p_values(
|
|
51
|
+
self, metrics: List[str] = None, one_sided: bool = True
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Compute p-values for the selected metrics based on the
|
|
55
|
+
permutation distribution.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
metrics (List[str]): List of metric names to compute p-values for.
|
|
59
|
+
If None, uses DEFAULT_METRICS.
|
|
60
|
+
one_sided (bool): Whether to compute a one-sided
|
|
61
|
+
test (default: True).
|
|
62
|
+
"""
|
|
63
|
+
if metrics is None:
|
|
64
|
+
metrics = self.DEFAULT_METRICS
|
|
65
|
+
|
|
66
|
+
self.p_values = {}
|
|
67
|
+
|
|
68
|
+
for metric in metrics:
|
|
69
|
+
real_value = getattr(self.real_metrics, metric, None)
|
|
70
|
+
if real_value is None:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Collect metric values across all permuted backtests
|
|
74
|
+
dist = np.array([
|
|
75
|
+
getattr(pm, metric, None)
|
|
76
|
+
for pm in self.permutated_metrics
|
|
77
|
+
if getattr(pm, metric, None) is not None
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
if len(dist) == 0:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if one_sided:
|
|
84
|
+
p = np.mean(dist >= real_value)
|
|
85
|
+
else:
|
|
86
|
+
p = np.mean(np.abs(dist) >= abs(real_value))
|
|
87
|
+
|
|
88
|
+
self.p_values[metric] = float(p)
|
|
89
|
+
|
|
90
|
+
def summary(
|
|
91
|
+
self, metrics: List[str] = None
|
|
92
|
+
) -> Dict[str, Dict[str, float]]:
|
|
93
|
+
"""
|
|
94
|
+
Return a summary of real values, mean permuted values, and p-values.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
metrics (List[str]): List of metric names to include
|
|
98
|
+
in the summary. If None, uses DEFAULT_METRICS.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dict[str, Dict[str, float]]: A dictionary where each key
|
|
102
|
+
is a metric name and the value is another dictionary
|
|
103
|
+
with keys 'real', 'permuted_mean', and 'p_value'.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if metrics is None:
|
|
107
|
+
metrics = self.DEFAULT_METRICS
|
|
108
|
+
|
|
109
|
+
if not self.p_values: # lazy compute
|
|
110
|
+
self.compute_p_values(metrics=metrics)
|
|
111
|
+
|
|
112
|
+
summary_dict = {}
|
|
113
|
+
for metric in metrics:
|
|
114
|
+
real_value = getattr(self.real_metrics, metric, None)
|
|
115
|
+
if real_value is None:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
dist = np.array([
|
|
119
|
+
getattr(pm, metric, None)
|
|
120
|
+
for pm in self.permutated_metrics
|
|
121
|
+
if getattr(pm, metric, None) is not None
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
# Filter out inf / nan
|
|
125
|
+
dist = dist[np.isfinite(dist)]
|
|
126
|
+
|
|
127
|
+
real_value = getattr(self.real_metrics, metric, None)
|
|
128
|
+
|
|
129
|
+
if real_value is None or not np.isfinite(real_value):
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if len(dist) == 0:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
summary_dict[metric] = {
|
|
136
|
+
"real": float(real_value),
|
|
137
|
+
"permuted_mean": float(np.mean(dist)),
|
|
138
|
+
"p_value": self.p_values.get(metric, None),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return summary_dict
|
|
142
|
+
|
|
143
|
+
def save(self, path: str) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Save the permutation test results to disk (JSON + Parquet).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
path (str): The directory path where to save the results.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
None
|
|
152
|
+
"""
|
|
153
|
+
def ensure_iso(value):
|
|
154
|
+
if hasattr(value, "isoformat"):
|
|
155
|
+
if value.tzinfo is None:
|
|
156
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
157
|
+
return value.isoformat()
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
os.makedirs(path, exist_ok=True)
|
|
161
|
+
|
|
162
|
+
# Save the real metrics
|
|
163
|
+
self.real_metrics.save(os.path.join(path, "original_metrics.json"))
|
|
164
|
+
|
|
165
|
+
permuted_dir = os.path.join(path, "permuted_metrics")
|
|
166
|
+
os.makedirs(permuted_dir, exist_ok=True)
|
|
167
|
+
|
|
168
|
+
for i, pm in enumerate(self.permutated_metrics):
|
|
169
|
+
pm.save(os.path.join(permuted_dir, f"permuted_{i}.json"))
|
|
170
|
+
|
|
171
|
+
# Save the P-values
|
|
172
|
+
with open(os.path.join(path, "p_values.json"), "w") as f:
|
|
173
|
+
json.dump(self.p_values, f)
|
|
174
|
+
|
|
175
|
+
# Create a metadata file to store additional info such as
|
|
176
|
+
# date range name, start and end dates
|
|
177
|
+
metadata = {
|
|
178
|
+
"backtest_start_date": ensure_iso(self.backtest_start_date),
|
|
179
|
+
"backtest_date_range_name": self.backtest_date_range_name,
|
|
180
|
+
"backtest_end_date": ensure_iso(self.backtest_end_date),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
with open(os.path.join(path, "metadata.json"), "w") as f:
|
|
184
|
+
json.dump(metadata, f)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def open(path: str) -> "BacktestPermutationTest":
|
|
188
|
+
"""
|
|
189
|
+
Load the permutation test results from disk (JSON + Parquet).
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
path (str): The directory path where the results are saved.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
BacktestPermutationTest: The loaded permutation test results.
|
|
196
|
+
"""
|
|
197
|
+
original_metrics = os.path.join(path, "original_metrics.json")
|
|
198
|
+
|
|
199
|
+
# Rehydrate BacktestMetrics
|
|
200
|
+
real_metrics = BacktestMetrics.open(original_metrics)
|
|
201
|
+
|
|
202
|
+
permuted_dir = os.path.join(path, "permuted_metrics")
|
|
203
|
+
|
|
204
|
+
permutated_metrics = []
|
|
205
|
+
if os.path.exists(permuted_dir):
|
|
206
|
+
for fname in os.listdir(permuted_dir):
|
|
207
|
+
if fname.startswith("permuted_"):
|
|
208
|
+
pm = BacktestMetrics.open(
|
|
209
|
+
os.path.join(permuted_dir, fname)
|
|
210
|
+
)
|
|
211
|
+
permutated_metrics.append(pm)
|
|
212
|
+
|
|
213
|
+
p_values_path = os.path.join(path, "p_values.json")
|
|
214
|
+
p_values = {}
|
|
215
|
+
|
|
216
|
+
if os.path.exists(p_values_path):
|
|
217
|
+
with open(p_values_path, "r") as f:
|
|
218
|
+
p_values = json.load(f)
|
|
219
|
+
|
|
220
|
+
# Load metadata
|
|
221
|
+
metadata_path = os.path.join(path, "metadata.json")
|
|
222
|
+
backtest_start_date = None
|
|
223
|
+
backtest_end_date = None
|
|
224
|
+
backtest_date_range_name = None
|
|
225
|
+
|
|
226
|
+
if os.path.exists(metadata_path):
|
|
227
|
+
with open(metadata_path, "r") as f:
|
|
228
|
+
metadata = json.load(f)
|
|
229
|
+
|
|
230
|
+
backtest_start_date = pd.to_datetime(
|
|
231
|
+
metadata.get("backtest_start_date"), utc=True
|
|
232
|
+
)
|
|
233
|
+
backtest_end_date = pd.to_datetime(
|
|
234
|
+
metadata.get("backtest_end_date"), utc=True
|
|
235
|
+
)
|
|
236
|
+
backtest_date_range_name = metadata.get(
|
|
237
|
+
"backtest_date_range_name"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return BacktestPermutationTest(
|
|
241
|
+
real_metrics=real_metrics,
|
|
242
|
+
permutated_metrics=permutated_metrics,
|
|
243
|
+
p_values=p_values,
|
|
244
|
+
backtest_start_date=backtest_start_date,
|
|
245
|
+
backtest_end_date=backtest_end_date,
|
|
246
|
+
backtest_date_range_name=backtest_date_range_name
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def create_directory_name(self) -> str:
|
|
250
|
+
"""
|
|
251
|
+
Create a directory name for the backtest run based on its attributes.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
str: A string representing the directory name.
|
|
255
|
+
"""
|
|
256
|
+
start_str = self.real_metrics.backtest_start_date.strftime("%Y%m%d")
|
|
257
|
+
end_str = self.real_metrics.backtest_end_date.strftime("%Y%m%d")
|
|
258
|
+
dir_name = f"permutation_test_{start_str}_{end_str}"
|
|
259
|
+
return dir_name
|
|
260
|
+
|
|
261
|
+
def to_dict(self) -> Dict:
|
|
262
|
+
"""
|
|
263
|
+
Convert the permutation test results to a dictionary.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
dict: A dictionary representation of the permutation test results.
|
|
267
|
+
"""
|
|
268
|
+
return {
|
|
269
|
+
"real_metrics": self.real_metrics.to_dict(),
|
|
270
|
+
"permutated_metrics": [
|
|
271
|
+
pm.to_dict() for pm in self.permutated_metrics
|
|
272
|
+
],
|
|
273
|
+
"p_values": self.p_values,
|
|
274
|
+
# Note: DataFrames are not included in the dict representation
|
|
275
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from typing import Union, List, Optional
|
|
9
|
+
|
|
10
|
+
from investing_algorithm_framework.domain.exceptions \
|
|
11
|
+
import OperationalException
|
|
12
|
+
from investing_algorithm_framework.domain.models.order import Order, \
|
|
13
|
+
OrderSide, OrderStatus
|
|
14
|
+
from investing_algorithm_framework.domain.models.position import Position
|
|
15
|
+
from investing_algorithm_framework.domain.models.trade import Trade
|
|
16
|
+
from investing_algorithm_framework.domain.models.portfolio import \
|
|
17
|
+
PortfolioSnapshot
|
|
18
|
+
from investing_algorithm_framework.domain.models.trade.trade_status import \
|
|
19
|
+
TradeStatus
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from .backtest_metrics import BacktestMetrics
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class BacktestRun:
|
|
30
|
+
"""
|
|
31
|
+
Represents a backtest of an algorithm. It contains the backtest metrics,
|
|
32
|
+
backtest results, and paths to strategy and data files.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
backtest_metrics (Optional[List[BacktestMetrics]]): A list of
|
|
36
|
+
backtest metrics objects, each representing the performance
|
|
37
|
+
metrics of a single backtest run.
|
|
38
|
+
backtest_start_date (datetime): The start date of the backtest.
|
|
39
|
+
backtest_end_date (datetime): The end date of the backtest.
|
|
40
|
+
backtest_date_range_name (str): The name of the date range used for
|
|
41
|
+
the backtest.
|
|
42
|
+
trading_symbol (str): The trading symbol used in the backtest.
|
|
43
|
+
initial_unallocated (float): The initial unallocated amount in the
|
|
44
|
+
backtest.
|
|
45
|
+
number_of_runs (int): The number of runs in the backtest.
|
|
46
|
+
portfolio_snapshots (List[PortfolioSnapshot]): A list of portfolio
|
|
47
|
+
snapshots taken during the backtest.
|
|
48
|
+
trades (List[Trade]): A list of trades executed during the backtest.
|
|
49
|
+
orders (List[Order]): A list of orders placed during the backtest.
|
|
50
|
+
positions (List[Position]): A list of positions held during the
|
|
51
|
+
backtest.
|
|
52
|
+
created_at (datetime): The date and time when the backtest was created.
|
|
53
|
+
symbols (List[str]): A list of trading symbols involved in
|
|
54
|
+
the backtest.
|
|
55
|
+
number_of_days (int): The total number of days the backtest ran.
|
|
56
|
+
number_of_trades (int): The total number of trades executed during
|
|
57
|
+
the backtest.
|
|
58
|
+
number_of_trades_closed (int): The total number of trades that were
|
|
59
|
+
closed during the backtest.
|
|
60
|
+
number_of_trades_open (int): The total number of trades that are
|
|
61
|
+
still open at the end of the backtest.
|
|
62
|
+
number_of_orders (int): The total number of orders placed during
|
|
63
|
+
the backtest.
|
|
64
|
+
number_of_positions (int): The total number of positions held
|
|
65
|
+
during the backtest.
|
|
66
|
+
"""
|
|
67
|
+
backtest_start_date: datetime
|
|
68
|
+
backtest_end_date: datetime
|
|
69
|
+
trading_symbol: str
|
|
70
|
+
initial_unallocated: float = 0.0
|
|
71
|
+
number_of_runs: int = 0
|
|
72
|
+
portfolio_snapshots: List[PortfolioSnapshot] = field(default_factory=list)
|
|
73
|
+
trades: List[Trade] = field(default_factory=list)
|
|
74
|
+
orders: List[Order] = field(default_factory=list)
|
|
75
|
+
positions: List[Position] = field(default_factory=list)
|
|
76
|
+
created_at: datetime = None,
|
|
77
|
+
symbols: List[str] = field(default_factory=list)
|
|
78
|
+
number_of_days: int = 0
|
|
79
|
+
number_of_trades: int = 0
|
|
80
|
+
number_of_trades_closed: int = 0
|
|
81
|
+
number_of_trades_open: int = 0
|
|
82
|
+
number_of_orders: int = 0
|
|
83
|
+
number_of_positions: int = 0
|
|
84
|
+
backtest_metrics: BacktestMetrics = None
|
|
85
|
+
backtest_date_range_name: str = None
|
|
86
|
+
data_sources: List[Dict] = field(default_factory=list)
|
|
87
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict:
|
|
90
|
+
"""
|
|
91
|
+
Convert the Backtest instance to a dictionary with all
|
|
92
|
+
date/datetime fields as ISO strings (always UTC).
|
|
93
|
+
"""
|
|
94
|
+
def ensure_iso(value):
|
|
95
|
+
if hasattr(value, "isoformat"):
|
|
96
|
+
if value.tzinfo is None:
|
|
97
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
98
|
+
return value.isoformat()
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
backtest_metrics = self.backtest_metrics.to_dict() \
|
|
102
|
+
if self.backtest_metrics else None
|
|
103
|
+
return {
|
|
104
|
+
"backtest_metrics": backtest_metrics,
|
|
105
|
+
"backtest_start_date": ensure_iso(self.backtest_start_date),
|
|
106
|
+
"backtest_date_range_name": self.backtest_date_range_name,
|
|
107
|
+
"backtest_end_date": ensure_iso(self.backtest_end_date),
|
|
108
|
+
"trading_symbol": self.trading_symbol,
|
|
109
|
+
"initial_unallocated": self.initial_unallocated,
|
|
110
|
+
"number_of_runs": self.number_of_runs,
|
|
111
|
+
"portfolio_snapshots": [
|
|
112
|
+
ps.to_dict() for ps in self.portfolio_snapshots
|
|
113
|
+
],
|
|
114
|
+
"trades": [trade.to_dict() for trade in self.trades],
|
|
115
|
+
"orders": [order.to_dict() for order in self.orders],
|
|
116
|
+
"positions": [position.to_dict() for position in self.positions],
|
|
117
|
+
"created_at": ensure_iso(self.created_at),
|
|
118
|
+
"symbols": self.symbols,
|
|
119
|
+
"number_of_days": self.number_of_days,
|
|
120
|
+
"number_of_trades": self.number_of_trades,
|
|
121
|
+
"number_of_trades_closed": self.number_of_trades_closed,
|
|
122
|
+
"number_of_trades_open": self.number_of_trades_open,
|
|
123
|
+
"number_of_orders": self.number_of_orders,
|
|
124
|
+
"number_of_positions": self.number_of_positions,
|
|
125
|
+
"metadata": self.metadata,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def open(directory_path: Union[str, Path]) -> 'BacktestRun':
|
|
130
|
+
"""
|
|
131
|
+
Open a backtest report from a directory and return a Backtest instance.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
directory_path (str): The path to the directory containing the
|
|
135
|
+
backtest report files.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Backtest: An instance of Backtest with the loaded metrics
|
|
139
|
+
and results.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
OperationalException: If the directory does not exist or if
|
|
143
|
+
there is an error loading the files.
|
|
144
|
+
"""
|
|
145
|
+
backtest_metrics = None
|
|
146
|
+
|
|
147
|
+
if not os.path.exists(directory_path):
|
|
148
|
+
raise OperationalException(
|
|
149
|
+
f"The directory {directory_path} does not exist."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Load combined backtest metrics
|
|
153
|
+
metrics_file = os.path.join(directory_path, "metrics.json")
|
|
154
|
+
|
|
155
|
+
if os.path.isfile(metrics_file):
|
|
156
|
+
backtest_metrics = BacktestMetrics.open(metrics_file)
|
|
157
|
+
|
|
158
|
+
# Load backtest results
|
|
159
|
+
run_file = os.path.join(directory_path, "run.json")
|
|
160
|
+
|
|
161
|
+
if os.path.isfile(run_file):
|
|
162
|
+
data = json.load(open(run_file, 'r'))
|
|
163
|
+
else:
|
|
164
|
+
raise OperationalException(
|
|
165
|
+
f"The run file {run_file} does not exist."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Parse datetime fields
|
|
169
|
+
data["backtest_start_date"] = datetime.strptime(
|
|
170
|
+
data["backtest_start_date"], "%Y-%m-%d %H:%M:%S"
|
|
171
|
+
)
|
|
172
|
+
data["backtest_end_date"] = datetime.strptime(
|
|
173
|
+
data["backtest_end_date"], "%Y-%m-%d %H:%M:%S"
|
|
174
|
+
)
|
|
175
|
+
data["created_at"] = datetime.strptime(
|
|
176
|
+
data["created_at"], "%Y-%m-%d %H:%M:%S"
|
|
177
|
+
)
|
|
178
|
+
# Convert all to utc timezone
|
|
179
|
+
data["backtest_start_date"] = data[
|
|
180
|
+
"backtest_start_date"].replace(tzinfo=timezone.utc)
|
|
181
|
+
data["backtest_end_date"] = data[
|
|
182
|
+
"backtest_end_date"].replace(tzinfo=timezone.utc)
|
|
183
|
+
data["created_at"] = data["created_at"].replace(tzinfo=timezone.utc)
|
|
184
|
+
|
|
185
|
+
# Parse orders
|
|
186
|
+
data["orders"] = [
|
|
187
|
+
Order.from_dict(order) for order in data.get("orders", [])
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
# Parse positions
|
|
191
|
+
data["positions"] = [
|
|
192
|
+
Position.from_dict(position)
|
|
193
|
+
for position in data.get("positions", [])
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
# Parse trades
|
|
197
|
+
data["trades"] = [
|
|
198
|
+
Trade.from_dict(trade) for trade in data.get("trades", [])
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
# Parse portfolio snapshots
|
|
202
|
+
data["portfolio_snapshots"] = [
|
|
203
|
+
PortfolioSnapshot.from_dict(ps)
|
|
204
|
+
for ps in data.get("portfolio_snapshots", [])
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
return BacktestRun(
|
|
208
|
+
backtest_metrics=backtest_metrics,
|
|
209
|
+
**data
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def create_directory_name(self) -> str:
|
|
213
|
+
"""
|
|
214
|
+
Create a directory name for the backtest run based on its attributes.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
str: A string representing the directory name.
|
|
218
|
+
"""
|
|
219
|
+
start_str = self.backtest_start_date.strftime("%Y%m%d")
|
|
220
|
+
end_str = self.backtest_end_date.strftime("%Y%m%d")
|
|
221
|
+
dir_name = f"backtest_{self.trading_symbol}_{start_str}_{end_str}"
|
|
222
|
+
return dir_name
|
|
223
|
+
|
|
224
|
+
def save(self, directory_path: Union[str, Path]) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Save the backtest run to a directory.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
directory_path (str): The directory where the metrics
|
|
230
|
+
file will be saved.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
OperationalException: If the directory does not exist or if
|
|
234
|
+
there is an error saving the files.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
None: This method does not return anything, it saves the
|
|
238
|
+
metrics to a file.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
metrics_path = os.path.join(directory_path, "metrics.json")
|
|
242
|
+
run_path = os.path.join(directory_path, "run.json")
|
|
243
|
+
|
|
244
|
+
if not os.path.exists(directory_path):
|
|
245
|
+
os.makedirs(directory_path)
|
|
246
|
+
|
|
247
|
+
# Call the save method of BacktestMetrics
|
|
248
|
+
if self.backtest_metrics:
|
|
249
|
+
self.backtest_metrics.save(metrics_path)
|
|
250
|
+
|
|
251
|
+
# Save the run data
|
|
252
|
+
with open(run_path, 'w') as f:
|
|
253
|
+
# string format datetime objects
|
|
254
|
+
data = self.to_dict()
|
|
255
|
+
|
|
256
|
+
# Remove backtest_metrics to avoid redundancy
|
|
257
|
+
data.pop("backtest_metrics", None)
|
|
258
|
+
|
|
259
|
+
# Ensure datetime objects are in UTC before formatting
|
|
260
|
+
backtest_start_date = self.backtest_start_date
|
|
261
|
+
|
|
262
|
+
if backtest_start_date.tzinfo is None:
|
|
263
|
+
# Naive datetime - treat as UTC
|
|
264
|
+
backtest_start_date = backtest_start_date.replace(
|
|
265
|
+
tzinfo=timezone.utc
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
# Timezone-aware - convert to UTC
|
|
269
|
+
backtest_start_date = backtest_start_date.astimezone(
|
|
270
|
+
timezone.utc
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
backtest_end_date = self.backtest_end_date
|
|
274
|
+
if backtest_end_date.tzinfo is None:
|
|
275
|
+
backtest_end_date = backtest_end_date.replace(
|
|
276
|
+
tzinfo=timezone.utc
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
backtest_end_date = backtest_end_date.astimezone(timezone.utc)
|
|
280
|
+
|
|
281
|
+
created_at = self.created_at
|
|
282
|
+
if created_at.tzinfo is None:
|
|
283
|
+
created_at = created_at.replace(tzinfo=timezone.utc)
|
|
284
|
+
else:
|
|
285
|
+
created_at = created_at.astimezone(timezone.utc)
|
|
286
|
+
|
|
287
|
+
data["backtest_start_date"] = backtest_start_date.strftime(
|
|
288
|
+
"%Y-%m-%d %H:%M:%S"
|
|
289
|
+
)
|
|
290
|
+
data["backtest_end_date"] = backtest_end_date.strftime(
|
|
291
|
+
"%Y-%m-%d %H:%M:%S"
|
|
292
|
+
)
|
|
293
|
+
data["created_at"] = created_at.strftime(
|
|
294
|
+
"%Y-%m-%d %H:%M:%S"
|
|
295
|
+
)
|
|
296
|
+
json.dump(data, f, default=str)
|
|
297
|
+
|
|
298
|
+
def get_trades(self, target_symbol=None, trade_status=None) -> List[Trade]:
|
|
299
|
+
"""
|
|
300
|
+
Get the trades of a backtest report
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
target_symbol (str): The target_symbol
|
|
304
|
+
trade_status: The trade_status
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
list: The trades of the backtest report
|
|
308
|
+
"""
|
|
309
|
+
selection = self.trades
|
|
310
|
+
|
|
311
|
+
if target_symbol is not None:
|
|
312
|
+
selection = [
|
|
313
|
+
trade for trade in selection
|
|
314
|
+
if trade.target_symbol.lower() == target_symbol.lower()
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
if trade_status is not None:
|
|
318
|
+
trade_status = TradeStatus.from_value(trade_status)
|
|
319
|
+
selection = [
|
|
320
|
+
trade for trade in selection
|
|
321
|
+
if trade.status == trade_status.value
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
return selection
|
|
325
|
+
|
|
326
|
+
def get_portfolio_snapshots(
|
|
327
|
+
self,
|
|
328
|
+
created_at_lt: Optional[datetime] = None,
|
|
329
|
+
created_at_lte: Optional[datetime] = None,
|
|
330
|
+
created_at_gt: Optional[datetime] = None,
|
|
331
|
+
created_at_gte: Optional[datetime] = None
|
|
332
|
+
) -> List[PortfolioSnapshot]:
|
|
333
|
+
"""
|
|
334
|
+
Get the portfolio snapshots of the backtest report
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
created_at_lt (datetime): The created_at date to filter
|
|
338
|
+
the snapshots
|
|
339
|
+
created_at_lte (datetime): The created_at date to filter
|
|
340
|
+
the snapshots
|
|
341
|
+
created_at_gt (datetime): The created_at date to filter
|
|
342
|
+
the snapshots
|
|
343
|
+
created_at_gte (datetime): The created_at date to filter
|
|
344
|
+
the snapshots
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
list: The portfolio snapshots of the backtest report
|
|
348
|
+
"""
|
|
349
|
+
selection = self.portfolio_snapshots
|
|
350
|
+
|
|
351
|
+
if created_at_lt is not None:
|
|
352
|
+
selection = [
|
|
353
|
+
snapshot for snapshot in selection
|
|
354
|
+
if snapshot.created_at < created_at_lt
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
if created_at_lte is not None:
|
|
358
|
+
selection = [
|
|
359
|
+
snapshot for snapshot in selection
|
|
360
|
+
if snapshot.created_at <= created_at_lte
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
if created_at_gt is not None:
|
|
364
|
+
selection = [
|
|
365
|
+
snapshot for snapshot in selection
|
|
366
|
+
if snapshot.created_at > created_at_gt
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
if created_at_gte is not None:
|
|
370
|
+
selection = [
|
|
371
|
+
snapshot for snapshot in selection
|
|
372
|
+
if snapshot.created_at >= created_at_gte
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
return selection
|
|
376
|
+
|
|
377
|
+
def get_orders(
|
|
378
|
+
self,
|
|
379
|
+
target_symbol=None,
|
|
380
|
+
order_side=None,
|
|
381
|
+
order_status=None,
|
|
382
|
+
created_at_lt=None,
|
|
383
|
+
) -> List[Order]:
|
|
384
|
+
"""
|
|
385
|
+
Get the orders of a backtest report
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
target_symbol (str): The target_symbol
|
|
389
|
+
order_side (str): The order side
|
|
390
|
+
order_status (str): The order status
|
|
391
|
+
created_at_lt (datetime): The created_at date to filter the orders
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
list: The orders of the backtest report
|
|
395
|
+
"""
|
|
396
|
+
selection = self.orders
|
|
397
|
+
|
|
398
|
+
if created_at_lt is not None:
|
|
399
|
+
selection = [
|
|
400
|
+
order for order in selection
|
|
401
|
+
if order.created_at < created_at_lt
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
if target_symbol is not None:
|
|
405
|
+
selection = [
|
|
406
|
+
order for order in selection
|
|
407
|
+
if order.target_symbol == target_symbol
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
if order_side is not None:
|
|
411
|
+
order_side = OrderSide.from_value(order_side)
|
|
412
|
+
selection = [
|
|
413
|
+
order for order in selection
|
|
414
|
+
if order.order_side == order_side.value
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
if order_status is not None:
|
|
418
|
+
status = OrderStatus.from_value(order_status)
|
|
419
|
+
selection = [
|
|
420
|
+
order for order in selection
|
|
421
|
+
if order.status == status.value
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
return selection
|
|
425
|
+
|
|
426
|
+
def __repr__(self):
|
|
427
|
+
"""
|
|
428
|
+
Return a string representation of the Backtest instance.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
str: A string representation of the Backtest instance.
|
|
432
|
+
"""
|
|
433
|
+
return json.dumps(
|
|
434
|
+
self.to_dict(), indent=4, sort_keys=True, default=str
|
|
435
|
+
)
|