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