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.

Files changed (260) hide show
  1. investing_algorithm_framework/__init__.py +197 -0
  2. investing_algorithm_framework/app/__init__.py +47 -0
  3. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  4. investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
  5. investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
  6. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  7. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  8. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  9. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  10. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  11. investing_algorithm_framework/app/app.py +2204 -0
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1667 -0
  14. investing_algorithm_framework/app/eventloop.py +590 -0
  15. investing_algorithm_framework/app/reporting/__init__.py +27 -0
  16. investing_algorithm_framework/app/reporting/ascii.py +921 -0
  17. investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
  18. investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
  19. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  20. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  21. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
  22. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  23. investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
  24. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  25. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
  26. investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
  27. investing_algorithm_framework/app/reporting/generate.py +185 -0
  28. investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
  29. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
  30. investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
  31. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
  32. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  33. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  34. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  35. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  36. investing_algorithm_framework/app/stateless/__init__.py +35 -0
  37. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
  38. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
  39. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
  40. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
  41. investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
  42. investing_algorithm_framework/app/strategy.py +675 -0
  43. investing_algorithm_framework/app/task.py +41 -0
  44. investing_algorithm_framework/app/web/__init__.py +5 -0
  45. investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
  46. investing_algorithm_framework/app/web/controllers/orders.py +20 -0
  47. investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
  48. investing_algorithm_framework/app/web/controllers/positions.py +18 -0
  49. investing_algorithm_framework/app/web/create_app.py +20 -0
  50. investing_algorithm_framework/app/web/error_handler.py +59 -0
  51. investing_algorithm_framework/app/web/responses.py +20 -0
  52. investing_algorithm_framework/app/web/run_strategies.py +4 -0
  53. investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
  54. investing_algorithm_framework/app/web/schemas/order.py +12 -0
  55. investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
  56. investing_algorithm_framework/app/web/schemas/position.py +15 -0
  57. investing_algorithm_framework/app/web/setup_cors.py +6 -0
  58. investing_algorithm_framework/cli/__init__.py +0 -0
  59. investing_algorithm_framework/cli/cli.py +207 -0
  60. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
  61. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  62. investing_algorithm_framework/cli/initialize_app.py +603 -0
  63. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  64. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  65. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  66. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  67. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  68. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  69. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  70. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  71. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  72. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  73. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  74. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  75. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  76. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  77. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  78. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  79. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  80. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  81. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  82. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  83. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  84. investing_algorithm_framework/create_app.py +54 -0
  85. investing_algorithm_framework/dependency_container.py +155 -0
  86. investing_algorithm_framework/domain/__init__.py +148 -0
  87. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  88. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  89. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  90. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  91. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  92. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  93. investing_algorithm_framework/domain/backtesting/backtest_run.py +435 -0
  94. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  95. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  96. investing_algorithm_framework/domain/config.py +111 -0
  97. investing_algorithm_framework/domain/constants.py +83 -0
  98. investing_algorithm_framework/domain/data_provider.py +334 -0
  99. investing_algorithm_framework/domain/data_structures.py +42 -0
  100. investing_algorithm_framework/domain/decimal_parsing.py +40 -0
  101. investing_algorithm_framework/domain/exceptions.py +112 -0
  102. investing_algorithm_framework/domain/models/__init__.py +43 -0
  103. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  104. investing_algorithm_framework/domain/models/base_model.py +25 -0
  105. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  106. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  107. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  108. investing_algorithm_framework/domain/models/event.py +35 -0
  109. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  110. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  111. investing_algorithm_framework/domain/models/order/__init__.py +6 -0
  112. investing_algorithm_framework/domain/models/order/order.py +384 -0
  113. investing_algorithm_framework/domain/models/order/order_side.py +36 -0
  114. investing_algorithm_framework/domain/models/order/order_status.py +37 -0
  115. investing_algorithm_framework/domain/models/order/order_type.py +30 -0
  116. investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
  117. investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
  118. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
  119. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
  120. investing_algorithm_framework/domain/models/position/__init__.py +4 -0
  121. investing_algorithm_framework/domain/models/position/position.py +68 -0
  122. investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
  123. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  124. investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
  125. investing_algorithm_framework/domain/models/time_frame.py +153 -0
  126. investing_algorithm_framework/domain/models/time_interval.py +124 -0
  127. investing_algorithm_framework/domain/models/time_unit.py +149 -0
  128. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  129. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  130. investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
  131. investing_algorithm_framework/domain/models/trade/trade.py +388 -0
  132. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
  133. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  134. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
  135. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
  136. investing_algorithm_framework/domain/order_executor.py +112 -0
  137. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  138. investing_algorithm_framework/domain/positions/__init__.py +4 -0
  139. investing_algorithm_framework/domain/positions/position_size.py +41 -0
  140. investing_algorithm_framework/domain/services/__init__.py +11 -0
  141. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  142. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  143. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  144. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  145. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  146. investing_algorithm_framework/domain/stateless_actions.py +7 -0
  147. investing_algorithm_framework/domain/strategy.py +44 -0
  148. investing_algorithm_framework/domain/utils/__init__.py +27 -0
  149. investing_algorithm_framework/domain/utils/csv.py +104 -0
  150. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  151. investing_algorithm_framework/domain/utils/dates.py +57 -0
  152. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  153. investing_algorithm_framework/domain/utils/polars.py +53 -0
  154. investing_algorithm_framework/domain/utils/random.py +41 -0
  155. investing_algorithm_framework/domain/utils/signatures.py +17 -0
  156. investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
  157. investing_algorithm_framework/domain/utils/synchronized.py +12 -0
  158. investing_algorithm_framework/download_data.py +108 -0
  159. investing_algorithm_framework/infrastructure/__init__.py +50 -0
  160. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  161. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
  162. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  163. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  164. investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
  165. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
  166. investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
  167. investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
  168. investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
  169. investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
  170. investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
  171. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  172. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  173. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
  174. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
  175. investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
  176. investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
  177. investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
  178. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
  179. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  180. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  181. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
  182. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
  183. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  184. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  185. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  186. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  187. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  188. investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
  189. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  190. investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
  191. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
  192. investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
  193. investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
  194. investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
  195. investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
  196. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  197. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
  198. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
  199. investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
  200. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  201. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
  202. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  203. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  204. investing_algorithm_framework/services/__init__.py +132 -0
  205. investing_algorithm_framework/services/backtesting/__init__.py +5 -0
  206. investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
  207. investing_algorithm_framework/services/configuration_service.py +96 -0
  208. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  209. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  210. investing_algorithm_framework/services/market_credential_service.py +40 -0
  211. investing_algorithm_framework/services/metrics/__init__.py +114 -0
  212. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  213. investing_algorithm_framework/services/metrics/beta.py +0 -0
  214. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  215. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  216. investing_algorithm_framework/services/metrics/drawdown.py +181 -0
  217. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  218. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  219. investing_algorithm_framework/services/metrics/generate.py +358 -0
  220. investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
  221. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  222. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  223. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  224. investing_algorithm_framework/services/metrics/returns.py +452 -0
  225. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  226. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  227. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  228. investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
  229. investing_algorithm_framework/services/metrics/trades.py +500 -0
  230. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  231. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  232. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  233. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  234. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  235. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  236. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  237. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  238. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  239. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  240. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  241. investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
  242. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  243. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  244. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  245. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  246. investing_algorithm_framework/services/positions/__init__.py +7 -0
  247. investing_algorithm_framework/services/positions/position_service.py +210 -0
  248. investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
  249. investing_algorithm_framework/services/repository_service.py +40 -0
  250. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  251. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
  252. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
  253. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
  254. investing_algorithm_framework/services/trade_service/__init__.py +3 -0
  255. investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
  256. investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
  257. investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
  258. investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
  259. investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
  260. 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
+ )