investing-algorithm-framework 1.3.1__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 (282) hide show
  1. investing_algorithm_framework/__init__.py +195 -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 +31 -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 +2233 -264
  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/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/action_handlers/__init__.py +6 -3
  37. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
  38. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +2 -1
  39. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
  40. investing_algorithm_framework/app/stateless/exception_handler.py +1 -1
  41. investing_algorithm_framework/app/strategy.py +873 -52
  42. investing_algorithm_framework/app/task.py +5 -3
  43. investing_algorithm_framework/app/web/__init__.py +2 -1
  44. investing_algorithm_framework/app/web/controllers/__init__.py +2 -2
  45. investing_algorithm_framework/app/web/controllers/orders.py +4 -3
  46. investing_algorithm_framework/app/web/controllers/portfolio.py +1 -1
  47. investing_algorithm_framework/app/web/controllers/positions.py +3 -3
  48. investing_algorithm_framework/app/web/create_app.py +4 -2
  49. investing_algorithm_framework/app/web/error_handler.py +1 -1
  50. investing_algorithm_framework/app/web/schemas/order.py +2 -2
  51. investing_algorithm_framework/app/web/schemas/position.py +1 -0
  52. investing_algorithm_framework/cli/__init__.py +0 -0
  53. investing_algorithm_framework/cli/cli.py +231 -0
  54. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
  55. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  56. investing_algorithm_framework/cli/initialize_app.py +603 -0
  57. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  58. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  59. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  60. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  61. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  62. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  63. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  64. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  65. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  66. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  67. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  68. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  69. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  70. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  71. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  72. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  73. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  74. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  75. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  76. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  77. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  78. investing_algorithm_framework/cli/validate_backtest_checkpoints.py +197 -0
  79. investing_algorithm_framework/create_app.py +43 -9
  80. investing_algorithm_framework/dependency_container.py +121 -33
  81. investing_algorithm_framework/domain/__init__.py +109 -22
  82. investing_algorithm_framework/domain/algorithm_id.py +69 -0
  83. investing_algorithm_framework/domain/backtesting/__init__.py +25 -0
  84. investing_algorithm_framework/domain/backtesting/backtest.py +548 -0
  85. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +113 -0
  86. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +241 -0
  87. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +470 -0
  88. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  89. investing_algorithm_framework/domain/backtesting/backtest_run.py +663 -0
  90. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  91. investing_algorithm_framework/domain/backtesting/backtest_utils.py +198 -0
  92. investing_algorithm_framework/domain/backtesting/combine_backtests.py +392 -0
  93. investing_algorithm_framework/domain/config.py +60 -138
  94. investing_algorithm_framework/domain/constants.py +23 -34
  95. investing_algorithm_framework/domain/data_provider.py +334 -0
  96. investing_algorithm_framework/domain/data_structures.py +42 -0
  97. investing_algorithm_framework/domain/decimal_parsing.py +40 -0
  98. investing_algorithm_framework/domain/exceptions.py +51 -1
  99. investing_algorithm_framework/domain/models/__init__.py +29 -14
  100. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  101. investing_algorithm_framework/domain/models/base_model.py +3 -1
  102. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  103. investing_algorithm_framework/domain/models/data/data_source.py +222 -0
  104. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  105. investing_algorithm_framework/domain/models/event.py +35 -0
  106. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  107. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  108. investing_algorithm_framework/domain/models/order/__init__.py +3 -4
  109. investing_algorithm_framework/domain/models/order/order.py +243 -86
  110. investing_algorithm_framework/domain/models/order/order_status.py +2 -2
  111. investing_algorithm_framework/domain/models/order/order_type.py +1 -3
  112. investing_algorithm_framework/domain/models/portfolio/__init__.py +7 -2
  113. investing_algorithm_framework/domain/models/portfolio/portfolio.py +134 -1
  114. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +37 -37
  115. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
  116. investing_algorithm_framework/domain/models/position/__init__.py +3 -2
  117. investing_algorithm_framework/domain/models/position/position.py +29 -0
  118. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  119. investing_algorithm_framework/domain/models/position/{position_cost.py → position_snapshot.py} +16 -8
  120. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  121. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  122. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -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 +94 -98
  126. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  127. investing_algorithm_framework/domain/models/time_unit.py +111 -2
  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 +11 -0
  131. investing_algorithm_framework/domain/models/trade/trade.py +389 -0
  132. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  133. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
  134. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
  135. investing_algorithm_framework/domain/order_executor.py +112 -0
  136. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  137. investing_algorithm_framework/domain/services/__init__.py +11 -0
  138. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  139. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  140. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  141. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  142. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  143. investing_algorithm_framework/domain/strategy.py +1 -29
  144. investing_algorithm_framework/domain/utils/__init__.py +16 -4
  145. investing_algorithm_framework/domain/utils/csv.py +22 -0
  146. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  147. investing_algorithm_framework/domain/utils/dates.py +57 -0
  148. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  149. investing_algorithm_framework/domain/utils/polars.py +53 -0
  150. investing_algorithm_framework/domain/utils/random.py +29 -0
  151. investing_algorithm_framework/download_data.py +244 -0
  152. investing_algorithm_framework/infrastructure/__init__.py +39 -11
  153. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  154. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1152 -0
  155. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  156. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  157. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  158. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +87 -13
  159. investing_algorithm_framework/infrastructure/models/__init__.py +13 -4
  160. investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
  161. investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -2
  162. investing_algorithm_framework/infrastructure/models/order/order.py +73 -73
  163. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  164. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  165. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +3 -2
  166. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
  167. investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +57 -3
  168. investing_algorithm_framework/infrastructure/models/position/__init__.py +2 -2
  169. investing_algorithm_framework/infrastructure/models/position/position.py +16 -11
  170. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
  171. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  172. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  173. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
  174. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
  175. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  176. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  177. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  178. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  179. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  180. investing_algorithm_framework/infrastructure/repositories/__init__.py +13 -5
  181. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  182. investing_algorithm_framework/infrastructure/repositories/order_repository.py +32 -19
  183. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +2 -2
  184. investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
  185. investing_algorithm_framework/infrastructure/repositories/position_repository.py +47 -4
  186. investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
  187. investing_algorithm_framework/infrastructure/repositories/repository.py +85 -31
  188. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  189. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
  190. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
  191. investing_algorithm_framework/infrastructure/services/__init__.py +9 -2
  192. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  193. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +193 -0
  194. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  195. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  196. investing_algorithm_framework/infrastructure/services/backtesting/__init__.py +9 -0
  197. investing_algorithm_framework/infrastructure/services/backtesting/backtest_service.py +2596 -0
  198. investing_algorithm_framework/infrastructure/services/backtesting/event_backtest_service.py +285 -0
  199. investing_algorithm_framework/infrastructure/services/backtesting/vector_backtest_service.py +468 -0
  200. investing_algorithm_framework/services/__init__.py +127 -10
  201. investing_algorithm_framework/services/configuration_service.py +95 -0
  202. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  203. investing_algorithm_framework/services/data_providers/data_provider_service.py +1058 -0
  204. investing_algorithm_framework/services/market_credential_service.py +40 -0
  205. investing_algorithm_framework/services/metrics/__init__.py +119 -0
  206. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  207. investing_algorithm_framework/services/metrics/beta.py +0 -0
  208. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  209. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  210. investing_algorithm_framework/services/metrics/drawdown.py +218 -0
  211. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  212. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  213. investing_algorithm_framework/services/metrics/generate.py +358 -0
  214. investing_algorithm_framework/services/metrics/mean_daily_return.py +84 -0
  215. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  216. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  217. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  218. investing_algorithm_framework/services/metrics/returns.py +452 -0
  219. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  220. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  221. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  222. investing_algorithm_framework/services/metrics/standard_deviation.py +156 -0
  223. investing_algorithm_framework/services/metrics/trades.py +473 -0
  224. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  225. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  226. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  227. investing_algorithm_framework/services/metrics/volatility.py +118 -0
  228. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  229. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  230. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  231. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  232. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  233. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  234. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  235. investing_algorithm_framework/services/{portfolio_configuration_service.py → portfolios/portfolio_configuration_service.py} +27 -12
  236. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  237. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  238. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  239. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  240. investing_algorithm_framework/services/positions/__init__.py +7 -0
  241. investing_algorithm_framework/services/positions/position_service.py +210 -0
  242. investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
  243. investing_algorithm_framework/services/repository_service.py +8 -2
  244. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  245. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +117 -0
  246. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  247. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  248. investing_algorithm_framework/services/trade_service/__init__.py +9 -0
  249. investing_algorithm_framework/services/trade_service/trade_service.py +1099 -0
  250. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  251. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  252. investing_algorithm_framework-7.25.6.dist-info/METADATA +535 -0
  253. investing_algorithm_framework-7.25.6.dist-info/RECORD +268 -0
  254. {investing_algorithm_framework-1.3.1.dist-info → investing_algorithm_framework-7.25.6.dist-info}/WHEEL +1 -2
  255. investing_algorithm_framework-7.25.6.dist-info/entry_points.txt +3 -0
  256. investing_algorithm_framework/app/algorithm.py +0 -410
  257. investing_algorithm_framework/domain/models/market_data/__init__.py +0 -11
  258. investing_algorithm_framework/domain/models/market_data/asset_price.py +0 -50
  259. investing_algorithm_framework/domain/models/market_data/ohlcv.py +0 -76
  260. investing_algorithm_framework/domain/models/market_data/order_book.py +0 -63
  261. investing_algorithm_framework/domain/models/market_data/ticker.py +0 -92
  262. investing_algorithm_framework/domain/models/order/order_fee.py +0 -45
  263. investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
  264. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -205
  265. investing_algorithm_framework/domain/singleton.py +0 -9
  266. investing_algorithm_framework/infrastructure/models/order/order_fee.py +0 -21
  267. investing_algorithm_framework/infrastructure/models/position/position_cost.py +0 -32
  268. investing_algorithm_framework/infrastructure/repositories/order_fee_repository.py +0 -15
  269. investing_algorithm_framework/infrastructure/repositories/position_cost_repository.py +0 -16
  270. investing_algorithm_framework/infrastructure/services/market_service.py +0 -422
  271. investing_algorithm_framework/services/market_data_service.py +0 -75
  272. investing_algorithm_framework/services/order_service.py +0 -464
  273. investing_algorithm_framework/services/portfolio_service.py +0 -105
  274. investing_algorithm_framework/services/position_cost_service.py +0 -5
  275. investing_algorithm_framework/services/position_service.py +0 -50
  276. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -219
  277. investing_algorithm_framework/setup_logging.py +0 -40
  278. investing_algorithm_framework-1.3.1.dist-info/AUTHORS.md +0 -8
  279. investing_algorithm_framework-1.3.1.dist-info/METADATA +0 -172
  280. investing_algorithm_framework-1.3.1.dist-info/RECORD +0 -103
  281. investing_algorithm_framework-1.3.1.dist-info/top_level.txt +0 -1
  282. {investing_algorithm_framework-1.3.1.dist-info → investing_algorithm_framework-7.25.6.dist-info}/LICENSE +0 -0
@@ -0,0 +1,663 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from datetime import datetime, timezone
5
+ from dataclasses import dataclass, field
6
+ from logging import getLogger
7
+ from typing import Union, List, Optional, Dict
8
+
9
+ from investing_algorithm_framework.domain.exceptions \
10
+ import OperationalException
11
+ from investing_algorithm_framework.domain.models.order import Order, \
12
+ OrderSide, OrderStatus
13
+ from investing_algorithm_framework.domain.models.position import Position
14
+ from investing_algorithm_framework.domain.models.trade import Trade
15
+ from investing_algorithm_framework.domain.models.portfolio import \
16
+ PortfolioSnapshot
17
+ from investing_algorithm_framework.domain.models.trade.trade_status import \
18
+ TradeStatus
19
+ from investing_algorithm_framework.domain.models.trade.trade_stop_loss import \
20
+ TradeStopLoss
21
+ from investing_algorithm_framework.domain.models.trade.trade_take_profit \
22
+ import TradeTakeProfit
23
+
24
+
25
+ from .backtest_metrics import BacktestMetrics
26
+
27
+
28
+ logger = getLogger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class BacktestRun:
33
+ """
34
+ Represents a backtest of an algorithm. It contains the backtest metrics,
35
+ backtest results, and paths to strategy and data files.
36
+
37
+ Attributes:
38
+ backtest_metrics (Optional[List[BacktestMetrics]]): A list of
39
+ backtest metrics objects, each representing the performance
40
+ metrics of a single backtest run.
41
+ backtest_start_date (datetime): The start date of the backtest.
42
+ backtest_end_date (datetime): The end date of the backtest.
43
+ backtest_date_range_name (str): The name of the date range used for
44
+ the backtest.
45
+ trading_symbol (str): The trading symbol used in the backtest.
46
+ initial_unallocated (float): The initial unallocated amount in the
47
+ backtest.
48
+ number_of_runs (int): The number of runs in the backtest.
49
+ portfolio_snapshots (List[PortfolioSnapshot]): A list of portfolio
50
+ snapshots taken during the backtest.
51
+ trades (List[Trade]): A list of trades executed during the backtest.
52
+ orders (List[Order]): A list of orders placed during the backtest.
53
+ positions (List[Position]): A list of positions held during the
54
+ backtest.
55
+ created_at (datetime): The date and time when the backtest was created.
56
+ symbols (List[str]): A list of trading symbols involved in
57
+ the backtest.
58
+ number_of_days (int): The total number of days the backtest ran.
59
+ number_of_trades (int): The total number of trades executed during
60
+ the backtest.
61
+ number_of_trades_closed (int): The total number of trades that were
62
+ closed during the backtest.
63
+ number_of_trades_open (int): The total number of trades that are
64
+ still open at the end of the backtest.
65
+ number_of_orders (int): The total number of orders placed during
66
+ the backtest.
67
+ number_of_positions (int): The total number of positions held
68
+ during the backtest.
69
+ """
70
+ backtest_start_date: datetime
71
+ backtest_end_date: datetime
72
+ trading_symbol: str
73
+ initial_unallocated: float = 0.0
74
+ number_of_runs: int = 0
75
+ portfolio_snapshots: List[PortfolioSnapshot] = field(default_factory=list)
76
+ trades: List[Trade] = field(default_factory=list)
77
+ orders: List[Order] = field(default_factory=list)
78
+ positions: List[Position] = field(default_factory=list)
79
+ created_at: datetime = None,
80
+ symbols: List[str] = field(default_factory=list)
81
+ number_of_days: int = 0
82
+ number_of_trades: int = 0
83
+ number_of_trades_closed: int = 0
84
+ number_of_trades_open: int = 0
85
+ number_of_orders: int = 0
86
+ number_of_positions: int = 0
87
+ backtest_metrics: BacktestMetrics = None
88
+ backtest_date_range_name: str = None
89
+ data_sources: List[Dict] = field(default_factory=list)
90
+ metadata: Dict[str, str] = field(default_factory=dict)
91
+
92
+ def to_dict(self) -> dict:
93
+ """
94
+ Convert the Backtest instance to a dictionary with all
95
+ date/datetime fields as ISO strings (always UTC).
96
+ """
97
+ def ensure_iso(value):
98
+ if hasattr(value, "isoformat"):
99
+ if value.tzinfo is None:
100
+ value = value.replace(tzinfo=timezone.utc)
101
+ return value.isoformat()
102
+ return value
103
+
104
+ backtest_metrics = self.backtest_metrics.to_dict() \
105
+ if self.backtest_metrics else None
106
+ return {
107
+ "backtest_metrics": backtest_metrics,
108
+ "backtest_start_date": ensure_iso(self.backtest_start_date),
109
+ "backtest_date_range_name": self.backtest_date_range_name,
110
+ "backtest_end_date": ensure_iso(self.backtest_end_date),
111
+ "trading_symbol": self.trading_symbol,
112
+ "initial_unallocated": self.initial_unallocated,
113
+ "number_of_runs": self.number_of_runs,
114
+ "portfolio_snapshots": [
115
+ ps.to_dict() for ps in self.portfolio_snapshots
116
+ ],
117
+ "trades": [trade.to_dict() for trade in self.trades],
118
+ "orders": [order.to_dict() for order in self.orders],
119
+ "positions": [position.to_dict() for position in self.positions],
120
+ "created_at": ensure_iso(self.created_at),
121
+ "symbols": self.symbols,
122
+ "number_of_days": self.number_of_days,
123
+ "number_of_trades": self.number_of_trades,
124
+ "number_of_trades_closed": self.number_of_trades_closed,
125
+ "number_of_trades_open": self.number_of_trades_open,
126
+ "number_of_orders": self.number_of_orders,
127
+ "number_of_positions": self.number_of_positions,
128
+ "metadata": self.metadata,
129
+ }
130
+
131
+ @staticmethod
132
+ def open(directory_path: Union[str, Path]) -> 'BacktestRun':
133
+ """
134
+ Open a backtest report from a directory and return a Backtest instance.
135
+
136
+ Args:
137
+ directory_path (str): The path to the directory containing the
138
+ backtest report files.
139
+
140
+ Returns:
141
+ Backtest: An instance of Backtest with the loaded metrics
142
+ and results.
143
+
144
+ Raises:
145
+ OperationalException: If the directory does not exist or if
146
+ there is an error loading the files.
147
+ """
148
+ backtest_metrics = None
149
+
150
+ if not os.path.exists(directory_path):
151
+ raise OperationalException(
152
+ f"The directory {directory_path} does not exist."
153
+ )
154
+
155
+ # Load combined backtest metrics
156
+ metrics_file = os.path.join(directory_path, "metrics.json")
157
+
158
+ if os.path.isfile(metrics_file):
159
+ backtest_metrics = BacktestMetrics.open(metrics_file)
160
+
161
+ # Load backtest results
162
+ run_file = os.path.join(directory_path, "run.json")
163
+
164
+ if os.path.isfile(run_file):
165
+ with open(run_file, 'r') as f:
166
+ data = json.load(f)
167
+ else:
168
+ raise OperationalException(
169
+ f"The run file {run_file} does not exist."
170
+ )
171
+
172
+ # Validate and set defaults for required fields
173
+ required_fields = {
174
+ "backtest_start_date": "2020-01-01 00:00:00",
175
+ "backtest_end_date": "2020-01-02 00:00:00",
176
+ "created_at": "2020-01-01 00:00:00",
177
+ "trading_symbol": "USD",
178
+ "initial_unallocated": 1000.0,
179
+ "number_of_runs": 1
180
+ }
181
+
182
+ for field_name, default_value in required_fields.items():
183
+ if field_name not in data:
184
+ logger.warning(f"Missing required field '{field_name}' in "
185
+ f"backtest data, using "
186
+ f"default: {default_value}")
187
+ data[field_name] = default_value
188
+
189
+ # Parse datetime fields
190
+ data["backtest_start_date"] = datetime.strptime(
191
+ data["backtest_start_date"], "%Y-%m-%d %H:%M:%S"
192
+ )
193
+ data["backtest_end_date"] = datetime.strptime(
194
+ data["backtest_end_date"], "%Y-%m-%d %H:%M:%S"
195
+ )
196
+ data["created_at"] = datetime.strptime(
197
+ data["created_at"], "%Y-%m-%d %H:%M:%S"
198
+ )
199
+ # Convert all to utc timezone
200
+ data["backtest_start_date"] = data[
201
+ "backtest_start_date"].replace(tzinfo=timezone.utc)
202
+ data["backtest_end_date"] = data[
203
+ "backtest_end_date"].replace(tzinfo=timezone.utc)
204
+ data["created_at"] = data["created_at"].replace(tzinfo=timezone.utc)
205
+
206
+ # Parse orders with error handling
207
+ orders = []
208
+ for order_data in data.get("orders", []):
209
+ try:
210
+ order = Order.from_dict(order_data)
211
+ orders.append(order)
212
+ except KeyError as e:
213
+ logger.error(f"Failed to parse order "
214
+ f"data, missing field {e}: {order_data}")
215
+ continue
216
+ except Exception as e:
217
+ logger.error(f"Failed to parse order data: {e}")
218
+ continue
219
+ data["orders"] = orders
220
+
221
+ # Parse positions with error handling
222
+ positions = []
223
+ for position_data in data.get("positions", []):
224
+ try:
225
+ position = Position.from_dict(position_data)
226
+ positions.append(position)
227
+ except KeyError as e:
228
+ logger.error(f"Failed to parse position data, "
229
+ f"missing field {e}: {position_data}")
230
+ continue
231
+ except Exception as e:
232
+ logger.error(f"Failed to parse position data: {e}")
233
+ continue
234
+ data["positions"] = positions
235
+
236
+ # Parse trades with error handling
237
+ trades = []
238
+ for trade_data in data.get("trades", []):
239
+ try:
240
+ trade = Trade.from_dict(trade_data)
241
+ trades.append(trade)
242
+ except KeyError as e:
243
+ logger.error(f"Failed to parse trade data, "
244
+ f"missing field {e}: {trade_data}")
245
+ # Skip this trade and continue with the next one
246
+ continue
247
+ except Exception as e:
248
+ logger.error(f"Failed to parse trade data: {e}")
249
+ continue
250
+ data["trades"] = trades
251
+
252
+ # Parse portfolio snapshots with error handling
253
+ portfolio_snapshots = []
254
+ for ps_data in data.get("portfolio_snapshots", []):
255
+ try:
256
+ ps = PortfolioSnapshot.from_dict(ps_data)
257
+ portfolio_snapshots.append(ps)
258
+ except KeyError as e:
259
+ logger.error(f"Failed to parse portfolio snapshot data, "
260
+ f"missing field {e}: {ps_data}")
261
+ continue
262
+ except Exception as e:
263
+ logger.error(f"Failed to parse portfolio snapshot data: {e}")
264
+ continue
265
+ data["portfolio_snapshots"] = portfolio_snapshots
266
+
267
+ return BacktestRun(
268
+ backtest_metrics=backtest_metrics,
269
+ **data
270
+ )
271
+
272
+ def create_directory_name(self) -> str:
273
+ """
274
+ Create a directory name for the backtest run based on its attributes.
275
+
276
+ Returns:
277
+ str: A string representing the directory name.
278
+ """
279
+ start_str = self.backtest_start_date.strftime("%Y%m%d")
280
+ end_str = self.backtest_end_date.strftime("%Y%m%d")
281
+ dir_name = f"backtest_{self.trading_symbol}_{start_str}_{end_str}"
282
+ return dir_name
283
+
284
+ def save(self, directory_path: Union[str, Path]) -> None:
285
+ """
286
+ Save the backtest run to a directory.
287
+
288
+ Args:
289
+ directory_path (str): The directory where the metrics
290
+ file will be saved.
291
+
292
+ Raises:
293
+ OperationalException: If the directory does not exist or if
294
+ there is an error saving the files.
295
+
296
+ Returns:
297
+ None: This method does not return anything, it saves the
298
+ metrics to a file.
299
+ """
300
+
301
+ metrics_path = os.path.join(directory_path, "metrics.json")
302
+ run_path = os.path.join(directory_path, "run.json")
303
+
304
+ if not os.path.exists(directory_path):
305
+ os.makedirs(directory_path)
306
+
307
+ # Call the save method of BacktestMetrics
308
+ if self.backtest_metrics:
309
+ self.backtest_metrics.save(metrics_path)
310
+
311
+ # Save the run data
312
+ with open(run_path, 'w') as f:
313
+ # string format datetime objects
314
+ data = self.to_dict()
315
+
316
+ # Remove backtest_metrics to avoid redundancy
317
+ data.pop("backtest_metrics", None)
318
+
319
+ # Ensure datetime objects are in UTC before formatting
320
+ backtest_start_date = self.backtest_start_date
321
+
322
+ if backtest_start_date.tzinfo is None:
323
+ # Naive datetime - treat as UTC
324
+ backtest_start_date = backtest_start_date.replace(
325
+ tzinfo=timezone.utc
326
+ )
327
+ else:
328
+ # Timezone-aware - convert to UTC
329
+ backtest_start_date = backtest_start_date.astimezone(
330
+ timezone.utc
331
+ )
332
+
333
+ backtest_end_date = self.backtest_end_date
334
+ if backtest_end_date.tzinfo is None:
335
+ backtest_end_date = backtest_end_date.replace(
336
+ tzinfo=timezone.utc
337
+ )
338
+ else:
339
+ backtest_end_date = backtest_end_date.astimezone(timezone.utc)
340
+
341
+ created_at = self.created_at
342
+
343
+ if created_at.tzinfo is None:
344
+ created_at = created_at.replace(tzinfo=timezone.utc)
345
+ else:
346
+ created_at = created_at.astimezone(timezone.utc)
347
+
348
+ data["backtest_start_date"] = backtest_start_date.strftime(
349
+ "%Y-%m-%d %H:%M:%S"
350
+ )
351
+ data["backtest_end_date"] = backtest_end_date.strftime(
352
+ "%Y-%m-%d %H:%M:%S"
353
+ )
354
+ data["created_at"] = created_at.strftime(
355
+ "%Y-%m-%d %H:%M:%S"
356
+ )
357
+ json.dump(data, f, default=str)
358
+
359
+ def get_trade(self, trade_id: str) -> Optional[Trade]:
360
+ """
361
+ Get a trade by its ID from the backtest report
362
+
363
+ Args:
364
+ trade_id (str): The trade ID
365
+
366
+ Returns:
367
+ Trade: The trade with the given ID, or None if not found
368
+ """
369
+ for trade in self.trades:
370
+ if trade.trade_id == trade_id:
371
+ return trade
372
+
373
+ return None
374
+
375
+ def get_trades(
376
+ self,
377
+ target_symbol: str = None,
378
+ trade_status: Union[TradeStatus, str] = None,
379
+ opened_at: datetime = None,
380
+ opened_at_lt: datetime = None,
381
+ opened_at_lte: datetime = None,
382
+ opened_at_gt: datetime = None,
383
+ opened_at_gte: datetime = None,
384
+ order_id: str = None
385
+ ) -> List[Trade]:
386
+ """
387
+ Get the trades of a backtest report
388
+
389
+ Args:
390
+ target_symbol (str): The target_symbol
391
+ trade_status (Union[TradeStatus, str]): The trade status
392
+ opened_at (datetime): The created_at date to filter the trades
393
+ opened_at_lt (datetime): The created_at date to filter the trades
394
+ opened_at_lte (datetime): The created_at date to filter the trades
395
+ opened_at_gt (datetime): The created_at date to filter the trades
396
+ opened_at_gte (datetime): The created_at date to filter the trades
397
+ order_id (str): The order ID to filter the trades
398
+
399
+ Returns:
400
+ list: The trades of the backtest report
401
+ """
402
+ selection = self.trades
403
+
404
+ if target_symbol is not None:
405
+ selection = [
406
+ trade for trade in selection
407
+ if trade.target_symbol.lower() == target_symbol.lower()
408
+ ]
409
+
410
+ if trade_status is not None:
411
+ trade_status = TradeStatus.from_value(trade_status)
412
+ selection = [
413
+ trade for trade in selection
414
+ if trade.status == trade_status.value
415
+ ]
416
+
417
+ if opened_at is not None:
418
+ selection = [
419
+ trade for trade in selection
420
+ if trade.opened_at == opened_at
421
+ ]
422
+
423
+ if opened_at_lt is not None:
424
+ selection = [
425
+ trade for trade in selection
426
+ if trade.opened_at < opened_at_lt
427
+ ]
428
+
429
+ if opened_at_lte is not None:
430
+ selection = [
431
+ trade for trade in selection
432
+ if trade.opened_at <= opened_at_lte
433
+ ]
434
+
435
+ if opened_at_gt is not None:
436
+ selection = [
437
+ trade for trade in selection
438
+ if trade.opened_at > opened_at_gt
439
+ ]
440
+
441
+ if opened_at_gte is not None:
442
+ selection = [
443
+ trade for trade in selection
444
+ if trade.opened_at >= opened_at_gte
445
+ ]
446
+
447
+ if order_id is not None:
448
+ new_selection = []
449
+ for trade in selection:
450
+
451
+ for order in trade.orders:
452
+ if order.order_id == order_id:
453
+ new_selection.append(trade)
454
+ break
455
+
456
+ selection = new_selection
457
+
458
+ return selection
459
+
460
+ def get_stop_losses(
461
+ self,
462
+ trade_id: str = None,
463
+ triggered: bool = None
464
+ ) -> List[TradeStopLoss]:
465
+ """
466
+ Get the stop losses of the backtest report
467
+
468
+ Args:
469
+ trade_id (str): The trade ID to filter the stop losses
470
+ triggered (bool): Whether to filter by triggered stop losses
471
+
472
+ Returns:
473
+ list: The stop losses of the backtest report
474
+ """
475
+ stop_losses = []
476
+
477
+ for trade in self.trades:
478
+ if trade_id is not None and trade.id != trade_id:
479
+ continue
480
+
481
+ for sl in trade.stop_losses:
482
+ if isinstance(sl, TradeStopLoss):
483
+ if triggered is not None:
484
+ if sl.triggered == triggered:
485
+ stop_losses.append(sl)
486
+ else:
487
+ stop_losses.append(sl)
488
+
489
+ return stop_losses
490
+
491
+ def get_take_profits(
492
+ self,
493
+ trade_id: str = None,
494
+ triggered: bool = None
495
+ ) -> List[TradeStopLoss]:
496
+ """
497
+ Get the take profits of the backtest report
498
+
499
+ Args:
500
+ trade_id (str): The trade ID to filter the take profits
501
+ triggered (bool): Whether to filter by triggered take profits
502
+
503
+ Returns:
504
+ list: The take profits of the backtest report
505
+ """
506
+ take_profits = []
507
+
508
+ for trade in self.trades:
509
+ if trade_id is not None and trade.id != trade_id:
510
+ continue
511
+
512
+ for tp in trade.take_profits:
513
+ if isinstance(tp, TradeTakeProfit):
514
+ if triggered is not None:
515
+ if tp.triggered == triggered:
516
+ take_profits.append(tp)
517
+ else:
518
+ take_profits.append(tp)
519
+
520
+ return take_profits
521
+
522
+ def get_portfolio_snapshots(
523
+ self,
524
+ created_at_lt: Optional[datetime] = None,
525
+ created_at_lte: Optional[datetime] = None,
526
+ created_at_gt: Optional[datetime] = None,
527
+ created_at_gte: Optional[datetime] = None
528
+ ) -> List[PortfolioSnapshot]:
529
+ """
530
+ Get the portfolio snapshots of the backtest report
531
+
532
+ Args:
533
+ created_at_lt (datetime): The created_at date to filter
534
+ the snapshots
535
+ created_at_lte (datetime): The created_at date to filter
536
+ the snapshots
537
+ created_at_gt (datetime): The created_at date to filter
538
+ the snapshots
539
+ created_at_gte (datetime): The created_at date to filter
540
+ the snapshots
541
+
542
+ Returns:
543
+ list: The portfolio snapshots of the backtest report
544
+ """
545
+ selection = self.portfolio_snapshots
546
+
547
+ if created_at_lt is not None:
548
+ selection = [
549
+ snapshot for snapshot in selection
550
+ if snapshot.created_at < created_at_lt
551
+ ]
552
+
553
+ if created_at_lte is not None:
554
+ selection = [
555
+ snapshot for snapshot in selection
556
+ if snapshot.created_at <= created_at_lte
557
+ ]
558
+
559
+ if created_at_gt is not None:
560
+ selection = [
561
+ snapshot for snapshot in selection
562
+ if snapshot.created_at > created_at_gt
563
+ ]
564
+
565
+ if created_at_gte is not None:
566
+ selection = [
567
+ snapshot for snapshot in selection
568
+ if snapshot.created_at >= created_at_gte
569
+ ]
570
+
571
+ return selection
572
+
573
+ def get_orders(
574
+ self,
575
+ target_symbol: str = None,
576
+ order_side: str = None,
577
+ order_status: Union[OrderStatus, str] = None,
578
+ created_at: datetime = None,
579
+ created_at_lt: datetime = None,
580
+ created_at_lte: datetime = None,
581
+ created_at_gt: datetime = None,
582
+ created_at_gte: datetime = None
583
+ ) -> List[Order]:
584
+ """
585
+ Get the orders of a backtest report
586
+
587
+ Args:
588
+ target_symbol (str): The target_symbol
589
+ order_side (str): The order side
590
+ order_status (Union[OrderStatus, str]): The order status
591
+ created_at (datetime): The created_at date to filter the orders
592
+ created_at_lt (datetime): The created_at date to filter the orders
593
+ created_at_lte (datetime): The created_at date to filter the orders
594
+ created_at_gt (datetime): The created_at date to filter the orders
595
+ created_at_gte (datetime): The created_at date to filter the orders
596
+
597
+ Returns:
598
+ list: The orders of the backtest report
599
+ """
600
+ selection = self.orders
601
+
602
+ if created_at is not None:
603
+ selection = [
604
+ order for order in selection
605
+ if order.created_at == created_at
606
+ ]
607
+
608
+ if created_at_lt is not None:
609
+ selection = [
610
+ order for order in selection
611
+ if order.created_at < created_at_lt
612
+ ]
613
+
614
+ if created_at_lte is not None:
615
+ selection = [
616
+ order for order in selection
617
+ if order.created_at <= created_at_lte
618
+ ]
619
+
620
+ if created_at_gt is not None:
621
+ selection = [
622
+ order for order in selection
623
+ if order.created_at > created_at_gt
624
+ ]
625
+
626
+ if created_at_gte is not None:
627
+ selection = [
628
+ order for order in selection
629
+ if order.created_at >= created_at_gte
630
+ ]
631
+
632
+ if target_symbol is not None:
633
+ selection = [
634
+ order for order in selection
635
+ if order.target_symbol == target_symbol
636
+ ]
637
+
638
+ if order_side is not None:
639
+ order_side = OrderSide.from_value(order_side)
640
+ selection = [
641
+ order for order in selection
642
+ if order.order_side == order_side.value
643
+ ]
644
+
645
+ if order_status is not None:
646
+ status = OrderStatus.from_value(order_status)
647
+ selection = [
648
+ order for order in selection
649
+ if order.status == status.value
650
+ ]
651
+
652
+ return selection
653
+
654
+ def __repr__(self):
655
+ """
656
+ Return a string representation of the Backtest instance.
657
+
658
+ Returns:
659
+ str: A string representation of the Backtest instance.
660
+ """
661
+ return json.dumps(
662
+ self.to_dict(), indent=4, sort_keys=True, default=str
663
+ )