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,651 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from collections import defaultdict
5
+ from datetime import datetime, timedelta, timezone
6
+ from typing import Dict, List, Union
7
+ from uuid import uuid4
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ import polars as pl
12
+
13
+ from investing_algorithm_framework.domain import BacktestRun, OrderType, \
14
+ TimeUnit, Trade, OperationalException, BacktestDateRange, TimeFrame, \
15
+ Backtest, TradeStatus, PortfolioSnapshot, Order, OrderStatus, OrderSide, \
16
+ Portfolio, DataType, generate_backtest_summary_metrics, \
17
+ PortfolioConfiguration
18
+ from investing_algorithm_framework.services.data_providers import \
19
+ DataProviderService
20
+ from investing_algorithm_framework.services.portfolios import \
21
+ PortfolioConfigurationService
22
+ from investing_algorithm_framework.services.metrics import \
23
+ create_backtest_metrics
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class BacktestService:
29
+ """
30
+ Service that facilitates backtests for algorithm objects.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ data_provider_service: DataProviderService,
36
+ order_service,
37
+ portfolio_service,
38
+ portfolio_snapshot_service,
39
+ position_repository,
40
+ trade_service,
41
+ configuration_service,
42
+ portfolio_configuration_service,
43
+ ):
44
+ super().__init__()
45
+ self._order_service = order_service
46
+ self._trade_service = trade_service
47
+ self._portfolio_service = portfolio_service
48
+ self._portfolio_snapshot_service = portfolio_snapshot_service
49
+ self._position_repository = position_repository
50
+ self._configuration_service = configuration_service
51
+ self._portfolio_configuration_service: PortfolioConfigurationService \
52
+ = portfolio_configuration_service
53
+ self._data_provider_service = data_provider_service
54
+
55
+ def validate_strategy_for_vector_backtest(self, strategy):
56
+ """
57
+ Validate if the strategy is suitable for backtesting.
58
+
59
+ Args:
60
+ strategy: The strategy to validate.
61
+
62
+ Raises:
63
+ OperationalException: If the strategy does not have the required
64
+ buy/sell signal functions.
65
+ """
66
+ if not hasattr(strategy, 'generate_buy_signals'):
67
+ raise OperationalException(
68
+ "Strategy must define a vectorized buy signal function "
69
+ "(buy_signal_vectorized)."
70
+ )
71
+ if not hasattr(strategy, 'generate_sell_signals'):
72
+ raise OperationalException(
73
+ "Strategy must define a vectorized sell signal function "
74
+ "(sell_signal_vectorized)."
75
+ )
76
+
77
+ def _get_data_frame_index(self, data: Union[pl.DataFrame, pd.DataFrame]):
78
+ """
79
+ Function to return the index for a given df. If the provided
80
+ data is of type pandas Dataframe, first will be checked if
81
+ it has a index. If this is not the case the function will
82
+ check if there is a 'DateTime' column and add this
83
+ as the index.
84
+
85
+ For a polars DataFrame, the 'DateTime' column will be
86
+ used as the index if it exists.
87
+
88
+ If no index is found an exception will be raised.
89
+
90
+ Args:
91
+ data: The data frame to process.
92
+
93
+ Raises:
94
+ OperationalException: If no valid index is found.
95
+
96
+ Returns:
97
+ The index of the data frame.
98
+ """
99
+ if isinstance(data, pl.DataFrame):
100
+ if "Datetime" in data.columns:
101
+ return data["Datetime"]
102
+ else:
103
+ raise OperationalException("No valid index found.")
104
+ elif isinstance(data, pd.DataFrame):
105
+ if data.index is not None:
106
+ return data.index
107
+ elif "Datetime" in data.columns:
108
+ return data["Datetime"]
109
+ else:
110
+ raise OperationalException("No valid index found.")
111
+ else:
112
+ raise ValueError("Unsupported data frame type.")
113
+
114
+ def create_vector_backtest(
115
+ self,
116
+ strategy,
117
+ backtest_date_range: BacktestDateRange,
118
+ risk_free_rate: float = 0.027,
119
+ initial_amount: float = None,
120
+ trading_symbol: str = None,
121
+ market: str = None,
122
+ ) -> BacktestRun:
123
+ """
124
+ Vectorized backtest for multiple assets using strategy
125
+ buy/sell signals.
126
+
127
+ Args:
128
+ strategy: The strategy to backtest.
129
+ backtest_date_range: The date range for the backtest.
130
+ risk_free_rate: The risk-free rate to use for the backtest
131
+ metrics. Default is 0.027 (2.7%).
132
+ initial_amount: The initial amount to use for the backtest.
133
+ If None, the initial amount will be taken from the first
134
+ portfolio configuration.
135
+ trading_symbol: The trading symbol to use for the backtest.
136
+ If None, the trading symbol will be taken from the first
137
+ portfolio configuration.
138
+ market: The market to use for the backtest. If None, the market
139
+ will be taken from the first portfolio configuration.
140
+
141
+ Returns:
142
+ BacktestRun: The backtest run containing the results and metrics.
143
+ """
144
+ portfolio_configurations = self._portfolio_configuration_service\
145
+ .get_all()
146
+
147
+ if (
148
+ portfolio_configurations is None
149
+ or len(portfolio_configurations) == 0
150
+ ) and (
151
+ initial_amount is None
152
+ or trading_symbol is None
153
+ or market is None
154
+ ):
155
+ raise OperationalException(
156
+ "No initial amount, trading symbol or market provided "
157
+ "for the backtest and no portfolio configurations found. "
158
+ "please register a portfolio configuration "
159
+ "or specify the initial amount, trading symbol and "
160
+ "market parameters before running a backtest."
161
+ )
162
+
163
+ if portfolio_configurations is None \
164
+ or len(portfolio_configurations) == 0:
165
+ portfolio_configurations = []
166
+ portfolio_configurations.append(
167
+ PortfolioConfiguration(
168
+ identifier="vector_backtest",
169
+ market=market,
170
+ trading_symbol=trading_symbol,
171
+ initial_balance=initial_amount
172
+ )
173
+ )
174
+
175
+ portfolio_configuration = portfolio_configurations[0]
176
+
177
+ trading_symbol = portfolio_configurations[0].trading_symbol
178
+ portfolio = Portfolio.from_portfolio_configuration(
179
+ portfolio_configuration
180
+ )
181
+
182
+ # Load vectorized backtest data
183
+ data = self._data_provider_service.get_vectorized_backtest_data(
184
+ data_sources=strategy.data_sources,
185
+ start_date=backtest_date_range.start_date,
186
+ end_date=backtest_date_range.end_date
187
+ )
188
+
189
+ # Compute signals from strategy
190
+ buy_signals = strategy.generate_buy_signals(data)
191
+ sell_signals = strategy.generate_sell_signals(data)
192
+
193
+ # Build master index (union of all indices in signal dict)
194
+ index = pd.Index([])
195
+
196
+ most_granular_ohlcv_data_source = \
197
+ BacktestService.get_most_granular_ohlcv_data_source(
198
+ strategy.data_sources
199
+ )
200
+ most_granular_ohlcv_data = self._data_provider_service.get_ohlcv_data(
201
+ symbol=most_granular_ohlcv_data_source.symbol,
202
+ start_date=backtest_date_range.start_date,
203
+ end_date=backtest_date_range.end_date,
204
+ pandas=True
205
+ )
206
+
207
+ # Make sure to filter out the buy and sell signals that are before
208
+ # the backtest start date
209
+ buy_signals = {k: v[v.index >= backtest_date_range.start_date]
210
+ for k, v in buy_signals.items()}
211
+ sell_signals = {k: v[v.index >= backtest_date_range.start_date]
212
+ for k, v in sell_signals.items()}
213
+
214
+ index = index.union(most_granular_ohlcv_data.index)
215
+ index = index.sort_values()
216
+
217
+ # Initialize trades and portfolio values
218
+ trades = []
219
+ orders = []
220
+ granular_ohlcv_data_order_by_symbol = {}
221
+ snapshots = [
222
+ PortfolioSnapshot(
223
+ trading_symbol=trading_symbol,
224
+ portfolio_id=portfolio.identifier,
225
+ created_at=backtest_date_range.start_date,
226
+ unallocated=initial_amount,
227
+ total_value=initial_amount,
228
+ total_net_gain=0.0
229
+ )
230
+ ]
231
+
232
+ for symbol in buy_signals.keys():
233
+ full_symbol = f"{symbol}/{trading_symbol}"
234
+ # find PositionSize object
235
+ pos_size_obj = next(
236
+ (p for p in strategy.position_sizes if
237
+ p.symbol == symbol), None
238
+ )
239
+ # Load most granular OHLCV data for the symbol
240
+ df = self._data_provider_service.get_ohlcv_data(
241
+ symbol=full_symbol,
242
+ start_date=backtest_date_range.start_date,
243
+ end_date=backtest_date_range.end_date,
244
+ pandas=True
245
+ )
246
+ granular_ohlcv_data_order_by_symbol[full_symbol] = df
247
+
248
+ # Align signals with most granular OHLCV data
249
+ close = df["Close"]
250
+ buy_signal = buy_signals[symbol].reindex(index, fill_value=False)
251
+ sell_signal = sell_signals[symbol].reindex(index, fill_value=False)
252
+
253
+ signal = pd.Series(0, index=index)
254
+ signal[buy_signal] = 1
255
+ signal[sell_signal] = -1
256
+ signal = signal.replace(0, np.nan).ffill().shift(1).fillna(0)
257
+ signal = signal.astype(float)
258
+
259
+ if pos_size_obj is None:
260
+ raise OperationalException(
261
+ f"No position size object defined "
262
+ f"for symbol {symbol}, please make sure to "
263
+ f"register a PositionSize object in the strategy."
264
+ )
265
+
266
+ capital_for_trade = pos_size_obj.get_size(
267
+ Portfolio(
268
+ unallocated=initial_amount,
269
+ initial_balance=initial_amount,
270
+ trading_symbol=trading_symbol,
271
+ net_size=0,
272
+ market="BACKTEST",
273
+ identifier="vector_backtest"
274
+ ) if pos_size_obj else (initial_amount / len(buy_signals)),
275
+ asset_price=close.iloc[0]
276
+ )
277
+
278
+ # Trade generation
279
+ last_trade = None
280
+
281
+ # Align signals with most granular OHLCV data
282
+ close = df["Close"].reindex(index, method='ffill')
283
+ buy_signal = buy_signals[symbol].reindex(index, fill_value=False)
284
+ sell_signal = sell_signals[symbol].reindex(index, fill_value=False)
285
+
286
+ # Loop over all timestamps in the backtest
287
+ for i in range(len(index)):
288
+
289
+ # 1 = buy, -1 = sell, 0 = hold
290
+ current_signal = signal.iloc[i]
291
+ current_price = float(close.iloc[i])
292
+ current_date = index[i]
293
+
294
+ # Convert the pd.Timestamp to an utc datetime object
295
+ if isinstance(current_date, pd.Timestamp):
296
+ current_date = current_date.to_pydatetime()
297
+
298
+ if current_date.tzinfo is None:
299
+ current_date = current_date.replace(tzinfo=timezone.utc)
300
+
301
+ # If we are not in a position, and we get a buy signal
302
+ if current_signal == 1 and last_trade is None:
303
+ amount = float(capital_for_trade / current_price)
304
+ buy_order = Order(
305
+ id=uuid4(),
306
+ target_symbol=symbol,
307
+ trading_symbol=trading_symbol,
308
+ order_type=OrderType.LIMIT,
309
+ price=current_price,
310
+ amount=amount,
311
+ status=OrderStatus.CLOSED,
312
+ created_at=current_date,
313
+ updated_at=current_date,
314
+ order_side=OrderSide.BUY
315
+ )
316
+ orders.append(buy_order)
317
+ trade = Trade(
318
+ id=uuid4(),
319
+ orders=[buy_order],
320
+ target_symbol=symbol,
321
+ trading_symbol=trading_symbol,
322
+ available_amount=amount,
323
+ remaining=0,
324
+ filled_amount=amount,
325
+ open_price=current_price,
326
+ opened_at=current_date,
327
+ closed_at=None,
328
+ amount=amount,
329
+ status=TradeStatus.OPEN.value,
330
+ cost=capital_for_trade
331
+ )
332
+ last_trade = trade
333
+ trades.append(trade)
334
+
335
+ # If we are in a position, and we get a sell signal
336
+ if current_signal == -1 and last_trade is not None:
337
+ net_gain_val = (
338
+ current_price - last_trade.open_price
339
+ ) * last_trade.available_amount
340
+ sell_order = Order(
341
+ id=uuid4(),
342
+ target_symbol=symbol,
343
+ trading_symbol=trading_symbol,
344
+ order_type=OrderType.LIMIT,
345
+ price=current_price,
346
+ amount=last_trade.available_amount,
347
+ status=OrderStatus.CLOSED,
348
+ created_at=current_date,
349
+ updated_at=current_date,
350
+ order_side=OrderSide.SELL
351
+ )
352
+ orders.append(sell_order)
353
+ trade_orders = last_trade.orders
354
+ trade_orders.append(sell_order)
355
+ last_trade.update(
356
+ {
357
+ "orders": trade_orders,
358
+ "closed_at": current_date,
359
+ "status": TradeStatus.CLOSED,
360
+ "updated_at": current_date,
361
+ "net_gain": net_gain_val
362
+ }
363
+ )
364
+ last_trade = None
365
+
366
+ unallocated = initial_amount
367
+ total_net_gain = 0.0
368
+ open_trades = []
369
+
370
+ # Create portfolio snapshots
371
+ for ts in index:
372
+ allocated = 0
373
+ interval_datetime = pd.Timestamp(ts).to_pydatetime()
374
+ interval_datetime = interval_datetime.replace(tzinfo=timezone.utc)
375
+
376
+ for trade in trades:
377
+
378
+ if trade.opened_at == interval_datetime:
379
+ # Snapshot taken at the moment a trade is opened
380
+ unallocated -= trade.cost
381
+ open_trades.append(trade)
382
+
383
+ if trade.closed_at == interval_datetime:
384
+ # Snapshot taken at the moment a trade is closed
385
+ unallocated += trade.cost + trade.net_gain
386
+ total_net_gain += trade.net_gain
387
+ open_trades.remove(trade)
388
+
389
+ for open_trade in open_trades:
390
+ ohlcv = granular_ohlcv_data_order_by_symbol[
391
+ f"{open_trade.target_symbol}/{trading_symbol}"
392
+ ]
393
+ try:
394
+ price = ohlcv.loc[:ts, "Close"].iloc[-1]
395
+ open_trade.last_reported_price = price
396
+ except IndexError:
397
+ continue # skip if no price yet
398
+
399
+ allocated += open_trade.filled_amount * price
400
+
401
+ # total_value = invested_value + unallocated
402
+ # total_net_gain = total_value - initial_amount
403
+ snapshots.append(
404
+ PortfolioSnapshot(
405
+ portfolio_id=portfolio.identifier,
406
+ created_at=interval_datetime,
407
+ unallocated=unallocated,
408
+ total_value=unallocated + allocated,
409
+ total_net_gain=total_net_gain
410
+ )
411
+ )
412
+
413
+ unique_symbols = set()
414
+ for trade in trades:
415
+ unique_symbols.add(trade.target_symbol)
416
+
417
+ number_of_trades_closed = len(
418
+ [t for t in trades if TradeStatus.CLOSED.equals(t.status)]
419
+ )
420
+ number_of_trades_open = len(
421
+ [t for t in trades if TradeStatus.OPEN.equals(t.status)]
422
+ )
423
+ # Create a backtest run object
424
+ run = BacktestRun(
425
+ trading_symbol=trading_symbol,
426
+ initial_unallocated=initial_amount,
427
+ number_of_runs=1,
428
+ portfolio_snapshots=snapshots,
429
+ trades=trades,
430
+ orders=orders,
431
+ positions=[],
432
+ created_at=datetime.now(timezone.utc),
433
+ backtest_start_date=backtest_date_range.start_date,
434
+ backtest_end_date=backtest_date_range.end_date,
435
+ backtest_date_range_name=backtest_date_range.name,
436
+ number_of_days=(
437
+ backtest_date_range.end_date - backtest_date_range.end_date
438
+ ).days,
439
+ number_of_trades=len(trades),
440
+ number_of_orders=len(orders),
441
+ number_of_trades_closed=number_of_trades_closed,
442
+ number_of_trades_open=number_of_trades_open,
443
+ number_of_positions=len(unique_symbols),
444
+ symbols=list(buy_signals.keys())
445
+ )
446
+
447
+ # Create backtest metrics
448
+ run.backtest_metrics = create_backtest_metrics(
449
+ run, risk_free_rate=risk_free_rate
450
+ )
451
+ return run
452
+
453
+ def generate_schedule(
454
+ self,
455
+ strategies,
456
+ tasks,
457
+ start_date,
458
+ end_date
459
+ ) -> Dict[datetime, Dict[str, List[str]]]:
460
+ """
461
+ Generates a dict-based schedule: datetime => {strategy_ids, task_ids}
462
+ """
463
+ schedule = defaultdict(
464
+ lambda: {"strategy_ids": set(), "task_ids": set(tasks)}
465
+ )
466
+
467
+ for strategy in strategies:
468
+ strategy_id = strategy.strategy_profile.strategy_id
469
+ interval = strategy.strategy_profile.interval
470
+ time_unit = strategy.strategy_profile.time_unit
471
+
472
+ if time_unit == TimeUnit.SECOND:
473
+ step = timedelta(seconds=interval)
474
+ elif time_unit == TimeUnit.MINUTE:
475
+ step = timedelta(minutes=interval)
476
+ elif time_unit == TimeUnit.HOUR:
477
+ step = timedelta(hours=interval)
478
+ elif time_unit == TimeUnit.DAY:
479
+ step = timedelta(days=interval)
480
+ else:
481
+ raise ValueError(f"Unsupported time unit: {time_unit}")
482
+
483
+ t = start_date
484
+ while t <= end_date:
485
+ schedule[t]["strategy_ids"].add(strategy_id)
486
+ t += step
487
+
488
+ return {
489
+ ts: {
490
+ "strategy_ids": sorted(data["strategy_ids"]),
491
+ "task_ids": sorted(data["task_ids"])
492
+ }
493
+ for ts, data in schedule.items()
494
+ }
495
+
496
+ def get_strategy_from_strategy_profiles(self, strategy_profiles, id):
497
+
498
+ for strategy_profile in strategy_profiles:
499
+
500
+ if strategy_profile.strategy_id == id:
501
+ return strategy_profile
502
+
503
+ raise ValueError(f"Strategy profile with id {id} not found.")
504
+
505
+ def _get_initial_unallocated(self) -> float:
506
+ """
507
+ Get the initial unallocated amount for the backtest.
508
+
509
+ Returns:
510
+ float: The initial unallocated amount.
511
+ """
512
+ portfolios = self._portfolio_service.get_all()
513
+ initial_unallocated = 0.0
514
+
515
+ for portfolio in portfolios:
516
+ initial_unallocated += portfolio.initial_balance
517
+
518
+ return initial_unallocated
519
+
520
+ def create_backtest(
521
+ self,
522
+ algorithm,
523
+ number_of_runs,
524
+ backtest_date_range: BacktestDateRange,
525
+ risk_free_rate,
526
+ strategy_directory_path=None
527
+ ) -> Backtest:
528
+ """
529
+ Create a backtest for the given algorithm.
530
+
531
+ It will store all results and metrics in a Backtest object through
532
+ the BacktestResults and BacktestMetrics objects. Optionally,
533
+ it will also store the strategy related paths and backtest
534
+ data file paths.
535
+
536
+ Args:
537
+ algorithm: The algorithm to create the backtest report for
538
+ number_of_runs: The number of runs
539
+ backtest_date_range: The backtest date range of the backtest
540
+ risk_free_rate: The risk-free rate to use for the backtest metrics
541
+ strategy_directory_path (optional, str): The path to the
542
+ strategy directory
543
+
544
+ Returns:
545
+ Backtest: The backtest containing the results and metrics.
546
+ """
547
+
548
+ # Get the first portfolio
549
+ portfolio = self._portfolio_service.get_all()[0]
550
+
551
+ # List all strategy related files in the strategy directory
552
+ strategy_related_paths = []
553
+
554
+ if strategy_directory_path is not None:
555
+ if not os.path.exists(strategy_directory_path) or \
556
+ not os.path.isdir(strategy_directory_path):
557
+ raise OperationalException(
558
+ "Strategy directory does not exist"
559
+ )
560
+
561
+ strategy_files = os.listdir(strategy_directory_path)
562
+ for file in strategy_files:
563
+ source_file = os.path.join(strategy_directory_path, file)
564
+ if os.path.isfile(source_file):
565
+ strategy_related_paths.append(source_file)
566
+ else:
567
+ if algorithm is not None and hasattr(algorithm, 'strategies'):
568
+ for strategy in algorithm.strategies:
569
+ mod = sys.modules[strategy.__module__]
570
+ strategy_directory_path = os.path.dirname(mod.__file__)
571
+ strategy_files = os.listdir(strategy_directory_path)
572
+ for file in strategy_files:
573
+ source_file = os.path.join(
574
+ strategy_directory_path, file
575
+ )
576
+ if os.path.isfile(source_file):
577
+ strategy_related_paths.append(source_file)
578
+
579
+ run = BacktestRun(
580
+ backtest_start_date=backtest_date_range.start_date,
581
+ backtest_end_date=backtest_date_range.end_date,
582
+ backtest_date_range_name=backtest_date_range.name,
583
+ initial_unallocated=self._get_initial_unallocated(),
584
+ trading_symbol=portfolio.trading_symbol,
585
+ created_at=datetime.now(tz=timezone.utc),
586
+ portfolio_snapshots=self._portfolio_snapshot_service.get_all(
587
+ {"portfolio_id": portfolio.id}
588
+ ),
589
+ number_of_runs=number_of_runs,
590
+ trades=self._trade_service.get_all(
591
+ {"portfolio": portfolio.id}
592
+ ),
593
+ orders=self._order_service.get_all(
594
+ {"portfolio": portfolio.id}
595
+ ),
596
+ positions=self._position_repository.get_all(
597
+ {"portfolio": portfolio.id}
598
+ ),
599
+ )
600
+ backtest_metrics = create_backtest_metrics(
601
+ run, risk_free_rate=risk_free_rate
602
+ )
603
+ run.backtest_metrics = backtest_metrics
604
+ return Backtest(
605
+ backtest_runs=[run],
606
+ backtest_summary=generate_backtest_summary_metrics(
607
+ [backtest_metrics]
608
+ )
609
+ )
610
+
611
+ @staticmethod
612
+ def get_most_granular_ohlcv_data_source(data_sources):
613
+ """
614
+ Get the most granular data source from a list of data sources.
615
+
616
+ Args:
617
+ data_sources: List of data sources.
618
+
619
+ Returns:
620
+ The most granular data source.
621
+ """
622
+ granularity_order = {
623
+ TimeFrame.ONE_MINUTE: 1,
624
+ TimeFrame.FIVE_MINUTE: 5,
625
+ TimeFrame.FIFTEEN_MINUTE: 15,
626
+ TimeFrame.ONE_HOUR: 60,
627
+ TimeFrame.TWO_HOUR: 120,
628
+ TimeFrame.FOUR_HOUR: 240,
629
+ TimeFrame.TWELVE_HOUR: 720,
630
+ TimeFrame.ONE_DAY: 1440,
631
+ TimeFrame.ONE_WEEK: 10080,
632
+ TimeFrame.ONE_MONTH: 43200
633
+ }
634
+
635
+ most_granular = None
636
+ highest_granularity = float('inf')
637
+
638
+ ohlcv_data_sources = [
639
+ ds for ds in data_sources if DataType.OHLCV.equals(ds.data_type)
640
+ ]
641
+
642
+ if len(ohlcv_data_sources) == 0:
643
+ raise OperationalException("No OHLCV data sources found")
644
+
645
+ for source in ohlcv_data_sources:
646
+
647
+ if granularity_order[source.time_frame] < highest_granularity:
648
+ highest_granularity = granularity_order[source.time_frame]
649
+ most_granular = source
650
+
651
+ return most_granular