investing-algorithm-framework 3.7.0__py3-none-any.whl → 7.19.15__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 (256) hide show
  1. investing_algorithm_framework/__init__.py +168 -45
  2. investing_algorithm_framework/app/__init__.py +32 -1
  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 +1933 -589
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1725 -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/action_handlers/__init__.py +4 -2
  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 +1 -1
  39. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
  40. investing_algorithm_framework/app/strategy.py +664 -84
  41. investing_algorithm_framework/app/task.py +5 -3
  42. investing_algorithm_framework/app/web/__init__.py +2 -1
  43. investing_algorithm_framework/app/web/create_app.py +4 -2
  44. investing_algorithm_framework/cli/__init__.py +0 -0
  45. investing_algorithm_framework/cli/cli.py +226 -0
  46. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
  47. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  48. investing_algorithm_framework/cli/initialize_app.py +603 -0
  49. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  50. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  51. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  52. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  53. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  54. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  55. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  56. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  57. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  58. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  59. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  60. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  61. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  62. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  63. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  64. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  65. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  66. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  67. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  68. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  69. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  70. investing_algorithm_framework/create_app.py +40 -6
  71. investing_algorithm_framework/dependency_container.py +72 -56
  72. investing_algorithm_framework/domain/__init__.py +71 -47
  73. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  74. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  75. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  76. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  77. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  78. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  79. investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
  80. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  81. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  82. investing_algorithm_framework/domain/config.py +59 -91
  83. investing_algorithm_framework/domain/constants.py +13 -38
  84. investing_algorithm_framework/domain/data_provider.py +334 -0
  85. investing_algorithm_framework/domain/data_structures.py +3 -2
  86. investing_algorithm_framework/domain/exceptions.py +51 -1
  87. investing_algorithm_framework/domain/models/__init__.py +17 -12
  88. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  89. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  90. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  91. investing_algorithm_framework/domain/models/event.py +35 -0
  92. investing_algorithm_framework/domain/models/market/market_credential.py +55 -1
  93. investing_algorithm_framework/domain/models/order/order.py +77 -83
  94. investing_algorithm_framework/domain/models/order/order_status.py +2 -2
  95. investing_algorithm_framework/domain/models/order/order_type.py +1 -3
  96. investing_algorithm_framework/domain/models/portfolio/portfolio.py +81 -3
  97. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +26 -3
  98. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
  99. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  100. investing_algorithm_framework/domain/models/position/position.py +12 -0
  101. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  102. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  103. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  104. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  105. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  106. investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
  107. investing_algorithm_framework/domain/models/time_frame.py +37 -0
  108. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  109. investing_algorithm_framework/domain/models/time_unit.py +66 -2
  110. investing_algorithm_framework/domain/models/trade/__init__.py +8 -1
  111. investing_algorithm_framework/domain/models/trade/trade.py +295 -171
  112. investing_algorithm_framework/domain/models/trade/trade_status.py +9 -2
  113. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
  114. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
  115. investing_algorithm_framework/domain/order_executor.py +112 -0
  116. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  117. investing_algorithm_framework/domain/services/__init__.py +2 -9
  118. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +0 -6
  119. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  120. investing_algorithm_framework/domain/strategy.py +1 -29
  121. investing_algorithm_framework/domain/utils/__init__.py +12 -7
  122. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  123. investing_algorithm_framework/domain/utils/dates.py +57 -0
  124. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  125. investing_algorithm_framework/domain/utils/polars.py +53 -0
  126. investing_algorithm_framework/domain/utils/random.py +29 -0
  127. investing_algorithm_framework/download_data.py +108 -0
  128. investing_algorithm_framework/infrastructure/__init__.py +31 -18
  129. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  130. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
  131. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  132. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  133. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  134. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
  135. investing_algorithm_framework/infrastructure/models/__init__.py +6 -11
  136. investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -1
  137. investing_algorithm_framework/infrastructure/models/order/order.py +35 -49
  138. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  139. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  140. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +1 -1
  141. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -0
  142. investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -5
  143. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  144. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  145. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
  146. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
  147. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  148. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  149. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  150. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  151. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  152. investing_algorithm_framework/infrastructure/repositories/__init__.py +8 -0
  153. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  154. investing_algorithm_framework/infrastructure/repositories/order_repository.py +5 -0
  155. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +1 -1
  156. investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
  157. investing_algorithm_framework/infrastructure/repositories/repository.py +81 -27
  158. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  159. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
  160. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
  161. investing_algorithm_framework/infrastructure/services/__init__.py +4 -4
  162. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  163. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
  164. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  165. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  166. investing_algorithm_framework/services/__init__.py +113 -16
  167. investing_algorithm_framework/services/backtesting/__init__.py +0 -7
  168. investing_algorithm_framework/services/backtesting/backtest_service.py +566 -359
  169. investing_algorithm_framework/services/configuration_service.py +77 -11
  170. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  171. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  172. investing_algorithm_framework/services/market_credential_service.py +16 -1
  173. investing_algorithm_framework/services/metrics/__init__.py +114 -0
  174. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  175. investing_algorithm_framework/services/metrics/beta.py +0 -0
  176. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  177. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  178. investing_algorithm_framework/services/metrics/drawdown.py +181 -0
  179. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  180. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  181. investing_algorithm_framework/services/metrics/generate.py +358 -0
  182. investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
  183. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  184. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  185. investing_algorithm_framework/services/metrics/returns.py +452 -0
  186. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  187. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  188. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  189. investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
  190. investing_algorithm_framework/services/metrics/trades.py +500 -0
  191. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  192. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  193. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  194. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  195. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  196. investing_algorithm_framework/services/order_service/__init__.py +3 -1
  197. investing_algorithm_framework/services/order_service/order_backtest_service.py +76 -89
  198. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  199. investing_algorithm_framework/services/order_service/order_service.py +407 -326
  200. investing_algorithm_framework/services/portfolios/__init__.py +3 -1
  201. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +37 -3
  202. investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +22 -8
  203. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  204. investing_algorithm_framework/services/portfolios/portfolio_service.py +96 -28
  205. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +97 -28
  206. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +116 -313
  207. investing_algorithm_framework/services/positions/__init__.py +7 -0
  208. investing_algorithm_framework/services/positions/position_service.py +210 -0
  209. investing_algorithm_framework/services/repository_service.py +8 -2
  210. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  211. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
  212. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  213. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  214. investing_algorithm_framework/services/trade_service/__init__.py +7 -1
  215. investing_algorithm_framework/services/trade_service/trade_service.py +1013 -315
  216. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  217. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  218. investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
  219. investing_algorithm_framework-7.19.15.dist-info/RECORD +263 -0
  220. investing_algorithm_framework-7.19.15.dist-info/entry_points.txt +3 -0
  221. investing_algorithm_framework/app/algorithm.py +0 -1105
  222. investing_algorithm_framework/domain/graphs.py +0 -382
  223. investing_algorithm_framework/domain/metrics/__init__.py +0 -6
  224. investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -11
  225. investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -43
  226. investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
  227. investing_algorithm_framework/domain/models/backtesting/backtest_report.py +0 -580
  228. investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -243
  229. investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
  230. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  231. investing_algorithm_framework/domain/services/market_data_sources.py +0 -344
  232. investing_algorithm_framework/domain/services/market_service.py +0 -153
  233. investing_algorithm_framework/domain/singleton.py +0 -9
  234. investing_algorithm_framework/domain/utils/backtesting.py +0 -472
  235. investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -12
  236. investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -559
  237. investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -254
  238. investing_algorithm_framework/infrastructure/models/market_data_sources/us_treasury_yield.py +0 -47
  239. investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
  240. investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -455
  241. investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
  242. investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
  243. investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -350
  244. investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py +0 -53
  245. investing_algorithm_framework/services/backtesting/graphs.py +0 -61
  246. investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -8
  247. investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -150
  248. investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -189
  249. investing_algorithm_framework/services/position_service.py +0 -31
  250. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -264
  251. investing_algorithm_framework-3.7.0.dist-info/METADATA +0 -339
  252. investing_algorithm_framework-3.7.0.dist-info/RECORD +0 -147
  253. /investing_algorithm_framework/{domain → services}/metrics/price_efficiency.py +0 -0
  254. /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
  255. {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
  256. {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
@@ -1,15 +1,28 @@
1
- from datetime import datetime, timedelta
2
-
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
3
10
  import pandas as pd
4
- from dateutil import parser
5
- from tqdm import tqdm
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
6
24
 
7
- from investing_algorithm_framework.domain import BacktestReport, \
8
- BACKTESTING_INDEX_DATETIME, TimeUnit, BacktestPosition, \
9
- TradingDataType, OrderStatus, OperationalException, MarketDataSource, \
10
- OrderSide, SYMBOLS, BacktestDateRange
11
- from investing_algorithm_framework.services.market_data_source_service import \
12
- MarketDataSourceService
25
+ logger = logging.getLogger(__name__)
13
26
 
14
27
 
15
28
  class BacktestService:
@@ -18,192 +31,467 @@ class BacktestService:
18
31
  """
19
32
 
20
33
  def __init__(
21
- self,
22
- market_data_source_service: MarketDataSourceService,
23
- order_service,
24
- portfolio_repository,
25
- position_repository,
26
- performance_service,
27
- configuration_service
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,
28
43
  ):
29
- self._resource_directory = None
44
+ super().__init__()
30
45
  self._order_service = order_service
31
- self._portfolio_repository = portfolio_repository
32
- self._data_index = {
33
- TradingDataType.OHLCV: {},
34
- TradingDataType.TICKER: {}
35
- }
36
- self._performance_service = performance_service
46
+ self._trade_service = trade_service
47
+ self._portfolio_service = portfolio_service
48
+ self._portfolio_snapshot_service = portfolio_snapshot_service
37
49
  self._position_repository = position_repository
38
- self._market_data_source_service: MarketDataSourceService \
39
- = market_data_source_service
40
- self._backtest_market_data_sources = []
41
50
  self._configuration_service = configuration_service
51
+ self._portfolio_configuration_service: PortfolioConfigurationService \
52
+ = portfolio_configuration_service
53
+ self._data_provider_service = data_provider_service
42
54
 
43
- @property
44
- def resource_directory(self):
45
- return self._resource_directory
55
+ def validate_strategy_for_vector_backtest(self, strategy):
56
+ """
57
+ Validate if the strategy is suitable for backtesting.
46
58
 
47
- @resource_directory.setter
48
- def resource_directory(self, resource_directory):
49
- self._resource_directory = resource_directory
59
+ Args:
60
+ strategy: The strategy to validate.
50
61
 
51
- def run_backtest(
52
- self, algorithm, backtest_date_range: BacktestDateRange
53
- ) -> BacktestReport:
62
+ Raises:
63
+ OperationalException: If the strategy does not have the required
64
+ buy/sell signal functions.
54
65
  """
55
- Run a backtest for the given algorithm. This function will run
56
- a backtest for the given algorithm and return a backtest report.
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
+ )
57
76
 
58
- A schedule is generated for the given algorithm and the strategies
59
- are run for each date in the schedule.
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.
60
84
 
61
- Also, all backtest data is downloaded (if not already downloaded) and
62
- the backtest is run for each date in the schedule.
85
+ For a polars DataFrame, the 'DateTime' column will be
86
+ used as the index if it exists.
63
87
 
64
- At the end of the run all traces
88
+ If no index is found an exception will be raised.
65
89
 
66
90
  Args:
67
- algorithm: The algorithm to run the backtest for
68
- backtest_date_range: The backtest date range
91
+ data: The data frame to process.
92
+
93
+ Raises:
94
+ OperationalException: If no valid index is found.
69
95
 
70
- return:
71
- BacktestReport: The backtest report
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:
72
123
  """
73
- strategy_profiles = []
74
- portfolios = self._portfolio_repository.get_all()
75
- initial_unallocated = 0
124
+ Vectorized backtest for multiple assets using strategy
125
+ buy/sell signals.
76
126
 
77
- for portfolio in portfolios:
78
- initial_unallocated += portfolio.unallocated
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.
79
140
 
80
- for strategy in algorithm.strategies:
81
- strategy_profiles.append(strategy.strategy_profile)
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
+ )
82
162
 
83
- # Check if required market data sources are registered
84
- self._check_if_required_market_data_sources_are_registered()
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
+ )
85
181
 
86
- schedule = self.generate_schedule(
87
- strategies=algorithm.strategies,
182
+ # Load vectorized backtest data
183
+ data = self._data_provider_service.get_vectorized_backtest_data(
184
+ data_sources=strategy.data_sources,
88
185
  start_date=backtest_date_range.start_date,
89
186
  end_date=backtest_date_range.end_date
90
187
  )
91
188
 
92
- for index, row in tqdm(
93
- schedule.iterrows(),
94
- total=len(schedule),
95
- desc=f"Running backtest for algorithm with name {algorithm.name}",
96
- colour="GREEN"
97
- ):
98
- strategy_profile = self.get_strategy_from_strategy_profiles(
99
- strategy_profiles, row['id']
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
100
199
  )
101
- index_date = parser.parse(str(index))
102
- self.run_backtest_for_profile(
103
- algorithm=algorithm,
104
- strategy=algorithm.get_strategy(strategy_profile.strategy_id),
105
- index_date=index_date,
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
106
205
  )
107
- return self.create_backtest_report(
108
- algorithm, len(schedule), backtest_date_range, initial_unallocated
109
- )
110
206
 
111
- def run_backtests(
112
- self, algorithms, backtest_date_range: BacktestDateRange
113
- ):
114
- """
115
- Run backtests for the given algorithms. This function will run
116
- backtests for the given algorithms and return a list of backtest
117
- reports.
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
+ )
118
265
 
119
- :param algorithms: The algorithms to run the backtests for
120
- :param backtest_date_range: The backtest date range of the backtests
121
- :return: A list of backtest reports
122
- """
123
- backtest_reports = []
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
+ )
124
277
 
125
- for algorithm in algorithms:
126
- backtest_reports.append(
127
- self.run_backtest(
128
- algorithm=algorithm,
129
- backtest_date_range=backtest_date_range
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
130
410
  )
131
411
  )
132
412
 
133
- return backtest_reports
134
-
135
- def run_backtest_for_profile(self, algorithm, strategy, index_date):
136
- algorithm.config[BACKTESTING_INDEX_DATETIME] = index_date
137
- market_data = {}
138
-
139
- if strategy.strategy_profile.market_data_sources is not None:
140
-
141
- for data_id in strategy.strategy_profile.market_data_sources:
413
+ unique_symbols = set()
414
+ for trade in trades:
415
+ unique_symbols.add(trade.target_symbol)
142
416
 
143
- if isinstance(data_id, MarketDataSource):
144
- market_data[data_id.get_identifier()] = \
145
- self._market_data_source_service.get_data(
146
- data_id.get_identifier()
147
- )
148
- else:
149
- market_data[data_id] = \
150
- self._market_data_source_service.get_data(data_id)
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
+ )
151
446
 
152
- strategy.context = algorithm.context
153
- strategy.run_strategy(algorithm=algorithm, market_data=market_data)
447
+ # Create backtest metrics
448
+ run.backtest_metrics = create_backtest_metrics(
449
+ run, risk_free_rate=risk_free_rate
450
+ )
451
+ return run
154
452
 
155
453
  def generate_schedule(
156
- self, strategies, start_date, end_date
157
- ) -> pd.DataFrame:
454
+ self,
455
+ strategies,
456
+ tasks,
457
+ start_date,
458
+ end_date
459
+ ) -> Dict[datetime, Dict[str, List[str]]]:
158
460
  """
159
- Generate a schedule for the given strategies. This function will
160
- calculate when the strategies should run based on the given start
161
- and end date. The schedule will be stored in a pandas DataFrame.
162
-
163
- Args:
164
- strategies: The strategies to generate the schedule for
165
- start_date: The start date of the schedule
166
- end_date: The end date of the schedule
167
-
168
- Returns:
169
- pd.DataFrame: The schedule DataFrame
461
+ Generates a dict-based schedule: datetime => {strategy_ids, task_ids}
170
462
  """
171
- data = []
463
+ schedule = defaultdict(
464
+ lambda: {"strategy_ids": set(), "task_ids": set(tasks)}
465
+ )
172
466
 
173
467
  for strategy in strategies:
174
- id = strategy.strategy_profile.strategy_id
175
- time_unit = strategy.strategy_profile.time_unit
468
+ strategy_id = strategy.strategy_profile.strategy_id
176
469
  interval = strategy.strategy_profile.interval
177
- current_time = start_date
178
-
179
- while current_time <= end_date:
180
- data.append({
181
- "id": id,
182
- 'run_time': current_time,
183
- })
184
-
185
- if TimeUnit.SECOND.equals(time_unit):
186
- current_time += timedelta(seconds=interval)
187
- elif TimeUnit.MINUTE.equals(time_unit):
188
- current_time += timedelta(minutes=interval)
189
- elif TimeUnit.HOUR.equals(time_unit):
190
- current_time += timedelta(hours=interval)
191
- elif TimeUnit.DAY.equals(time_unit):
192
- current_time += timedelta(days=interval)
193
- else:
194
- raise ValueError(f"Unsupported time unit: {time_unit}")
195
-
196
- schedule_df = pd.DataFrame(data)
197
- if schedule_df.empty:
198
- raise OperationalException(
199
- "Could not generate schedule "
200
- "for backtest, do you have a strategy "
201
- "registered for your algorithm?"
202
- )
470
+ time_unit = strategy.strategy_profile.time_unit
203
471
 
204
- schedule_df.sort_values(by='run_time', inplace=True)
205
- schedule_df.set_index('run_time', inplace=True)
206
- return schedule_df
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
+ }
207
495
 
208
496
  def get_strategy_from_strategy_profiles(self, strategy_profiles, id):
209
497
 
@@ -214,231 +502,150 @@ class BacktestService:
214
502
 
215
503
  raise ValueError(f"Strategy profile with id {id} not found.")
216
504
 
217
- def create_backtest_report(
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(
218
521
  self,
219
522
  algorithm,
220
523
  number_of_runs,
221
524
  backtest_date_range: BacktestDateRange,
222
- initial_unallocated=0
223
- ) -> BacktestReport:
525
+ risk_free_rate,
526
+ strategy_directory_path=None
527
+ ) -> Backtest:
224
528
  """
225
- Create a backtest report for the given algorithm. This function
226
- will create a backtest report for the given algorithm and return
227
- the backtest report instance.
529
+ Create a backtest for the given algorithm.
228
530
 
229
- It will calculate various performance metrics for the backtest.
230
- Also, it will add all traces to the backtest report. The traces
231
- are collected from each strategy that was run during the backtest.
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.
232
535
 
233
536
  Args:
234
537
  algorithm: The algorithm to create the backtest report for
235
538
  number_of_runs: The number of runs
236
539
  backtest_date_range: The backtest date range of the backtest
237
- initial_unallocated: The initial unallocated amount
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
238
543
 
239
544
  Returns:
240
- BacktestReport: The backtest report instance of BacktestReport
545
+ Backtest: The backtest containing the results and metrics.
241
546
  """
242
547
 
243
- for portfolio in self._portfolio_repository.get_all():
244
- ids = [strategy.strategy_id for strategy in algorithm.strategies]
245
-
246
- # Check if strategy_id is None
247
- if None in ids:
248
- # Remove None from ids
249
- ids = [x for x in ids if x is not None]
250
-
251
- backtest_report = BacktestReport(
252
- name=algorithm.name,
253
- strategy_identifiers=ids,
254
- backtest_date_range=backtest_date_range,
255
- initial_unallocated=initial_unallocated,
256
- trading_symbol=portfolio.trading_symbol,
257
- created_at=datetime.utcnow(),
258
- )
259
- backtest_report.number_of_runs = number_of_runs
260
- backtest_report.number_of_orders = self._order_service.count({
261
- "portfolio": portfolio.id
262
- })
263
- backtest_report.number_of_positions = \
264
- self._position_repository.count({
265
- "portfolio": portfolio.id,
266
- "amount_gt": 0
267
- })
268
- backtest_report.percentage_negative_trades = \
269
- self._performance_service \
270
- .get_percentage_negative_trades(portfolio.id)
271
- backtest_report.percentage_positive_trades = \
272
- self._performance_service \
273
- .get_percentage_positive_trades(portfolio.id)
274
- backtest_report.number_of_trades_closed = \
275
- self._performance_service \
276
- .get_number_of_trades_closed(portfolio.id)
277
- backtest_report.number_of_trades_open = \
278
- self._performance_service \
279
- .get_number_of_trades_open(portfolio.id)
280
- backtest_report.total_cost = portfolio.total_cost
281
- backtest_report.total_net_gain = portfolio.total_net_gain
282
- backtest_report.total_net_gain_percentage = \
283
- self._performance_service \
284
- .get_total_net_gain_percentage_of_backtest(
285
- portfolio.id, backtest_report
286
- )
287
- positions = self._position_repository.get_all({
288
- "portfolio": portfolio.id
289
- })
290
- orders = self._order_service.get_all({
291
- "portfolio": portfolio.id
292
- })
293
- tickers = {}
294
-
295
- for position in positions:
296
-
297
- if position.symbol != portfolio.trading_symbol:
298
- ticker_symbol = \
299
- f"{position.symbol}/{portfolio.trading_symbol}"
300
-
301
- if not self._market_data_source_service\
302
- .has_ticker_market_data_source(
303
- symbol=ticker_symbol, market=portfolio.market
304
- ):
305
- raise OperationalException(
306
- f"Ticker market data source for "
307
- f"symbol {ticker_symbol} and market "
308
- f"{portfolio.market} not found, please make "
309
- f"sure you register a ticker market data "
310
- f"source for this symbol and market in "
311
- f"backtest mode. Otherwise, the backtest "
312
- f"report cannot be generated."
313
- )
314
- tickers[ticker_symbol] = \
315
- self._market_data_source_service.get_ticker(
316
- f"{position.symbol}/{portfolio.trading_symbol}",
317
- market=portfolio.market
318
- )
548
+ # Get the first portfolio
549
+ portfolio = self._portfolio_service.get_all()[0]
319
550
 
320
- backtest_report.growth_rate = self._performance_service \
321
- .get_growth_rate_of_backtest(
322
- portfolio.id, tickers, backtest_report
323
- )
324
- backtest_report.growth = self._performance_service \
325
- .get_growth_of_backtest(
326
- portfolio.id, tickers, backtest_report
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"
327
559
  )
328
- backtest_report.total_value = self._performance_service \
329
- .get_total_value(portfolio.id, tickers, backtest_report)
330
- backtest_report.average_trade_duration = \
331
- self._performance_service \
332
- .get_average_trade_duration(portfolio.id)
333
- backtest_report.average_trade_size = \
334
- self._performance_service.get_average_trade_size(portfolio.id)
335
- positions = self._position_repository.get_all({
336
- "portfolio": portfolio.id
337
- })
338
- backtest_positions = []
339
-
340
- for position in positions:
341
-
342
- if position.symbol == portfolio.trading_symbol:
343
- backtest_position = BacktestPosition(
344
- position,
345
- trading_symbol=True,
346
- total_value_portfolio=backtest_report.total_value
347
- )
348
- backtest_position.price = 1
349
- else:
350
- pending_buy_orders = self._order_service.get_all({
351
- "portfolio": portfolio.id,
352
- "target_symbol": position.symbol,
353
- "status": OrderStatus.OPEN.value,
354
- "order_side": OrderSide.BUY.value
355
- })
356
- amount_in_pending_buy_orders = 0
357
-
358
- for order in pending_buy_orders:
359
- amount_in_pending_buy_orders += order.amount
360
-
361
- pending_sell_orders = self._order_service.get_all({
362
- "portfolio": portfolio.id,
363
- "target_symbol": position.symbol,
364
- "status": OrderStatus.OPEN.value,
365
- "order_side": OrderSide.SELL.value
366
- })
367
- amount_in_pending_sell_orders = 0
368
-
369
- for order in pending_sell_orders:
370
- amount_in_pending_sell_orders += order.amount
371
-
372
- backtest_position = BacktestPosition(
373
- position,
374
- amount_pending_buy=amount_in_pending_buy_orders,
375
- amount_pending_sell=amount_in_pending_sell_orders,
376
- total_value_portfolio=backtest_report.total_value
377
- )
378
560
 
379
- # Probably not needed
380
- ticker = self._market_data_source_service \
381
- .get_ticker(
382
- symbol=f"{position.symbol}"
383
- f"/{portfolio.trading_symbol}",
384
- market=portfolio.market
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
385
575
  )
386
- backtest_position.price = ticker["bid"]
387
- backtest_positions.append(backtest_position)
388
- backtest_report.positions = backtest_positions
389
- backtest_report.trades = algorithm.get_trades()
390
- backtest_report.orders = orders
391
- backtest_report.context = algorithm.context
392
- traces = {}
393
-
394
- # Add traces to the backtest report
395
- for strategy in algorithm.strategies:
396
- strategy_traces = strategy.get_traces()
397
- traces[strategy.strategy_id] = strategy_traces
398
-
399
- backtest_report.traces = traces
400
-
401
- # Calculate metrics for the backtest report
402
- backtest_report.calculate_metrics()
403
- return backtest_report
404
-
405
- def set_backtest_market_data_sources(self, market_data_sources):
406
- self._backtest_market_data_sources = market_data_sources
407
-
408
- def get_backtest_market_data_sources(self):
409
- return self._backtest_market_data_sources
410
-
411
- def get_backtest_market_data_source(self, symbol, market):
412
-
413
- for market_data_source in self._backtest_market_data_sources:
414
- if market_data_source.symbol == symbol \
415
- and market_data_source.market == market:
416
- return market_data_source
417
- raise OperationalException(
418
- f"Market data source for "
419
- f"symbol {symbol} and market {market} not found"
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
+ )
420
609
  )
421
610
 
422
- def _check_if_required_market_data_sources_are_registered(self):
611
+ @staticmethod
612
+ def get_most_granular_ohlcv_data_source(data_sources):
423
613
  """
424
- Check if the required market data sources are registered.
614
+ Get the most granular data source from a list of data sources.
425
615
 
426
- It will iterate over all registered symbols and markets and check
427
- if a ticker market data source is registered for the symbol and market.
616
+ Args:
617
+ data_sources: List of data sources.
618
+
619
+ Returns:
620
+ The most granular data source.
428
621
  """
429
- symbols = self._configuration_service.config[SYMBOLS]
430
-
431
- if symbols is not None:
432
-
433
- for symbol in symbols:
434
- if not self._market_data_source_service\
435
- .has_ticker_market_data_source(
436
- symbol=symbol
437
- ):
438
- raise OperationalException(
439
- f"Ticker market data source for symbol {symbol} not "
440
- f"found, please make sure you register a ticker "
441
- f"market data source for this symbol in backtest "
442
- f"mode. Otherwise, the backtest report "
443
- f"cannot be generated."
444
- )
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