investing-algorithm-framework 1.5__py3-none-any.whl → 7.25.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. investing_algorithm_framework/__init__.py +192 -16
  2. investing_algorithm_framework/analysis/__init__.py +16 -0
  3. investing_algorithm_framework/analysis/backtest_data_ranges.py +202 -0
  4. investing_algorithm_framework/analysis/data.py +170 -0
  5. investing_algorithm_framework/analysis/markdown.py +91 -0
  6. investing_algorithm_framework/analysis/ranking.py +298 -0
  7. investing_algorithm_framework/app/__init__.py +29 -4
  8. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  9. investing_algorithm_framework/app/algorithm/algorithm.py +193 -0
  10. investing_algorithm_framework/app/algorithm/algorithm_factory.py +118 -0
  11. investing_algorithm_framework/app/app.py +2220 -379
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1724 -0
  14. investing_algorithm_framework/app/eventloop.py +620 -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/time_metrics_table.py +80 -0
  31. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  32. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  33. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  34. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  35. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +6 -3
  36. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
  37. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +2 -1
  38. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
  39. investing_algorithm_framework/app/strategy.py +867 -60
  40. investing_algorithm_framework/app/task.py +5 -3
  41. investing_algorithm_framework/app/web/__init__.py +2 -1
  42. investing_algorithm_framework/app/web/controllers/__init__.py +2 -2
  43. investing_algorithm_framework/app/web/controllers/orders.py +3 -2
  44. investing_algorithm_framework/app/web/controllers/positions.py +2 -2
  45. investing_algorithm_framework/app/web/create_app.py +4 -2
  46. investing_algorithm_framework/app/web/schemas/position.py +1 -0
  47. investing_algorithm_framework/cli/__init__.py +0 -0
  48. investing_algorithm_framework/cli/cli.py +231 -0
  49. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
  50. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  51. investing_algorithm_framework/cli/initialize_app.py +603 -0
  52. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  53. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  54. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  55. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  56. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  57. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  58. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  59. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  60. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  61. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  62. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  63. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  64. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  65. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  66. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  67. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  68. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  69. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  70. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  71. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  72. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  73. investing_algorithm_framework/cli/validate_backtest_checkpoints.py +197 -0
  74. investing_algorithm_framework/create_app.py +40 -7
  75. investing_algorithm_framework/dependency_container.py +100 -47
  76. investing_algorithm_framework/domain/__init__.py +97 -30
  77. investing_algorithm_framework/domain/algorithm_id.py +69 -0
  78. investing_algorithm_framework/domain/backtesting/__init__.py +25 -0
  79. investing_algorithm_framework/domain/backtesting/backtest.py +548 -0
  80. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +113 -0
  81. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +241 -0
  82. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +470 -0
  83. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  84. investing_algorithm_framework/domain/backtesting/backtest_run.py +663 -0
  85. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  86. investing_algorithm_framework/domain/backtesting/backtest_utils.py +198 -0
  87. investing_algorithm_framework/domain/backtesting/combine_backtests.py +392 -0
  88. investing_algorithm_framework/domain/config.py +59 -136
  89. investing_algorithm_framework/domain/constants.py +18 -37
  90. investing_algorithm_framework/domain/data_provider.py +334 -0
  91. investing_algorithm_framework/domain/data_structures.py +42 -0
  92. investing_algorithm_framework/domain/exceptions.py +51 -1
  93. investing_algorithm_framework/domain/models/__init__.py +26 -19
  94. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  95. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  96. investing_algorithm_framework/domain/models/data/data_source.py +222 -0
  97. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  98. investing_algorithm_framework/domain/models/event.py +35 -0
  99. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  100. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  101. investing_algorithm_framework/domain/models/order/__init__.py +3 -4
  102. investing_algorithm_framework/domain/models/order/order.py +198 -65
  103. investing_algorithm_framework/domain/models/order/order_status.py +2 -2
  104. investing_algorithm_framework/domain/models/order/order_type.py +1 -3
  105. investing_algorithm_framework/domain/models/portfolio/__init__.py +6 -2
  106. investing_algorithm_framework/domain/models/portfolio/portfolio.py +98 -3
  107. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +37 -43
  108. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
  109. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  110. investing_algorithm_framework/domain/models/position/position.py +20 -0
  111. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  112. investing_algorithm_framework/domain/models/position/position_snapshot.py +0 -2
  113. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  114. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  115. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  116. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  117. investing_algorithm_framework/domain/models/strategy_profile.py +19 -141
  118. investing_algorithm_framework/domain/models/time_frame.py +94 -98
  119. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  120. investing_algorithm_framework/domain/models/time_unit.py +66 -2
  121. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  122. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  123. investing_algorithm_framework/domain/models/trade/__init__.py +11 -0
  124. investing_algorithm_framework/domain/models/trade/trade.py +389 -0
  125. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  126. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
  127. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
  128. investing_algorithm_framework/domain/order_executor.py +112 -0
  129. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  130. investing_algorithm_framework/domain/services/__init__.py +11 -0
  131. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  132. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  133. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  134. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  135. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  136. investing_algorithm_framework/domain/strategy.py +1 -29
  137. investing_algorithm_framework/domain/utils/__init__.py +15 -5
  138. investing_algorithm_framework/domain/utils/csv.py +22 -0
  139. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  140. investing_algorithm_framework/domain/utils/dates.py +57 -0
  141. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  142. investing_algorithm_framework/domain/utils/polars.py +53 -0
  143. investing_algorithm_framework/domain/utils/random.py +29 -0
  144. investing_algorithm_framework/download_data.py +244 -0
  145. investing_algorithm_framework/infrastructure/__init__.py +37 -11
  146. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  147. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1152 -0
  148. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  149. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  150. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  151. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
  152. investing_algorithm_framework/infrastructure/models/__init__.py +7 -3
  153. investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -2
  154. investing_algorithm_framework/infrastructure/models/order/order.py +53 -53
  155. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  156. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  157. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +1 -1
  158. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -2
  159. investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -6
  160. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +3 -1
  161. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  162. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  163. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
  164. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
  165. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  166. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  167. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  168. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  169. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  170. investing_algorithm_framework/infrastructure/repositories/__init__.py +10 -4
  171. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  172. investing_algorithm_framework/infrastructure/repositories/order_repository.py +16 -5
  173. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +2 -2
  174. investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
  175. investing_algorithm_framework/infrastructure/repositories/repository.py +84 -30
  176. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  177. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
  178. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
  179. investing_algorithm_framework/infrastructure/services/__init__.py +9 -4
  180. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  181. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +193 -0
  182. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  183. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  184. investing_algorithm_framework/infrastructure/services/backtesting/__init__.py +9 -0
  185. investing_algorithm_framework/infrastructure/services/backtesting/backtest_service.py +2596 -0
  186. investing_algorithm_framework/infrastructure/services/backtesting/event_backtest_service.py +285 -0
  187. investing_algorithm_framework/infrastructure/services/backtesting/vector_backtest_service.py +468 -0
  188. investing_algorithm_framework/services/__init__.py +123 -15
  189. investing_algorithm_framework/services/configuration_service.py +77 -11
  190. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  191. investing_algorithm_framework/services/data_providers/data_provider_service.py +1058 -0
  192. investing_algorithm_framework/services/market_credential_service.py +40 -0
  193. investing_algorithm_framework/services/metrics/__init__.py +119 -0
  194. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  195. investing_algorithm_framework/services/metrics/beta.py +0 -0
  196. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  197. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  198. investing_algorithm_framework/services/metrics/drawdown.py +218 -0
  199. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  200. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  201. investing_algorithm_framework/services/metrics/generate.py +358 -0
  202. investing_algorithm_framework/services/metrics/mean_daily_return.py +84 -0
  203. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  204. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  205. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  206. investing_algorithm_framework/services/metrics/returns.py +452 -0
  207. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  208. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  209. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  210. investing_algorithm_framework/services/metrics/standard_deviation.py +156 -0
  211. investing_algorithm_framework/services/metrics/trades.py +473 -0
  212. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  213. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  214. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  215. investing_algorithm_framework/services/metrics/volatility.py +118 -0
  216. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  217. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  218. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  219. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  220. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  221. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  222. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  223. investing_algorithm_framework/services/{portfolio_configuration_service.py → portfolios/portfolio_configuration_service.py} +27 -12
  224. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  225. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  226. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  227. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  228. investing_algorithm_framework/services/positions/__init__.py +7 -0
  229. investing_algorithm_framework/services/positions/position_service.py +210 -0
  230. investing_algorithm_framework/services/repository_service.py +8 -2
  231. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  232. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +117 -0
  233. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  234. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  235. investing_algorithm_framework/services/trade_service/__init__.py +9 -0
  236. investing_algorithm_framework/services/trade_service/trade_service.py +1099 -0
  237. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  238. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  239. investing_algorithm_framework-7.25.6.dist-info/METADATA +535 -0
  240. investing_algorithm_framework-7.25.6.dist-info/RECORD +268 -0
  241. {investing_algorithm_framework-1.5.dist-info → investing_algorithm_framework-7.25.6.dist-info}/WHEEL +1 -2
  242. investing_algorithm_framework-7.25.6.dist-info/entry_points.txt +3 -0
  243. investing_algorithm_framework/app/algorithm.py +0 -630
  244. investing_algorithm_framework/domain/models/backtest_profile.py +0 -414
  245. investing_algorithm_framework/domain/models/market_data/__init__.py +0 -11
  246. investing_algorithm_framework/domain/models/market_data/asset_price.py +0 -50
  247. investing_algorithm_framework/domain/models/market_data/ohlcv.py +0 -105
  248. investing_algorithm_framework/domain/models/market_data/order_book.py +0 -63
  249. investing_algorithm_framework/domain/models/market_data/ticker.py +0 -92
  250. investing_algorithm_framework/domain/models/order/order_fee.py +0 -45
  251. investing_algorithm_framework/domain/models/trade.py +0 -78
  252. investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
  253. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  254. investing_algorithm_framework/domain/singleton.py +0 -9
  255. investing_algorithm_framework/domain/utils/backtesting.py +0 -82
  256. investing_algorithm_framework/infrastructure/models/order/order_fee.py +0 -21
  257. investing_algorithm_framework/infrastructure/repositories/order_fee_repository.py +0 -15
  258. investing_algorithm_framework/infrastructure/services/market_backtest_service.py +0 -360
  259. investing_algorithm_framework/infrastructure/services/market_service.py +0 -410
  260. investing_algorithm_framework/infrastructure/services/performance_service.py +0 -192
  261. investing_algorithm_framework/services/backtest_service.py +0 -268
  262. investing_algorithm_framework/services/market_data_service.py +0 -77
  263. investing_algorithm_framework/services/order_backtest_service.py +0 -122
  264. investing_algorithm_framework/services/order_service.py +0 -752
  265. investing_algorithm_framework/services/portfolio_service.py +0 -164
  266. investing_algorithm_framework/services/portfolio_snapshot_service.py +0 -68
  267. investing_algorithm_framework/services/position_cost_service.py +0 -5
  268. investing_algorithm_framework/services/position_service.py +0 -63
  269. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -225
  270. investing_algorithm_framework-1.5.dist-info/AUTHORS.md +0 -8
  271. investing_algorithm_framework-1.5.dist-info/METADATA +0 -230
  272. investing_algorithm_framework-1.5.dist-info/RECORD +0 -119
  273. investing_algorithm_framework-1.5.dist-info/top_level.txt +0 -1
  274. /investing_algorithm_framework/{infrastructure/services/performance_backtest_service.py → app/reporting/tables/stop_loss_table.py} +0 -0
  275. /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
  276. {investing_algorithm_framework-1.5.dist-info → investing_algorithm_framework-7.25.6.dist-info}/LICENSE +0 -0
@@ -0,0 +1,548 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from dataclasses import dataclass, field
5
+ from logging import getLogger
6
+ from typing import Dict, Union, List
7
+
8
+ from investing_algorithm_framework.domain.exceptions \
9
+ import OperationalException
10
+
11
+ from .backtest_metrics import BacktestMetrics
12
+ from .backtest_run import BacktestRun
13
+ from .backtest_permutation_test import BacktestPermutationTest
14
+ from .backtest_date_range import BacktestDateRange
15
+ from .backtest_summary_metrics import BacktestSummaryMetrics
16
+ from .combine_backtests import generate_backtest_summary_metrics
17
+
18
+
19
+ logger = getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class Backtest:
24
+ """
25
+ Represents a backtest of an algorithm. It contains the backtest metrics,
26
+ backtest results, and paths to strategy and data files.
27
+
28
+ Attributes:
29
+ backtest_runs (List[BacktestRun]): A list of backtest runs,
30
+ each representing the performance metrics of a single
31
+ backtest run.
32
+ backtest_summary (BacktestSummaryMetrics): An aggregated view of
33
+ the backtest metrics, combining results from multiple backtests
34
+ metrics into a single summary.
35
+ backtest_permutation_tests (List[BacktestPermutationTestMetrics]): A
36
+ list of backtest permutation tests,
37
+ each representing the performance metrics of a single
38
+ backtest permutation test.
39
+ metadata (Dict[str, str]): Metadata related to the backtest, such as
40
+ configuration parameters or additional information about the
41
+ strategy that was backtested. This can be used for later
42
+ reference or analysis.
43
+ risk_free_rate (float): The risk-free rate used in the backtest,
44
+ typically expressed as a decimal (e.g., 0.03 for 3%). This
45
+ strategy_ids (List[int]): List of strategy IDs associated with
46
+ this backtest.
47
+ algorithm_id (int): The ID of the algorithm associated with this
48
+ backtest.
49
+ """
50
+ algorithm_id: str
51
+ backtest_runs: List[BacktestRun] = field(default_factory=list)
52
+ backtest_summary: BacktestSummaryMetrics = field(default=None)
53
+ backtest_permutation_tests: List[BacktestPermutationTest] = \
54
+ field(default_factory=list)
55
+ metadata: Dict[str, str] = field(default_factory=dict)
56
+ risk_free_rate: float = None
57
+ strategy_ids: List[int] = field(default_factory=list)
58
+
59
+ def get_all_backtest_runs(
60
+ self, backtest_date_ranges=None
61
+ ) -> List[BacktestRun]:
62
+ """
63
+ Retrieve all BacktestRun instances from the backtest.
64
+
65
+ Args:
66
+ backtest_date_ranges (List[BacktestDateRange], optional): A list of
67
+ date ranges to filter the backtest runs. If provided, only
68
+
69
+ Returns:
70
+ List[BacktestRun]: A list of all BacktestRun instances.
71
+ """
72
+
73
+ if backtest_date_ranges is not None:
74
+ filtered_runs = []
75
+ for date_range in backtest_date_ranges:
76
+ run = self.get_backtest_run(date_range)
77
+ if run:
78
+ filtered_runs.append(run)
79
+ return filtered_runs
80
+
81
+ return self.backtest_runs
82
+
83
+ def get_backtest_run(
84
+ self, date_range: BacktestDateRange
85
+ ) -> Union[BacktestRun, None]:
86
+ """
87
+ Retrieve a specific BacktestRun based on the provided date range.
88
+
89
+ Args:
90
+ date_range (BacktestDateRange): The date range to search for.
91
+
92
+ Returns:
93
+ Union[BacktestRun, None]: The matching BacktestRun if found,
94
+ otherwise None.
95
+ """
96
+ for run in self.backtest_runs:
97
+ if (run.backtest_start_date == date_range.start_date and
98
+ run.backtest_end_date == date_range.end_date):
99
+ return run
100
+ return None
101
+
102
+ def get_all_backtest_permutation_tests(
103
+ self
104
+ ) -> List[BacktestPermutationTest]:
105
+ """
106
+ Retrieve all BacktestPermutationTest instances from the backtest.
107
+
108
+ Returns:
109
+ List[BacktestPermutationTest]: A list of all
110
+ BacktestPermutationTest instances.
111
+ """
112
+ return self.backtest_permutation_tests
113
+
114
+ def get_backtest_permutation_test(
115
+ self, date_range: BacktestDateRange
116
+ ) -> Union[BacktestPermutationTest, None]:
117
+ """
118
+ Retrieve a specific BacktestPermutationTest based on
119
+ the provided date range.
120
+
121
+ Args:
122
+ date_range (BacktestDateRange): The date range to search for.
123
+
124
+ Returns:
125
+ Union[BacktestPermutationTest, None]: The
126
+ matching BacktestPermutationTest if found,
127
+ otherwise None.
128
+ """
129
+ for perm_test in self.backtest_permutation_tests:
130
+ if (perm_test.backtest_start_date == date_range.start_date and
131
+ perm_test.backtest_end_date == date_range.end_date):
132
+ return perm_test
133
+ return None
134
+
135
+ def get_all_backtest_metrics(self) -> List[BacktestMetrics]:
136
+ """
137
+ Retrieve all BacktestMetrics from the backtest runs.
138
+
139
+ Returns:
140
+ List[BacktestMetrics]: A list of BacktestMetrics from
141
+ all backtest runs.
142
+ """
143
+ return [
144
+ run.backtest_metrics for run in self.backtest_runs
145
+ if run.backtest_metrics
146
+ ]
147
+
148
+ def get_backtest_metrics(
149
+ self, date_range: BacktestDateRange
150
+ ) -> Union[BacktestMetrics, None]:
151
+ """
152
+ Retrieve the BacktestMetrics for a specific BacktestRun based on
153
+ the provided date range.
154
+
155
+ Args:
156
+ date_range (Optional[BacktestDateRange]): The date range to
157
+ search for.
158
+
159
+ Returns:
160
+ Union[BacktestMetrics, None]: The BacktestMetrics of the matching
161
+ BacktestRun if found, otherwise None.
162
+ """
163
+ run = self.get_backtest_run(date_range)
164
+ if run:
165
+ return run.backtest_metrics
166
+ return None
167
+
168
+ def to_dict(self) -> dict:
169
+ """
170
+ Convert the Backtest instance to a dictionary.
171
+
172
+ Returns:
173
+ dict: A dictionary representation of the Backtest instance.
174
+ """
175
+
176
+ backtest_summary = self.backtest_summary.to_dict() \
177
+ if self.backtest_summary else None
178
+ return {
179
+ "backtest_runs": [
180
+ br.to_dict() for br in self.backtest_runs
181
+ ] if self.backtest_runs else None,
182
+ "backtest_summary": backtest_summary,
183
+ "backtest_permutation_tests":
184
+ [
185
+ bpt.to_dict() for bpt in self.backtest_permutation_tests
186
+ ] if self.backtest_permutation_tests else None,
187
+ "metadata": self.metadata,
188
+ "risk_free_rate": self.risk_free_rate,
189
+ "strategy_ids": self.strategy_ids,
190
+ "algorithm_id": self.algorithm_id
191
+ }
192
+
193
+ @staticmethod
194
+ def open(
195
+ directory_path: Union[str, Path],
196
+ backtest_date_ranges: List[BacktestDateRange] = None,
197
+ ) -> 'Backtest':
198
+ """
199
+ Open a backtest report from a directory and return a Backtest instance.
200
+
201
+ Args:
202
+ directory_path (str): The path to the directory containing the
203
+ backtest report files.
204
+ backtest_date_ranges (List[BacktestDateRange], optional): A list of
205
+ date ranges to filter the backtest runs. If provided, only
206
+ backtest runs matching these date ranges will be loaded.
207
+
208
+ Returns:
209
+ Backtest: An instance of Backtest with the loaded metrics
210
+ and results.
211
+
212
+ Raises:
213
+ OperationalException: If the directory does not exist or if
214
+ there is an error loading the files.
215
+ """
216
+ algorithm_id = None
217
+ backtest_runs = []
218
+ backtest_summary_metrics = None
219
+ permutation_metrics = []
220
+ metadata = {}
221
+ risk_free_rate = None
222
+
223
+ if not os.path.exists(directory_path):
224
+ raise OperationalException(
225
+ f"The directory {directory_path} does not exist."
226
+ )
227
+
228
+ if not os.path.isdir(directory_path):
229
+ raise OperationalException(
230
+ f"Backtest path {directory_path} is not a directory."
231
+ )
232
+
233
+ # Load algorithm_id if available
234
+ id_file = os.path.join(directory_path, "algorithm_id.json")
235
+
236
+ if os.path.isfile(id_file):
237
+ with open(id_file, 'r') as f:
238
+ try:
239
+ algorithm_id = json.load(f).get('algorithm_id', None)
240
+ except json.JSONDecodeError as e:
241
+ logger.error(f"Error decoding algorithm_id JSON: {e}")
242
+ algorithm_id = None
243
+
244
+ # Load all backtest runs
245
+ runs_dir = os.path.join(directory_path, "runs")
246
+
247
+ if os.path.isdir(runs_dir):
248
+ for dir_name in os.listdir(runs_dir):
249
+ run_path = os.path.join(runs_dir, dir_name)
250
+ if os.path.isdir(run_path):
251
+
252
+ if backtest_date_ranges is not None:
253
+ temp_run = BacktestRun.open(run_path)
254
+ match_found = False
255
+
256
+ for date_range in backtest_date_ranges:
257
+ if (
258
+ temp_run.backtest_start_date ==
259
+ date_range.start_date and
260
+ temp_run.backtest_end_date ==
261
+ date_range.end_date
262
+ ):
263
+
264
+ if date_range.name is not None:
265
+ if (
266
+ temp_run.backtest_date_range_name ==
267
+ date_range.name
268
+ ):
269
+ match_found = True
270
+ break
271
+ else:
272
+ match_found = True
273
+ break
274
+
275
+ if not match_found:
276
+ continue
277
+
278
+ backtest_runs.append(BacktestRun.open(run_path))
279
+
280
+ # Load combined backtests summary
281
+ if backtest_date_ranges is not None:
282
+ summary_file = os.path.join(directory_path, "summary.json")
283
+
284
+ if os.path.isfile(summary_file):
285
+ backtest_summary_metrics = \
286
+ BacktestSummaryMetrics.open(summary_file)
287
+ else:
288
+ # Generate new summary from loaded backtest runs
289
+ temp_metrics = []
290
+ for br in backtest_runs:
291
+ if br.backtest_metrics:
292
+ temp_metrics.append(br.backtest_metrics)
293
+
294
+ backtest_summary_metrics = \
295
+ generate_backtest_summary_metrics(temp_metrics)
296
+
297
+ # Load backtest permutation test metrics
298
+ perm_test_dir = os.path.join(directory_path, "permutation_tests")
299
+
300
+ if os.path.isdir(perm_test_dir):
301
+ for dir_name in os.listdir(perm_test_dir):
302
+ perm_test_file = os.path.join(perm_test_dir, dir_name)
303
+ if os.path.isdir(perm_test_file):
304
+ permutation_metrics.append(
305
+ BacktestPermutationTest.open(perm_test_file)
306
+ )
307
+
308
+ # Load metadata if available
309
+ meta_file = os.path.join(directory_path, "metadata.json")
310
+
311
+ if os.path.isfile(meta_file):
312
+ with open(meta_file, 'r') as f:
313
+ metadata = json.load(f)
314
+
315
+ # Load risk-free rate if available
316
+ risk_free_rate_file = os.path.join(
317
+ directory_path, "risk_free_rate.json"
318
+ )
319
+
320
+ if os.path.isfile(risk_free_rate_file):
321
+ with open(risk_free_rate_file, 'r') as f:
322
+ try:
323
+ risk_free_rate = json.load(f).get(
324
+ 'risk_free_rate', None
325
+ )
326
+ except json.JSONDecodeError as e:
327
+ logger.error(f"Error decoding risk-free rate JSON: {e}")
328
+ risk_free_rate = None
329
+
330
+ return Backtest(
331
+ algorithm_id=algorithm_id,
332
+ backtest_runs=backtest_runs,
333
+ backtest_summary=backtest_summary_metrics,
334
+ backtest_permutation_tests=permutation_metrics,
335
+ metadata=metadata,
336
+ risk_free_rate=risk_free_rate
337
+ )
338
+
339
+ def save(
340
+ self,
341
+ directory_path: Union[str, Path],
342
+ backtest_date_ranges: List[BacktestDateRange] = None,
343
+ ) -> None:
344
+ """
345
+ Save the backtest metrics to a file in JSON format. The metrics will
346
+ always be saved in a file named `metrics.json`
347
+
348
+ Args:
349
+ directory_path (str): The directory where the metrics
350
+ file will be saved.
351
+ backtest_date_ranges (List[BacktestDateRange], optional): A list
352
+ of date ranges to filter the backtest runs. If provided, only
353
+ backtest runs matching these date ranges will be saved.
354
+
355
+ Raises:
356
+ OperationalException: If the directory does not exist or if
357
+ there is an error saving the files.
358
+
359
+ Returns:
360
+ None: This method does not return anything, it saves the
361
+ metrics to a file.
362
+ """
363
+ if not os.path.exists(directory_path):
364
+ os.makedirs(directory_path)
365
+
366
+ # Call the save method of all backtest runs
367
+ if self.backtest_runs:
368
+ run_path = os.path.join(directory_path, "runs")
369
+ os.makedirs(run_path, exist_ok=True)
370
+
371
+ if backtest_date_ranges is not None:
372
+ runs = self.get_all_backtest_runs()
373
+ else:
374
+ runs = self.backtest_runs
375
+
376
+ for br in runs:
377
+ dir_name = br.create_directory_name()
378
+ destination_run_path = os.path.join(run_path, dir_name)
379
+ os.makedirs(destination_run_path, exist_ok=True)
380
+ br.save(destination_run_path)
381
+
382
+ # Save combined backtest metrics if available
383
+ if self.backtest_summary:
384
+ summary_file = os.path.join(
385
+ directory_path, "summary.json"
386
+ )
387
+ self.backtest_summary.save(summary_file)
388
+
389
+ if self.backtest_permutation_tests:
390
+ permutation_dir_path = os.path.join(
391
+ directory_path, "permutation_tests"
392
+ )
393
+ os.makedirs(permutation_dir_path, exist_ok=True)
394
+
395
+ for pm in self.backtest_permutation_tests:
396
+ dir_name = pm.create_directory_name()
397
+ pm_path = os.path.join(permutation_dir_path, dir_name)
398
+ pm.save(pm_path)
399
+
400
+ # Save metadata if available
401
+ if self.metadata:
402
+ meta_file = os.path.join(directory_path, "metadata.json")
403
+ with open(meta_file, 'w') as f:
404
+ json.dump(self.metadata, f, indent=4)
405
+
406
+ # Save risk-free rate if available
407
+ if self.risk_free_rate is not None:
408
+ risk_free_rate_file = os.path.join(
409
+ directory_path, "risk_free_rate.json"
410
+ )
411
+ with open(risk_free_rate_file, 'w') as f:
412
+ json.dump(
413
+ {'risk_free_rate': self.risk_free_rate}, f, indent=4
414
+ )
415
+
416
+ # Save strategy IDs if available
417
+ if self.strategy_ids:
418
+ strategy_ids_file = os.path.join(
419
+ directory_path, "strategy_ids.json"
420
+ )
421
+ with open(strategy_ids_file, 'w') as f:
422
+ json.dump({'strategy_ids': self.strategy_ids}, f, indent=4)
423
+
424
+ # Save algorithm ID if available
425
+ if self.algorithm_id is not None:
426
+ algorithm_id_file = os.path.join(
427
+ directory_path, "algorithm_id.json"
428
+ )
429
+ with open(algorithm_id_file, 'w') as f:
430
+ json.dump(
431
+ {'algorithm_id': self.algorithm_id}, f, indent=4
432
+ )
433
+
434
+ # Save the permutation tests if available
435
+ if self.backtest_permutation_tests:
436
+ permutation_tests_path = os.path.join(
437
+ directory_path, "permutation_tests"
438
+ )
439
+ os.makedirs(permutation_tests_path, exist_ok=True)
440
+
441
+ for bpt in self.backtest_permutation_tests:
442
+ dir_name = bpt.create_directory_name()
443
+ bpt_path = os.path.join(permutation_tests_path, dir_name)
444
+ os.makedirs(bpt_path, exist_ok=True)
445
+ bpt.save(bpt_path)
446
+
447
+ def __repr__(self):
448
+ """
449
+ Return a string representation of the Backtest instance.
450
+
451
+ Returns:
452
+ str: A string representation of the Backtest instance.
453
+ """
454
+ return json.dumps(
455
+ self.to_dict(), indent=4, sort_keys=True, default=str
456
+ )
457
+
458
+ def merge(self, other: 'Backtest') -> 'Backtest':
459
+ """
460
+ Function to merge another Backtest instance into this one.
461
+
462
+ Args:
463
+ other (Backtest): The other Backtest instance to merge.
464
+
465
+ Returns:
466
+ Backtest: The merged Backtest instance.
467
+ """
468
+
469
+ merged = Backtest()
470
+ merged.backtest_runs = self.backtest_runs + other.backtest_runs
471
+
472
+ summary = BacktestSummaryMetrics()
473
+
474
+ for bt_run in merged.get_all_backtest_metrics():
475
+ summary.add(bt_run)
476
+
477
+ merged.backtest_summary = summary
478
+ merged.backtest_permutation_tests = \
479
+ self.backtest_permutation_tests + other.backtest_permutation_tests
480
+
481
+ # Merge metadata
482
+ merged.metadata = {**self.metadata, **other.metadata}
483
+
484
+ if self.risk_free_rate is None:
485
+ merged.risk_free_rate = other.risk_free_rate
486
+
487
+ if self.strategy_ids is None:
488
+ merged.strategy_ids = other.strategy_ids
489
+
490
+ if self.algorithm_id is None:
491
+ merged.algorithm_id = other.algorithm_id
492
+
493
+ return merged
494
+
495
+ def get_metadata(self) -> Dict[str, str]:
496
+ """
497
+ Get the metadata of the backtest.
498
+
499
+ Returns:
500
+ Dict[str, str]: A dictionary containing the metadata
501
+ of the backtest.
502
+ """
503
+ return self.metadata
504
+
505
+ def get_backtest_date_ranges(self):
506
+ """
507
+ Get the date ranges for the backtest.
508
+
509
+ Returns:
510
+ List[BacktestDateRange]: A list of BacktestDateRange objects
511
+ representing the date ranges for each backtest run.
512
+ """
513
+ return [
514
+ BacktestDateRange(
515
+ start_date=run.backtest_start_date,
516
+ end_date=run.backtest_end_date,
517
+ name=run.backtest_date_range_name
518
+ )
519
+ for run in self.backtest_runs
520
+ ]
521
+
522
+ def add_permutation_test(
523
+ self, permutation_test: BacktestPermutationTest
524
+ ) -> None:
525
+ """
526
+ Add a permutation test to the backtest.
527
+
528
+ Args:
529
+ permutation_test (BacktestPermutationTest): The permutation test
530
+ to add.
531
+ """
532
+ self.backtest_permutation_tests.append(permutation_test)
533
+
534
+ def __hash__(self):
535
+ if self.algorithm_id is None:
536
+ raise ValueError(
537
+ "Cannot hash Backtest without an algorithm_id value, Please "
538
+ "make sure the Backtest instance has an algorithm_id set."
539
+ )
540
+
541
+ meta_id = self.metadata.get("algorithm_id")
542
+ return hash(meta_id)
543
+
544
+ def __eq__(self, other):
545
+ if not isinstance(other, Backtest):
546
+ return False
547
+
548
+ return self.algorithm_id == other.algorithm_id
@@ -0,0 +1,113 @@
1
+ from datetime import datetime, timezone
2
+ from dateutil.parser import parse
3
+ from logging import getLogger
4
+
5
+ from investing_algorithm_framework.domain.exceptions import \
6
+ OperationalException
7
+
8
+ logger = getLogger("investing_algorithm_framework")
9
+
10
+
11
+ class BacktestDateRange:
12
+ """
13
+ Represents a date range for a backtest. This class
14
+ will check that the start and end dates are valid for a backtest.
15
+
16
+ Attributes:
17
+ _start_date (datetime): The start date of the backtest.
18
+ _end_date (datetime): The end date of the backtest. If not provided,
19
+ it defaults to the current UTC time.
20
+ _name (str): An optional name for the backtest date range.
21
+ """
22
+ def __init__(self, start_date, end_date=None, name=None):
23
+
24
+ if isinstance(start_date, str):
25
+ start_date = parse(start_date)
26
+
27
+ if end_date is not None and isinstance(end_date, str):
28
+ end_date = parse(end_date)
29
+
30
+ if end_date is None:
31
+ self._end_date = datetime.now(tz=timezone.utc)
32
+
33
+ # Check if start_date end end_date are utc datetime objects
34
+ time_zone_info = start_date.tzinfo
35
+
36
+ if time_zone_info is None or time_zone_info is not timezone.utc:
37
+ logger.warning(
38
+ "Start date must be a UTC datetime object. "
39
+ f"Received: {start_date}"
40
+ )
41
+ # Convert to UTC if not already
42
+ start_date = start_date.astimezone(timezone.utc)
43
+
44
+ time_zone_info = end_date.tzinfo
45
+
46
+ if time_zone_info is None or time_zone_info is not timezone.utc:
47
+ logger.warning(
48
+ "End date must be a UTC datetime object. "
49
+ f"Received: {end_date}"
50
+ )
51
+ # Convert to UTC if not already
52
+ end_date = end_date.astimezone(timezone.utc)
53
+
54
+ self._start_date = start_date
55
+ self._end_date = end_date
56
+ self._name = name
57
+
58
+ if end_date < start_date:
59
+ raise ValueError(
60
+ "End date cannot be before start date for a backtest "
61
+ "date range. " +
62
+ f"(start_date: {start_date}, end_date: {end_date})"
63
+ )
64
+
65
+ # Check if the start date is rounded to the nearest hour
66
+ if start_date.minute != 0 or start_date.second != 0 \
67
+ or start_date.microsecond != 0:
68
+ raise OperationalException(
69
+ "Start date must be rounded to the nearest hour. "
70
+ f"Received: {start_date}"
71
+ )
72
+ # Check if the end date is rounded to the nearest hour
73
+ if end_date.minute != 0 or end_date.second != 0 \
74
+ or end_date.microsecond != 0:
75
+ raise OperationalException(
76
+ "End date must be rounded to the nearest hour. "
77
+ f"Received: {end_date}"
78
+ )
79
+
80
+ @property
81
+ def start_date(self):
82
+ return self._start_date
83
+
84
+ @property
85
+ def end_date(self):
86
+ return self._end_date
87
+
88
+ @property
89
+ def name(self):
90
+ return self._name
91
+
92
+ def __eq__(self, other):
93
+ """
94
+ Two BacktestDateRange objects are equal if they have the same
95
+ start and end dates, regardless of their names.
96
+ """
97
+ if not isinstance(other, BacktestDateRange):
98
+ return False
99
+ return (self._start_date == other._start_date and
100
+ self._end_date == other._end_date)
101
+
102
+ def __hash__(self):
103
+ """
104
+ Hash based on start and end dates to make the object hashable
105
+ for use in sets and as dictionary keys.
106
+ """
107
+ return hash((self._start_date, self._end_date))
108
+
109
+ def __repr__(self):
110
+ return f"{self.name}: {self._start_date} - {self._end_date}"
111
+
112
+ def __str__(self):
113
+ return self.__repr__()