investing-algorithm-framework 6.9.1__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 (192) hide show
  1. investing_algorithm_framework/__init__.py +147 -44
  2. investing_algorithm_framework/app/__init__.py +23 -6
  3. investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
  4. investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
  5. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  6. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  7. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  8. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  9. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  10. investing_algorithm_framework/app/app.py +1322 -707
  11. investing_algorithm_framework/app/context.py +196 -88
  12. investing_algorithm_framework/app/eventloop.py +590 -0
  13. investing_algorithm_framework/app/reporting/__init__.py +16 -5
  14. investing_algorithm_framework/app/reporting/ascii.py +57 -202
  15. investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
  16. investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
  17. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  18. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  19. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
  20. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  21. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  22. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
  23. investing_algorithm_framework/app/reporting/generate.py +100 -114
  24. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
  25. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
  26. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
  27. investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
  28. investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
  29. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
  30. investing_algorithm_framework/app/strategy.py +315 -175
  31. investing_algorithm_framework/app/task.py +5 -3
  32. investing_algorithm_framework/cli/cli.py +30 -12
  33. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
  34. investing_algorithm_framework/cli/initialize_app.py +20 -1
  35. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
  36. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  37. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  38. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
  39. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
  40. investing_algorithm_framework/create_app.py +3 -5
  41. investing_algorithm_framework/dependency_container.py +25 -39
  42. investing_algorithm_framework/domain/__init__.py +45 -38
  43. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  44. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  45. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  46. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  47. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  48. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  49. investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
  50. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  51. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  52. investing_algorithm_framework/domain/config.py +27 -0
  53. investing_algorithm_framework/domain/constants.py +6 -34
  54. investing_algorithm_framework/domain/data_provider.py +200 -56
  55. investing_algorithm_framework/domain/exceptions.py +34 -1
  56. investing_algorithm_framework/domain/models/__init__.py +10 -19
  57. investing_algorithm_framework/domain/models/base_model.py +0 -6
  58. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  59. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  60. investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
  61. investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
  62. investing_algorithm_framework/domain/models/order/order.py +34 -13
  63. investing_algorithm_framework/domain/models/order/order_status.py +1 -1
  64. investing_algorithm_framework/domain/models/order/order_type.py +1 -1
  65. investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
  66. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
  67. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
  68. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  69. investing_algorithm_framework/domain/models/position/position.py +9 -0
  70. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  71. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  72. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  73. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  74. investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
  75. investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
  76. investing_algorithm_framework/domain/models/time_frame.py +7 -0
  77. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  78. investing_algorithm_framework/domain/models/time_unit.py +63 -1
  79. investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
  80. investing_algorithm_framework/domain/models/trade/trade.py +56 -32
  81. investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
  82. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
  83. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
  84. investing_algorithm_framework/domain/order_executor.py +19 -0
  85. investing_algorithm_framework/domain/portfolio_provider.py +20 -1
  86. investing_algorithm_framework/domain/services/__init__.py +0 -13
  87. investing_algorithm_framework/domain/strategy.py +1 -29
  88. investing_algorithm_framework/domain/utils/__init__.py +5 -1
  89. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  90. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  91. investing_algorithm_framework/domain/utils/polars.py +17 -14
  92. investing_algorithm_framework/download_data.py +40 -10
  93. investing_algorithm_framework/infrastructure/__init__.py +13 -25
  94. investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
  95. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
  96. investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
  97. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  98. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  99. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
  100. investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
  101. investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
  102. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
  103. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
  104. investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
  105. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  106. investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
  107. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
  108. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
  109. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
  110. investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
  111. investing_algorithm_framework/services/__init__.py +105 -8
  112. investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
  113. investing_algorithm_framework/services/configuration_service.py +14 -4
  114. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  115. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  116. investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
  117. investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
  118. investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
  119. investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
  120. investing_algorithm_framework/services/metrics/generate.py +358 -0
  121. investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
  122. investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
  123. investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
  124. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  125. investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
  126. investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
  127. investing_algorithm_framework/services/metrics/trades.py +500 -0
  128. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  129. investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
  130. investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
  131. investing_algorithm_framework/services/order_service/order_service.py +9 -71
  132. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
  133. investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
  134. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
  135. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
  136. investing_algorithm_framework/services/repository_service.py +5 -2
  137. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  138. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
  139. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  140. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  141. investing_algorithm_framework/services/trade_service/__init__.py +7 -1
  142. investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
  143. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  144. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  145. investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
  146. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
  147. investing_algorithm_framework/app/reporting/evaluation.py +0 -243
  148. investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
  149. investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
  150. investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
  151. investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
  152. investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
  153. investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
  154. investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
  155. investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
  156. investing_algorithm_framework/domain/models/data_source.py +0 -21
  157. investing_algorithm_framework/domain/models/date_range.py +0 -64
  158. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
  159. investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
  160. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  161. investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
  162. investing_algorithm_framework/domain/services/market_service.py +0 -153
  163. investing_algorithm_framework/domain/services/observable.py +0 -51
  164. investing_algorithm_framework/domain/services/observer.py +0 -19
  165. investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
  166. investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
  167. investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
  168. investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
  169. investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
  170. investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
  171. investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
  172. investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
  173. investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
  174. investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
  175. investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
  176. investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
  177. investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
  178. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
  179. investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
  180. /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
  181. /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
  182. /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
  183. /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
  184. /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
  185. /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
  186. /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
  187. /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
  188. /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
  189. /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
  190. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
  191. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
  192. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/entry_points.txt +0 -0
@@ -1,278 +1,497 @@
1
- import json
2
1
  import logging
3
2
  import os
4
- import re
3
+ import sys
4
+ from collections import defaultdict
5
5
  from datetime import datetime, timedelta, timezone
6
+ from typing import Dict, List, Union
7
+ from uuid import uuid4
6
8
 
9
+ import numpy as np
7
10
  import pandas as pd
8
- from dateutil import parser
9
- from tqdm import tqdm
10
-
11
- from investing_algorithm_framework.domain import BacktestResult, \
12
- BACKTESTING_INDEX_DATETIME, TimeUnit, TradingDataType, \
13
- OperationalException, MarketDataSource, Observable, Event, \
14
- SYMBOLS, BacktestDateRange, DATETIME_FORMAT_BACKTESTING
15
- from investing_algorithm_framework.services.market_data_source_service import \
16
- MarketDataSourceService
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
17
24
 
18
25
  logger = logging.getLogger(__name__)
19
- BACKTEST_REPORT_FILE_NAME_PATTERN = (
20
- r"^report_\w+_backtest-start-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
21
- r"backtest-end-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
22
- r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}\.json$"
23
- )
24
- BACKTEST_REPORT_DIRECTORY_PATTERN = (
25
- r"^report_\w+_backtest-start-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
26
- r"backtest-end-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
27
- r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}$"
28
- )
29
-
30
-
31
- class BacktestService(Observable):
26
+
27
+
28
+ class BacktestService:
32
29
  """
33
30
  Service that facilitates backtests for algorithm objects.
34
31
  """
35
32
 
36
33
  def __init__(
37
34
  self,
38
- market_data_source_service: MarketDataSourceService,
35
+ data_provider_service: DataProviderService,
39
36
  order_service,
40
37
  portfolio_service,
41
38
  portfolio_snapshot_service,
42
39
  position_repository,
43
40
  trade_service,
44
- performance_service,
45
41
  configuration_service,
46
42
  portfolio_configuration_service,
47
- strategy_orchestrator_service
48
43
  ):
49
44
  super().__init__()
50
- self._resource_directory = None
51
45
  self._order_service = order_service
52
46
  self._trade_service = trade_service
53
47
  self._portfolio_service = portfolio_service
54
- self._data_index = {
55
- TradingDataType.OHLCV: {},
56
- TradingDataType.TICKER: {}
57
- }
58
- self._performance_service = performance_service
59
48
  self._portfolio_snapshot_service = portfolio_snapshot_service
60
49
  self._position_repository = position_repository
61
- self._market_data_source_service: MarketDataSourceService \
62
- = market_data_source_service
63
- self._backtest_market_data_sources = []
64
50
  self._configuration_service = configuration_service
65
- self._portfolio_configuration_service = portfolio_configuration_service
66
- self._strategy_orchestrator_service = strategy_orchestrator_service
51
+ self._portfolio_configuration_service: PortfolioConfigurationService \
52
+ = portfolio_configuration_service
53
+ self._data_provider_service = data_provider_service
67
54
 
68
- @property
69
- def resource_directory(self):
70
- return self._resource_directory
55
+ def validate_strategy_for_vector_backtest(self, strategy):
56
+ """
57
+ Validate if the strategy is suitable for backtesting.
71
58
 
72
- @resource_directory.setter
73
- def resource_directory(self, resource_directory):
74
- self._resource_directory = resource_directory
59
+ Args:
60
+ strategy: The strategy to validate.
75
61
 
76
- def run_backtest(
77
- self,
78
- algorithm,
79
- context,
80
- strategy_orchestrator_service,
81
- backtest_date_range: BacktestDateRange,
82
- initial_amount=None
83
- ) -> BacktestResult:
62
+ Raises:
63
+ OperationalException: If the strategy does not have the required
64
+ buy/sell signal functions.
84
65
  """
85
- Run a backtest for the given algorithm. This function will run
86
- 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
+ )
87
76
 
88
- A schedule is generated for the given algorithm and the strategies
89
- 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.
84
+
85
+ For a polars DataFrame, the 'DateTime' column will be
86
+ used as the index if it exists.
90
87
 
91
- Also, all backtest data is downloaded (if not already downloaded) and
92
- the backtest is run for each date in the schedule.
88
+ If no index is found an exception will be raised.
93
89
 
94
90
  Args:
95
- algorithm: The algorithm to run the backtest for
96
- backtest_date_range: The backtest date range
97
- initial_amount: The initial amount of the backtest portfolio
98
- strategy_orchestrator_service: The strategy orchestrator service
99
- context (Context): The context of the object of the application
91
+ data: The data frame to process.
92
+
93
+ Raises:
94
+ OperationalException: If no valid index is found.
100
95
 
101
96
  Returns:
102
- BacktestResult - The backtest report
97
+ The index of the data frame.
103
98
  """
104
- logging.info(
105
- f"Running backtest for algorithm with name {algorithm.name}"
106
- )
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.
107
126
 
108
- # Create backtest portfolio
109
- portfolio_configurations = \
110
- self._portfolio_configuration_service.get_all()
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.
111
140
 
112
- for portfolio_configuration in portfolio_configurations:
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
+ )
113
162
 
114
- if self._portfolio_service.exists(
115
- {"identifier": portfolio_configuration.identifier}
116
- ):
117
- # Delete existing portfolio
118
- portfolio = self._portfolio_service.find(
119
- {"identifier": portfolio_configuration.identifier}
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
120
172
  )
121
- self._portfolio_service.delete(portfolio.id)
122
-
123
- # Check if the portfolio configuration has an initial balance
124
- self._portfolio_service.create_portfolio_from_configuration(
125
- portfolio_configuration,
126
- initial_amount=initial_amount,
127
- created_at=backtest_date_range.start_date,
128
173
  )
129
174
 
130
- strategy_profiles = []
131
- portfolios = self._portfolio_service.get_all()
132
- initial_unallocated = 0
133
-
134
- for portfolio in portfolios:
135
- initial_unallocated += portfolio.unallocated
136
-
137
- for strategy in algorithm.strategies:
138
- strategy_profiles.append(strategy.strategy_profile)
175
+ portfolio_configuration = portfolio_configurations[0]
139
176
 
140
- # Check if required market data sources are registered
141
- self._check_if_required_market_data_sources_are_registered()
177
+ trading_symbol = portfolio_configurations[0].trading_symbol
178
+ portfolio = Portfolio.from_portfolio_configuration(
179
+ portfolio_configuration
180
+ )
142
181
 
143
- schedule = self.generate_schedule(
144
- strategies=algorithm.strategies,
182
+ # Load vectorized backtest data
183
+ data = self._data_provider_service.get_vectorized_backtest_data(
184
+ data_sources=strategy.data_sources,
145
185
  start_date=backtest_date_range.start_date,
146
186
  end_date=backtest_date_range.end_date
147
187
  )
148
188
 
149
- logger.info(f"Prepared backtests for {len(schedule)} strategies")
189
+ # Compute signals from strategy
190
+ buy_signals = strategy.generate_buy_signals(data)
191
+ sell_signals = strategy.generate_sell_signals(data)
150
192
 
151
- for index, row in tqdm(
152
- schedule.iterrows(),
153
- total=len(schedule),
154
- desc=f"Running backtest for algorithm with name {algorithm.name}",
155
- colour="GREEN"
156
- ):
157
- strategy_profile = self.get_strategy_from_strategy_profiles(
158
- strategy_profiles, row['id']
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
159
199
  )
160
- index_date = parser.parse(str(index))
161
- self._configuration_service.add_value(
162
- BACKTESTING_INDEX_DATETIME, 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
163
205
  )
164
- config = self._configuration_service.get_config()
165
- strategy = algorithm.get_strategy(strategy_profile.strategy_id)
166
- strategy_orchestrator_service.run_backtest_strategy(
167
- context=context, strategy=strategy, config=config
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
168
229
  )
169
- self.notify_observers(Event.STRATEGY_RUN, {
170
- "created_at": index_date,
171
- })
172
-
173
- report = self.create_backtest_report(
174
- algorithm,
175
- context,
176
- len(schedule),
177
- backtest_date_range,
178
- initial_unallocated
179
- )
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
+ )
180
265
 
181
- # Cleanup backtest portfolio
182
- portfolio_configurations = \
183
- self._portfolio_configuration_service.get_all()
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
+ )
184
277
 
185
- for portfolio_configuration in portfolio_configurations:
186
- portfolio = self._portfolio_service.find(
187
- {"identifier": portfolio_configuration.identifier}
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
+ )
188
411
  )
189
- self._portfolio_service.delete(portfolio.id)
190
412
 
191
- return report
413
+ unique_symbols = set()
414
+ for trade in trades:
415
+ unique_symbols.add(trade.target_symbol)
192
416
 
193
- def run_backtest_for_profile(
194
- self, context, algorithm, strategy, index_date
195
- ):
196
- self._configuration_service.add_value(
197
- BACKTESTING_INDEX_DATETIME, index_date
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())
198
445
  )
199
- algorithm.config[BACKTESTING_INDEX_DATETIME] = index_date
200
- market_data = {}
201
-
202
- if strategy.strategy_profile.market_data_sources is not None:
203
-
204
- for data_id in strategy.strategy_profile.market_data_sources:
205
-
206
- if isinstance(data_id, MarketDataSource):
207
- market_data[data_id.get_identifier()] = \
208
- self._market_data_source_service.get_data(
209
- data_id.get_identifier()
210
- )
211
- else:
212
- market_data[data_id] = \
213
- self._market_data_source_service.get_data(data_id)
214
-
215
- strategy.run_strategy(context=context, market_data=market_data)
216
446
 
217
- def run_backtest_v2(self, strategy, context):
218
- config = self._configuration_service.get_config()
219
- self._strategy_orchestrator_service.run_backtest_strategy(
220
- context=context, strategy=strategy, config=config
447
+ # Create backtest metrics
448
+ run.backtest_metrics = create_backtest_metrics(
449
+ run, risk_free_rate=risk_free_rate
221
450
  )
451
+ return run
222
452
 
223
453
  def generate_schedule(
224
- self, strategies, start_date, end_date
225
- ) -> pd.DataFrame:
454
+ self,
455
+ strategies,
456
+ tasks,
457
+ start_date,
458
+ end_date
459
+ ) -> Dict[datetime, Dict[str, List[str]]]:
226
460
  """
227
- Generate a schedule for the given strategies. This function will
228
- calculate when the strategies should run based on the given start
229
- and end date. The schedule will be stored in a pandas DataFrame.
230
-
231
- Args:
232
- strategies: The strategies to generate the schedule for
233
- start_date: The start date of the schedule
234
- end_date: The end date of the schedule
235
-
236
- Returns:
237
- pd.DataFrame: The schedule DataFrame
461
+ Generates a dict-based schedule: datetime => {strategy_ids, task_ids}
238
462
  """
239
- data = []
463
+ schedule = defaultdict(
464
+ lambda: {"strategy_ids": set(), "task_ids": set(tasks)}
465
+ )
240
466
 
241
467
  for strategy in strategies:
242
- id = strategy.strategy_profile.strategy_id
243
- time_unit = strategy.strategy_profile.time_unit
468
+ strategy_id = strategy.strategy_profile.strategy_id
244
469
  interval = strategy.strategy_profile.interval
245
- current_time = start_date
246
-
247
- while current_time <= end_date:
248
- data.append({
249
- "id": id,
250
- 'run_time': current_time,
251
- })
252
-
253
- if TimeUnit.SECOND.equals(time_unit):
254
- current_time += timedelta(seconds=interval)
255
- elif TimeUnit.MINUTE.equals(time_unit):
256
- current_time += timedelta(minutes=interval)
257
- elif TimeUnit.HOUR.equals(time_unit):
258
- current_time += timedelta(hours=interval)
259
- elif TimeUnit.DAY.equals(time_unit):
260
- current_time += timedelta(days=interval)
261
- else:
262
- raise ValueError(f"Unsupported time unit: {time_unit}")
263
-
264
- schedule_df = pd.DataFrame(data)
265
-
266
- if schedule_df.empty:
267
- raise OperationalException(
268
- "Could not generate schedule "
269
- "for backtest, do you have a strategy "
270
- "registered for your algorithm?"
271
- )
470
+ time_unit = strategy.strategy_profile.time_unit
272
471
 
273
- schedule_df.sort_values(by='run_time', inplace=True)
274
- schedule_df.set_index('run_time', inplace=True)
275
- 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
+ }
276
495
 
277
496
  def get_strategy_from_strategy_profiles(self, strategy_profiles, id):
278
497
 
@@ -283,309 +502,150 @@ class BacktestService(Observable):
283
502
 
284
503
  raise ValueError(f"Strategy profile with id {id} not found.")
285
504
 
286
- def create_backtest_report(
287
- self,
288
- algorithm,
289
- context,
290
- number_of_runs,
291
- backtest_date_range: BacktestDateRange,
292
- initial_unallocated=0
293
- ) -> BacktestResult:
505
+ def _get_initial_unallocated(self) -> float:
294
506
  """
295
- Create a backtest report for the given algorithm. This function
296
- will create a backtest report for the given algorithm and return
297
- the backtest report instance.
298
-
299
- It will calculate various performance metrics for the backtest.
300
- Also, it will add all traces to the backtest report. The traces
301
- are collected from each strategy that was run during the backtest.
302
-
303
- Args:
304
- algorithm: The algorithm to create the backtest report for
305
- number_of_runs: The number of runs
306
- backtest_date_range: The backtest date range of the backtest
307
- initial_unallocated: The initial unallocated amount
507
+ Get the initial unallocated amount for the backtest.
308
508
 
309
509
  Returns:
310
- BacktestResult: The backtest report instance of BacktestResult
510
+ float: The initial unallocated amount.
311
511
  """
512
+ portfolios = self._portfolio_service.get_all()
513
+ initial_unallocated = 0.0
312
514
 
313
- for portfolio in self._portfolio_service.get_all():
314
- ids = [strategy.strategy_id for strategy in algorithm.strategies]
315
-
316
- # Check if strategy_id is None
317
- if None in ids:
318
- # Remove None from ids
319
- ids = [x for x in ids if x is not None]
320
-
321
- positions = self._position_repository.get_all({
322
- "portfolio": portfolio.id
323
- })
324
- tickers = {}
325
-
326
- for position in positions:
327
-
328
- if position.symbol != portfolio.trading_symbol:
329
- ticker_symbol = \
330
- f"{position.symbol}/{portfolio.trading_symbol}"
331
-
332
- if not self._market_data_source_service\
333
- .has_ticker_market_data_source(
334
- symbol=ticker_symbol, market=portfolio.market
335
- ):
336
- raise OperationalException(
337
- f"Ticker market data source for "
338
- f"symbol {ticker_symbol} and market "
339
- f"{portfolio.market} not found, please make "
340
- f"sure you register a ticker market data "
341
- f"source for this symbol and market in "
342
- f"backtest mode. Otherwise, the backtest "
343
- f"report cannot be generated."
344
- )
345
- tickers[ticker_symbol] = \
346
- self._market_data_source_service.get_ticker(
347
- f"{position.symbol}/{portfolio.trading_symbol}",
348
- market=portfolio.market
349
- )
350
-
351
- positions = self._position_repository.get_all({
352
- "portfolio": portfolio.id
353
- })
354
-
355
- # Create the last snapshot of the portfolio
356
- self._portfolio_snapshot_service.create_snapshot(
357
- portfolio=portfolio,
358
- created_at=backtest_date_range.end_date
359
- )
360
-
361
- backtest_report = BacktestResult(
362
- name=algorithm.name,
363
- backtest_date_range=backtest_date_range,
364
- initial_unallocated=initial_unallocated,
365
- trading_symbol=portfolio.trading_symbol,
366
- created_at=datetime.now(tz=timezone.utc),
367
- portfolio_snapshots=self._portfolio_snapshot_service.get_all(
368
- {"portfolio_id": portfolio.id}
369
- ),
370
- number_of_runs=number_of_runs,
371
- trades=self._trade_service.get_all(
372
- {"portfolio": portfolio.id}
373
- ),
374
- orders=self._order_service.get_all(
375
- {"portfolio": portfolio.id}
376
- ),
377
- positions=self._position_repository.get_all(
378
- {"portfolio": portfolio.id}
379
- ),
380
- )
381
-
382
- # Calculate metrics for the backtest report
383
- return backtest_report
384
-
385
- def set_backtest_market_data_sources(self, market_data_sources):
386
- self._backtest_market_data_sources = market_data_sources
387
-
388
- def get_backtest_market_data_sources(self):
389
- return self._backtest_market_data_sources
390
-
391
- def get_backtest_market_data_source(self, symbol, market):
392
-
393
- for market_data_source in self._backtest_market_data_sources:
394
- if market_data_source.symbol == symbol \
395
- and market_data_source.market == market:
396
- return market_data_source
397
- raise OperationalException(
398
- f"Market data source for "
399
- f"symbol {symbol} and market {market} not found"
400
- )
401
-
402
- def _check_if_required_market_data_sources_are_registered(self):
403
- """
404
- Check if the required market data sources are registered.
515
+ for portfolio in portfolios:
516
+ initial_unallocated += portfolio.initial_balance
405
517
 
406
- It will iterate over all registered symbols and markets and check
407
- if a ticker market data source is registered for the symbol and market.
408
- """
409
- symbols = self._configuration_service.config[SYMBOLS]
410
-
411
- if symbols is not None:
412
-
413
- for symbol in symbols:
414
- if not self._market_data_source_service\
415
- .has_ticker_market_data_source(
416
- symbol=symbol
417
- ):
418
- raise OperationalException(
419
- f"Ticker market data source for symbol {symbol} not "
420
- f"found, please make sure you register a ticker "
421
- f"market data source for this symbol in backtest "
422
- f"mode. Otherwise, the backtest report "
423
- f"cannot be generated."
424
- )
518
+ return initial_unallocated
425
519
 
426
- def get_report(
520
+ def create_backtest(
427
521
  self,
428
- algorithm_name: str,
522
+ algorithm,
523
+ number_of_runs,
429
524
  backtest_date_range: BacktestDateRange,
430
- directory: str
431
- ) -> BacktestResult:
432
- """
433
- Function to get a report based on the algorithm name and
434
- backtest date range if it exists.
435
-
436
- Args:
437
- algorithm_name: str - The name of the algorithm
438
- backtest_date_range: BacktestDateRange - The backtest date range
439
- directory: str - The output directory
440
-
441
- Returns:
442
- BacktestResult - The backtest report if it exists, otherwise None
525
+ risk_free_rate,
526
+ strategy_directory_path=None
527
+ ) -> Backtest:
443
528
  """
529
+ Create a backtest for the given algorithm.
444
530
 
445
- # Loop through all files in the output directory
446
- for root, _, files in os.walk(directory):
447
- for file in files:
448
- # Check if the file contains the algorithm name
449
- # and backtest date range
450
- if self._is_backtest_report(os.path.join(root, file)):
451
- # Read the file
452
- with open(os.path.join(root, file), "r") as json_file:
453
-
454
- name = \
455
- self._get_algorithm_name_from_backtest_report_file(
456
- os.path.join(root, file)
457
- )
458
-
459
- if name == algorithm_name:
460
- backtest_start_date = \
461
- self._get_start_date_from_backtest_report_file(
462
- os.path.join(root, file)
463
- )
464
- backtest_end_date = \
465
- self._get_end_date_from_backtest_report_file(
466
- os.path.join(root, file)
467
- )
468
-
469
- if backtest_start_date == \
470
- backtest_date_range.start_date \
471
- and backtest_end_date == \
472
- backtest_date_range.end_date:
473
- # Parse the JSON file
474
- report = json.load(json_file)
475
- # Convert the JSON file to a
476
- # BacktestResult object
477
- return BacktestResult.from_dict(report)
478
-
479
- return None
480
-
481
- def _get_start_date_from_backtest_report_file(self, path: str) -> datetime:
482
- """
483
- Function to get the backtest start date from a backtest report file.
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.
484
535
 
485
536
  Args:
486
- path: str - The path to the backtest report file
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
487
543
 
488
544
  Returns:
489
- datetime - The backtest start date
545
+ Backtest: The backtest containing the results and metrics.
490
546
  """
491
547
 
492
- # Get the backtest start date from the file name
493
- backtest_start_date = os.path.basename(path).split("_")[3]
494
-
495
- try:
496
- # Parse the backtest start date
497
- return datetime.strptime(
498
- backtest_start_date, DATETIME_FORMAT_BACKTESTING
499
- )
500
- except ValueError:
501
- # Try to parse the backtest start date with a different format
502
- return parser.parse(backtest_start_date)
548
+ # Get the first portfolio
549
+ portfolio = self._portfolio_service.get_all()[0]
503
550
 
504
- def _get_end_date_from_backtest_report_file(self, path: str) -> datetime:
505
- """
506
- Function to get the backtest end date from a backtest report file.
551
+ # List all strategy related files in the strategy directory
552
+ strategy_related_paths = []
507
553
 
508
- Args:
509
- path: str - The path to the backtest report file
510
-
511
- Returns:
512
- datetime - The backtest end date
513
- """
514
-
515
- # Get the backtest end date from the file name
516
- backtest_end_date = os.path.basename(path).split("_")[5]
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
+ )
517
560
 
518
- try:
519
- # Parse the backtest end date
520
- return datetime.strptime(
521
- backtest_end_date, DATETIME_FORMAT_BACKTESTING
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]
522
608
  )
523
- except ValueError:
524
- # Try to parse the backtest end date with a different format
525
- return parser.parse(backtest_end_date)
526
-
527
- def _get_algorithm_name_from_backtest_report_file(self, path: str) -> str:
528
- """
529
- Function to get the algorithm name from a backtest report file.
530
-
531
- Args:
532
- path: str - The path to the backtest report file
533
-
534
- Returns:
535
- str - The algorithm name
536
- """
537
- # Get the word between "report_" and "_backtest_start_date"
538
- # it can contain _
539
- # Get the algorithm name from the file name
540
- algorithm_name = os.path.basename(path).split("_")[1]
541
- return algorithm_name
609
+ )
542
610
 
543
- def _is_backtest_report(self, path: str) -> bool:
611
+ @staticmethod
612
+ def get_most_granular_ohlcv_data_source(data_sources):
544
613
  """
545
- Function to check if a file is a backtest report file.
614
+ Get the most granular data source from a list of data sources.
546
615
 
547
616
  Args:
548
- path: str - The path to the file
617
+ data_sources: List of data sources.
549
618
 
550
619
  Returns:
551
- bool - True if the file is a backtest report file, otherwise False
620
+ The most granular data source.
552
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
+ }
553
634
 
554
- # Check if the file is a JSON file
555
- if path.endswith(".json"):
635
+ most_granular = None
636
+ highest_granularity = float('inf')
556
637
 
557
- # Check if the file name matches the backtest
558
- # report file name pattern
559
- if re.match(
560
- BACKTEST_REPORT_FILE_NAME_PATTERN, os.path.basename(path)
561
- ):
562
- return True
638
+ ohlcv_data_sources = [
639
+ ds for ds in data_sources if DataType.OHLCV.equals(ds.data_type)
640
+ ]
563
641
 
564
- return False
642
+ if len(ohlcv_data_sources) == 0:
643
+ raise OperationalException("No OHLCV data sources found")
565
644
 
566
- @staticmethod
567
- def create_report_directory_name(report) -> str:
568
- """
569
- Function to create a directory name for a backtest report.
570
- The directory name will be automatically generated based on the
571
- algorithm name and creation date.
645
+ for source in ohlcv_data_sources:
572
646
 
573
- Args:
574
- report: BacktestResult - The backtest report to create a
575
- directory for.
647
+ if granularity_order[source.time_frame] < highest_granularity:
648
+ highest_granularity = granularity_order[source.time_frame]
649
+ most_granular = source
576
650
 
577
- Returns:
578
- directory_name: str The directory name for the
579
- backtest report file.
580
- """
581
- created_at = report.results\
582
- .created_at.strftime(DATETIME_FORMAT_BACKTESTING)
583
- backtest_start_date = report.results.backtest_date_range.start_date
584
- backtest_end_date = report.results.backtest_date_range.end_date
585
- name = report.results.name
586
-
587
- start_date = backtest_start_date.strftime(DATETIME_FORMAT_BACKTESTING)
588
- end_date = backtest_end_date.strftime(DATETIME_FORMAT_BACKTESTING)
589
- directory_name = f"report_{name}_backtest-start-date_" \
590
- f"{start_date}_backtest-end-date_{end_date}_{created_at}"
591
- return directory_name
651
+ return most_granular