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
@@ -2,14 +2,12 @@ import inspect
2
2
  import logging
3
3
  import os
4
4
  import threading
5
- from time import sleep
6
- from typing import List, Optional, Any
5
+ from datetime import datetime, timezone, timedelta
6
+ from typing import List, Optional, Any, Dict, Tuple
7
7
 
8
8
  from flask import Flask
9
9
 
10
- from investing_algorithm_framework.app.algorithm import Algorithm, \
11
- AlgorithmFactory
12
- from investing_algorithm_framework.app.stateless import ActionHandler
10
+ from investing_algorithm_framework.app.algorithm import Algorithm
13
11
  from investing_algorithm_framework.app.strategy import TradingStrategy
14
12
  from investing_algorithm_framework.app.task import Task
15
13
  from investing_algorithm_framework.app.web import create_flask_app
@@ -17,17 +15,23 @@ from investing_algorithm_framework.domain import DATABASE_NAME, TimeUnit, \
17
15
  DATABASE_DIRECTORY_PATH, RESOURCE_DIRECTORY, ENVIRONMENT, Environment, \
18
16
  SQLALCHEMY_DATABASE_URI, OperationalException, StateHandler, \
19
17
  BACKTESTING_START_DATE, BACKTESTING_END_DATE, APP_MODE, MarketCredential, \
20
- AppMode, BacktestDateRange, DATABASE_DIRECTORY_NAME, \
21
- BACKTESTING_INITIAL_AMOUNT, SNAPSHOT_INTERVAL, \
22
- MarketDataSource, PortfolioConfiguration, SnapshotInterval, \
23
- PortfolioProvider, OrderExecutor, ImproperlyConfigured
18
+ AppMode, BacktestDateRange, DATABASE_DIRECTORY_NAME, DataSource, \
19
+ BACKTESTING_INITIAL_AMOUNT, SNAPSHOT_INTERVAL, Backtest, DataError, \
20
+ PortfolioConfiguration, SnapshotInterval, DataType, combine_backtests, \
21
+ PortfolioProvider, OrderExecutor, ImproperlyConfigured, TimeFrame, \
22
+ DataProvider, INDEX_DATETIME, tqdm, BacktestPermutationTest, \
23
+ LAST_SNAPSHOT_DATETIME, BACKTESTING_FLAG, generate_backtest_summary_metrics
24
24
  from investing_algorithm_framework.infrastructure import setup_sqlalchemy, \
25
- create_all_tables, CCXTOrderExecutor, CCXTPortfolioProvider
25
+ create_all_tables, CCXTOrderExecutor, CCXTPortfolioProvider, \
26
+ BacktestOrderExecutor, CCXTOHLCVDataProvider, clear_db, \
27
+ PandasOHLCVDataProvider
26
28
  from investing_algorithm_framework.services import OrderBacktestService, \
27
- BacktestMarketDataSourceService, BacktestPortfolioService, \
28
- BacktestService
29
+ BacktestPortfolioService, BacktestTradeOrderEvaluator, \
30
+ DefaultTradeOrderEvaluator, get_risk_free_rate_us
29
31
  from .app_hook import AppHook
30
- from .reporting import BacktestReport
32
+ from .eventloop import EventLoopService
33
+ from .analysis import create_ohlcv_permutation
34
+
31
35
 
32
36
  logger = logging.getLogger("investing_algorithm_framework")
33
37
  COLOR_RESET = '\033[0m'
@@ -43,8 +47,6 @@ class App:
43
47
  Attributes:
44
48
  container: The dependency container for the app. This is used
45
49
  to store all the services and repositories for the app.
46
- algorithm: The algorithm to run. This is used to run the
47
- trading bot.
48
50
  _flask_app: The flask app instance. This is used to run the
49
51
  web app.
50
52
  _state_handler: The state handler for the app. This is used
@@ -55,9 +57,6 @@ class App:
55
57
  started or not.
56
58
  _tasks (List[Task]): List of task that need to be run by the
57
59
  application.
58
- _algorithm (Algorithm): The algorithm instance. An algorithm is a
59
- bundle of tasks and strategies. The algorithm is only
60
- initialized when the application in started.
61
60
  """
62
61
 
63
62
  def __init__(self, state_handler=None, name=None):
@@ -66,12 +65,12 @@ class App:
66
65
  self._started = False
67
66
  self._tasks = []
68
67
  self._strategies = []
69
- self._market_data_sources = []
68
+ self._data_providers: List[Tuple[DataProvider, int]] = []
70
69
  self._on_initialize_hooks = []
71
70
  self._on_strategy_run_hooks = []
72
71
  self._on_after_initialize_hooks = []
72
+ self._trade_order_evaluator = None
73
73
  self._state_handler = state_handler
74
- self._strategy_orchestrator_service = None
75
74
  self._run_history = None
76
75
  self._name = name
77
76
 
@@ -79,6 +78,56 @@ class App:
79
78
  def context(self):
80
79
  return self.container.context()
81
80
 
81
+ @property
82
+ def resource_directory_path(self):
83
+ """
84
+ Returns the resource directory path from the configuration.
85
+ This directory is used to store resources such as market data,
86
+ database files, and other resources required by the app.
87
+ """
88
+ config = self.config
89
+ resource_directory_path = config.get(RESOURCE_DIRECTORY, None)
90
+
91
+ # Check if the resource directory is set
92
+ if resource_directory_path is None:
93
+ logger.info(
94
+ "Resource directory not set, setting" +
95
+ " to current working directory"
96
+ )
97
+ resource_directory_path = os.path.join(os.getcwd(), "resources")
98
+ configuration_service = self.container.configuration_service()
99
+ configuration_service.add_value(
100
+ RESOURCE_DIRECTORY, resource_directory_path
101
+ )
102
+
103
+ return resource_directory_path
104
+
105
+ @property
106
+ def database_directory_path(self):
107
+ """
108
+ Returns the database directory path from the configuration.
109
+ This directory is used to store database files required by the app.
110
+ """
111
+ config = self.config
112
+ database_directory_path = config.get(DATABASE_DIRECTORY_PATH, None)
113
+
114
+ # Check if the database directory is set
115
+ if database_directory_path is None:
116
+ logger.info(
117
+ "Database directory not set, setting" +
118
+ " to current working directory"
119
+ )
120
+ resource_directory_path = self.resource_directory_path
121
+ database_directory_path = os.path.join(
122
+ resource_directory_path, "databases"
123
+ )
124
+ configuration_service = self.container.configuration_service()
125
+ configuration_service.add_value(
126
+ DATABASE_DIRECTORY_PATH, database_directory_path
127
+ )
128
+
129
+ return database_directory_path
130
+
82
131
  @property
83
132
  def name(self):
84
133
  return self._name
@@ -131,9 +180,22 @@ class App:
131
180
  None
132
181
  """
133
182
  self.add_strategies(algorithm.strategies)
134
- self.add_data_sources(algorithm.data_sources)
135
183
  self.add_tasks(algorithm.tasks)
136
184
 
185
+ def add_trade_order_evaluator(self, trade_order_evaluator):
186
+ """
187
+ Function to add a trade order evaluator to the app. This is used
188
+ to evaluate trades and orders based on OHLCV data.
189
+
190
+ Args:
191
+ trade_order_evaluator: The trade order evaluator to add to the app.
192
+ This should be an instance of TradeOrderEvaluator.
193
+
194
+ Returns:
195
+ None
196
+ """
197
+ self._trade_order_evaluator = trade_order_evaluator
198
+
137
199
  def set_config(self, key: str, value: Any) -> None:
138
200
  """
139
201
  Function to add a key-value pair to the app's configuration.
@@ -148,55 +210,276 @@ class App:
148
210
  configuration_service = self.container.configuration_service()
149
211
  configuration_service.add_value(key, value)
150
212
 
151
- def set_config_with_dict(self, dictionary) -> None:
213
+ def set_config_with_dict(self, config: dict) -> None:
152
214
  """
153
- Function to add a dictionary to the app's configuration.
154
- This method is useful for adding multiple configuration values
215
+ Function to set the configuration for the app with a dictionary.
216
+ This is useful for setting multiple configuration values at once.
155
217
 
156
218
  Args:
157
- dictionary (Dict): A dictionary containing key-value pairs
158
- to add to the configuration
219
+ config (dict): A dictionary containing the configuration
159
220
 
160
221
  Returns:
161
222
  None
162
223
  """
163
224
  configuration_service = self.container.configuration_service()
164
- configuration_service.add_dict(dictionary)
225
+ configuration_service.initialize_from_dict(config)
165
226
 
166
- def initialize_services(self) -> None:
227
+ def initialize_config(self):
167
228
  """
168
- Method to initialize the services for the app. This method should
169
- be called before running the application. This method initializes
170
- all services so that they are ready to be used.
229
+ Function to initialize the configuration for the app. This method
230
+ should be called before running the algorithm.
171
231
 
172
232
  Returns:
173
233
  None
174
234
  """
175
- self._initialize_default_order_executors()
176
- self._initialize_default_portfolio_providers()
235
+ data = {
236
+ ENVIRONMENT: self.config.get(ENVIRONMENT, Environment.PROD.value),
237
+ DATABASE_DIRECTORY_NAME: "databases",
238
+ LAST_SNAPSHOT_DATETIME: None
239
+ }
240
+ configuration_service = self.container.configuration_service()
241
+ configuration_service.initialize_from_dict(data)
242
+ config = configuration_service.get_config()
177
243
 
178
- # Initialize all market credentials
179
- market_credential_service = self.container.market_credential_service()
180
- market_credential_service.initialize()
244
+ if INDEX_DATETIME not in config or config[INDEX_DATETIME] is None:
245
+ configuration_service.add_value(
246
+ INDEX_DATETIME, datetime.now(timezone.utc)
247
+ )
248
+
249
+ if Environment.TEST.equals(config[ENVIRONMENT]):
250
+ configuration_service.add_value(
251
+ DATABASE_NAME, "test-database.sqlite3"
252
+ )
253
+ elif Environment.PROD.equals(config[ENVIRONMENT]):
254
+ configuration_service.add_value(
255
+ DATABASE_NAME, "prod-database.sqlite3"
256
+ )
257
+ else:
258
+ configuration_service.add_value(
259
+ DATABASE_NAME, "dev-database.sqlite3"
260
+ )
261
+
262
+ resource_dir = config[RESOURCE_DIRECTORY]
263
+ database_dir_name = config.get(DATABASE_DIRECTORY_NAME)
264
+ configuration_service.add_value(
265
+ DATABASE_DIRECTORY_PATH,
266
+ os.path.join(resource_dir, database_dir_name)
267
+ )
268
+ config = configuration_service.get_config()
269
+
270
+ if SQLALCHEMY_DATABASE_URI not in config \
271
+ or config[SQLALCHEMY_DATABASE_URI] is None:
272
+ path = "sqlite:///" + os.path.join(
273
+ configuration_service.config[DATABASE_DIRECTORY_PATH],
274
+ configuration_service.config[DATABASE_NAME]
275
+ )
276
+ configuration_service.add_value(SQLALCHEMY_DATABASE_URI, path)
277
+
278
+ def initialize_backtest_config(
279
+ self,
280
+ backtest_date_range: BacktestDateRange,
281
+ initial_amount=None,
282
+ snapshot_interval: SnapshotInterval = None
283
+ ):
284
+ """
285
+ Function to initialize the configuration for the app in backtest mode.
286
+ This method should be called before running the algorithm in backtest
287
+ mode. It sets the environment to BACKTEST and initializes the
288
+ configuration accordingly.
289
+
290
+ Args:
291
+ backtest_date_range (BacktestDateRange): The date range for the
292
+ backtest. This should be an instance of BacktestDateRange.
293
+ initial_amount (float): The initial amount to start the backtest
294
+ with. This will be the amount of trading currency that the
295
+ backtest portfolio will start with.
296
+ snapshot_interval (SnapshotInterval): The snapshot interval to
297
+ use for the backtest. This is used to determine how often the
298
+ portfolio snapshot should be taken during the backtest.
299
+
300
+ Returns:
301
+ None
302
+ """
303
+ logger.info("Initializing backtest configuration")
304
+ data = {
305
+ ENVIRONMENT: Environment.BACKTEST.value,
306
+ BACKTESTING_START_DATE: backtest_date_range.start_date,
307
+ BACKTESTING_END_DATE: backtest_date_range.end_date,
308
+ DATABASE_NAME: "backtest-database.sqlite3",
309
+ DATABASE_DIRECTORY_NAME: "backtest_databases",
310
+ DATABASE_DIRECTORY_PATH: os.path.join(
311
+ self.resource_directory_path,
312
+ "backtest_databases"
313
+ ),
314
+ BACKTESTING_INITIAL_AMOUNT: initial_amount,
315
+ INDEX_DATETIME: backtest_date_range.start_date,
316
+ LAST_SNAPSHOT_DATETIME: None,
317
+ BACKTESTING_FLAG: True
318
+ }
319
+ configuration_service = self.container.configuration_service()
320
+ configuration_service.initialize_from_dict(data)
321
+
322
+ if snapshot_interval is not None:
323
+ configuration_service.add_value(
324
+ SNAPSHOT_INTERVAL,
325
+ SnapshotInterval.from_value(snapshot_interval).value
326
+ )
327
+
328
+ def initialize_storage(self, remove_database_if_exists: bool = False):
329
+ """
330
+ Function to initialize the storage for the app. The given
331
+ resource directory will be created if it does not exist.
332
+ The database directory will also be created if it does not
333
+ exist.
334
+ """
335
+ resource_directory_path = self.resource_directory_path
336
+
337
+ if not os.path.exists(resource_directory_path):
338
+ os.makedirs(resource_directory_path)
339
+ logger.info(
340
+ f"Resource directory created at {resource_directory_path}"
341
+ )
342
+
343
+ database_directory_path = self.database_directory_path
344
+
345
+ if not os.path.exists(database_directory_path):
346
+ os.makedirs(database_directory_path)
347
+ logger.info(
348
+ f"Database directory created at {database_directory_path}"
349
+ )
350
+
351
+ database_path = os.path.join(
352
+ database_directory_path, self.config[DATABASE_NAME]
353
+ )
354
+
355
+ if remove_database_if_exists:
356
+
357
+ if os.path.exists(database_path):
358
+ logger.info(
359
+ f"Removing existing database at {database_path}"
360
+ )
361
+ os.remove(database_path)
362
+
363
+ # Create the sqlalchemy database uri
364
+ path = f"sqlite:///{database_path}"
365
+ self.set_config(SQLALCHEMY_DATABASE_URI, path)
366
+
367
+ # Setup sql if needed
368
+ setup_sqlalchemy(self)
369
+ create_all_tables()
370
+
371
+ def initialize_data_sources(
372
+ self,
373
+ data_sources: List[DataSource],
374
+ ):
375
+ """
376
+ Function to initialize the data sources for the app. This method
377
+ should be called before running the algorithm. This method
378
+ initializes all data sources so that they are ready to be used.
379
+
380
+ Args:
381
+ data_sources (List[DataSource]): The data sources to initialize.
382
+ This should be a list of DataSource instances.
383
+
384
+ Returns:
385
+ None
386
+ """
387
+ logger.info("Initializing data sources")
388
+
389
+ if data_sources is None or len(data_sources) == 0:
390
+ return
391
+
392
+ data_provider_service = self.container.data_provider_service()
393
+ data_provider_service.reset()
394
+
395
+ for data_provider_tuple in self._data_providers:
396
+ data_provider_service.add_data_provider(
397
+ data_provider_tuple[0], priority=data_provider_tuple[1]
398
+ )
399
+
400
+ # Add the default data providers
401
+ data_provider_service.add_data_provider(CCXTOHLCVDataProvider())
402
+
403
+ # Initialize all data sources
404
+ data_provider_service.index_data_providers(data_sources)
405
+
406
+ def initialize_data_sources_backtest(
407
+ self,
408
+ data_sources: List[DataSource],
409
+ backtest_date_range: BacktestDateRange,
410
+ show_progress: bool = True
411
+ ):
412
+ """
413
+ Function to initialize the data sources for the app in backtest mode.
414
+ This method should be called before running the algorithm in backtest
415
+ mode. It initializes all data sources so that they are
416
+ ready to be used.
417
+
418
+ Args:
419
+ data_sources (List[DataSource]): The data sources to initialize.
420
+ backtest_date_range (BacktestDateRange): The date range for the
421
+ backtest. This should be an instance of BacktestDateRange.
422
+ show_progress (bool): Whether to show a progress bar when
423
+ preparing the backtest data for each data provider.
181
424
 
182
- def initialize_services_backtest(self) -> None:
425
+ Returns:
426
+ None
183
427
  """
184
- Method to initialize the services for the app in backtest mode.
185
- This method should be called before running the application.
186
- This method initializes all services so that they are ready to
187
- be used.
428
+ logger.info("Initializing data sources for backtest")
429
+
430
+ if data_sources is None or len(data_sources) == 0:
431
+ return
432
+
433
+ data_provider_service = self.container.data_provider_service()
434
+ data_provider_service.reset()
435
+
436
+ for data_provider_tuple in self._data_providers:
437
+ data_provider_service.add_data_provider(
438
+ data_provider_tuple[0], priority=data_provider_tuple[1]
439
+ )
440
+
441
+ # Add the default data providers
442
+ data_provider_service.add_data_provider(CCXTOHLCVDataProvider())
443
+
444
+ # Initialize all data sources
445
+ data_provider_service.index_backtest_data_providers(
446
+ data_sources, backtest_date_range, show_progress=show_progress
447
+ )
448
+
449
+ description = "Preparing backtest data for all data sources"
450
+ data_providers = data_provider_service.data_provider_index.get_all()
451
+
452
+ # Prepare the backtest data for each data provider
453
+ if not show_progress:
454
+ for _, data_provider in data_providers:
188
455
 
189
- The method overrides the portfolio service, market data source
190
- service and order service with the backtest equivalents.
456
+ data_provider.prepare_backtest_data(
457
+ backtest_start_date=backtest_date_range.start_date,
458
+ backtest_end_date=backtest_date_range.end_date
459
+ )
460
+ else:
461
+ for _, data_provider in \
462
+ tqdm(
463
+ data_providers, desc=description, colour="green"
464
+ ):
465
+
466
+ data_provider.prepare_backtest_data(
467
+ backtest_start_date=backtest_date_range.start_date,
468
+ backtest_end_date=backtest_date_range.end_date
469
+ )
191
470
 
192
- App hooks are not added when running in backtest mode.
471
+ def initialize_backtest_services(self):
472
+ """
473
+ Function to initialize the backtest services for the app. This method
474
+ should be called before running the algorithm in backtest mode.
475
+ It initializes the backtest services so that they are ready to be used.
193
476
 
194
477
  Returns:
195
478
  None
196
479
  """
197
480
  configuration_service = self.container.configuration_service()
198
- self._initialize_default_order_executors()
199
- self._initialize_default_portfolio_providers()
481
+ self.initialize_order_executors()
482
+ self.initialize_portfolio_providers()
200
483
  portfolio_conf_service = self.container \
201
484
  .portfolio_configuration_service()
202
485
  portfolio_snap_service = self.container \
@@ -218,25 +501,11 @@ class App:
218
501
  )
219
502
  )
220
503
 
221
- # Override the market data source service with the backtest market
222
- # data source service
223
- self.container.market_data_source_service.override(
224
- BacktestMarketDataSourceService(
225
- market_service=self.container.market_service(),
226
- market_credential_service=self.container
227
- .market_credential_service(),
228
- configuration_service=self.container
229
- .configuration_service(),
230
- )
231
- )
232
-
233
504
  portfolio_conf_service = self.container. \
234
505
  portfolio_configuration_service()
235
506
  portfolio_snap_service = self.container. \
236
507
  portfolio_snapshot_service()
237
508
  configuration_service = self.container.configuration_service()
238
- market_data_source_service = self.container. \
239
- market_data_source_service()
240
509
  # Override the order service with the backtest order service
241
510
  self.container.order_service.override(
242
511
  OrderBacktestService(
@@ -247,107 +516,10 @@ class App:
247
516
  portfolio_configuration_service=portfolio_conf_service,
248
517
  portfolio_snapshot_service=portfolio_snap_service,
249
518
  configuration_service=configuration_service,
250
- market_data_source_service=market_data_source_service
251
519
  )
252
520
  )
253
521
 
254
- def initialize_config(self):
255
- """
256
- Function to initialize the configuration for the app. This method
257
- should be called before running the algorithm.
258
- """
259
- logger.info("Initializing configuration")
260
- configuration_service = self.container.configuration_service()
261
- config = configuration_service.get_config()
262
-
263
- # Check if the resource directory is set
264
- if RESOURCE_DIRECTORY not in config \
265
- or config[RESOURCE_DIRECTORY] is None:
266
- logger.info(
267
- "Resource directory not set, setting" +
268
- " to current working directory"
269
- )
270
- path = os.path.join(os.getcwd(), "resources")
271
- configuration_service.add_value(RESOURCE_DIRECTORY, path)
272
-
273
- config = configuration_service.get_config()
274
- logger.info(f"Resource directory set to {config[RESOURCE_DIRECTORY]}")
275
-
276
- if DATABASE_NAME not in config or config[DATABASE_NAME] is None:
277
- configuration_service.add_value(
278
- DATABASE_NAME, "prod-database.sqlite3"
279
- )
280
-
281
- # Set the database directory name
282
- if Environment.BACKTEST.equals(config[ENVIRONMENT]):
283
- configuration_service.add_value(
284
- DATABASE_DIRECTORY_NAME, "backtest_databases"
285
- )
286
- configuration_service.add_value(
287
- DATABASE_NAME, "backtest-database.sqlite3"
288
- )
289
- else:
290
- configuration_service.add_value(
291
- DATABASE_DIRECTORY_NAME, "databases"
292
- )
293
-
294
- if Environment.TEST.equals(config[ENVIRONMENT]):
295
- configuration_service.add_value(
296
- DATABASE_NAME, "test-database.sqlite3"
297
- )
298
- elif Environment.PROD.equals(config[ENVIRONMENT]):
299
- configuration_service.add_value(
300
- DATABASE_NAME, "prod-database.sqlite3"
301
- )
302
- else:
303
- configuration_service.add_value(
304
- DATABASE_NAME, "dev-database.sqlite3"
305
- )
306
-
307
- config = configuration_service.get_config()
308
- resource_dir = config[RESOURCE_DIRECTORY]
309
- database_dir_name = config.get(DATABASE_DIRECTORY_NAME)
310
- configuration_service.add_value(
311
- DATABASE_DIRECTORY_PATH,
312
- os.path.join(resource_dir, database_dir_name)
313
- )
314
- config = configuration_service.get_config()
315
-
316
- if SQLALCHEMY_DATABASE_URI not in config \
317
- or config[SQLALCHEMY_DATABASE_URI] is None:
318
- path = "sqlite:///" + os.path.join(
319
- configuration_service.config[DATABASE_DIRECTORY_PATH],
320
- configuration_service.config[DATABASE_NAME]
321
- )
322
- configuration_service.add_value(SQLALCHEMY_DATABASE_URI, path)
323
-
324
- config = configuration_service.get_config()
325
-
326
- if APP_MODE not in config:
327
- configuration_service.add_value(APP_MODE, AppMode.DEFAULT.value)
328
-
329
- def initialize_data_sources(self, algorithm):
330
- """
331
- Function to initialize the data sources for the app. This method
332
- should be called before running the algorithm. This method
333
- initializes all data sources so that they are ready to be used.
334
-
335
- Returns:
336
- None
337
- """
338
- logger.info("Initializing data sources")
339
- market_data_source_service = self.container \
340
- .market_data_source_service()
341
- market_data_source_service.clear_market_data_sources()
342
-
343
- # Add all market data sources of the strategies to the market
344
- # data source service
345
- market_data_source_service.market_data_sources = algorithm.data_sources
346
-
347
- # Initialize the market data source service
348
- market_data_source_service.initialize_market_data_sources()
349
-
350
- def initialize(self):
522
+ def initialize_services(self):
351
523
  """
352
524
  Method to initialize the app. This method should be called before
353
525
  running the algorithm. It initializes the services and the algorithm
@@ -359,44 +531,26 @@ class App:
359
531
  None
360
532
  """
361
533
  logger.info("Initializing app")
534
+ self.initialize_order_executors()
535
+ self.initialize_portfolio_providers()
362
536
 
363
- # Ensure that all resource directories exist
364
- self._create_resources_if_not_exists()
365
-
366
- # Set up the database
367
- setup_sqlalchemy(self)
368
- create_all_tables()
369
-
370
- # Check if environment is in backtest mode
371
- config = self.container.configuration_service().get_config()
372
-
373
- # Initialize services in backtest
374
- if Environment.BACKTEST.equals(config[ENVIRONMENT]):
375
- self.initialize_services_backtest()
376
- else:
377
- self.initialize_services()
378
-
537
+ # Initialize all market credentials
538
+ market_credential_service = self.container.market_credential_service()
539
+ market_credential_service.initialize()
379
540
  portfolio_configuration_service = self.container \
380
541
  .portfolio_configuration_service()
381
542
 
382
- # Re-init the market service because the portfolio configuration
383
- # service is a singleton
384
- portfolio_configuration_service.market_service \
385
- = self.container.market_service()
386
-
387
543
  if portfolio_configuration_service.count() == 0:
388
544
  raise OperationalException("No portfolios configured")
389
545
 
390
546
  configuration_service = self.container.configuration_service()
547
+ config = configuration_service.get_config()
391
548
 
392
- if config[APP_MODE] == AppMode.WEB.value:
549
+ if AppMode.WEB.equals(config[APP_MODE]):
393
550
  configuration_service.add_value(APP_MODE, AppMode.WEB.value)
394
551
  self._initialize_web()
395
552
 
396
- if not Environment.BACKTEST.equals(config[ENVIRONMENT]):
397
- self._initialize_portfolios()
398
-
399
- def run(self, payload: dict = None, number_of_iterations: int = None):
553
+ def run(self, number_of_iterations: int = None):
400
554
  """
401
555
  Entry point to run the application. This method should be called to
402
556
  start the trading bot. This method can be called in three modes:
@@ -421,27 +575,17 @@ class App:
421
575
  initializes the algorithm with the services and the configuration.
422
576
 
423
577
  Args:
424
- payload (dict): The payload to handle for the algorithm
425
578
  number_of_iterations (int): The number of iterations to run the
426
- algorithm for
579
+ algorithm for
427
580
 
428
581
  Returns:
429
582
  None
430
583
  """
431
- strategy_orchestrator_service = None
584
+ self.initialize_config()
585
+ self.initialize_storage()
586
+ event_loop_service = None
432
587
 
433
588
  try:
434
- configuration_service = self.container.configuration_service()
435
- config = configuration_service.get_config()
436
-
437
- # Run method should never be called with environment set to
438
- # backtest, if it is, then set the environment to prod
439
- if config[ENVIRONMENT] == Environment.BACKTEST.value:
440
- configuration_service.add_value(
441
- ENVIRONMENT, Environment.PROD.value
442
- )
443
-
444
- self.initialize_config()
445
589
 
446
590
  # Load the state if a state handler is provided
447
591
  if self._state_handler is not None:
@@ -450,44 +594,18 @@ class App:
450
594
  config = self.container.configuration_service().get_config()
451
595
  self._state_handler.load(config[RESOURCE_DIRECTORY])
452
596
 
453
- self.initialize()
454
597
  logger.info("App initialization complete")
455
598
 
456
599
  # Run all on_initialize hooks
457
600
  for hook in self._on_initialize_hooks:
458
601
  hook.on_run(self.context)
459
602
 
460
- configuration_service = self.container.configuration_service()
461
- config = configuration_service.get_config()
462
-
463
- algorithm_factory: AlgorithmFactory = \
464
- self.container.algorithm_factory()
465
- algorithm = algorithm_factory.create_algorithm(
466
- name=self._name,
467
- strategies=self._strategies,
468
- tasks=self._tasks,
469
- data_sources=self._market_data_sources,
470
- on_strategy_run_hooks=self._on_strategy_run_hooks,
471
- )
472
- self.initialize_data_sources(algorithm)
473
-
474
- strategy_orchestrator_service = \
475
- self.container.strategy_orchestrator_service()
476
- strategy_orchestrator_service.initialize(algorithm)
477
-
478
- # Run in payload mode if payload is provided
479
- if payload is not None:
480
- logger.info("Running with payload")
481
- context = self.container.context()
482
- action_handler = ActionHandler.of(payload)
483
- response = action_handler.handle(
484
- payload=payload,
485
- context=context,
486
- strategy_orchestrator_service=strategy_orchestrator_service
487
- )
488
- return response
603
+ algorithm = self.get_algorithm()
604
+ self.initialize_data_sources(algorithm.data_sources)
605
+ self.initialize_services()
606
+ self.initialize_portfolios()
489
607
 
490
- if AppMode.WEB.equals(config[APP_MODE]):
608
+ if AppMode.WEB.equals(self.config[APP_MODE]):
491
609
  logger.info("Running web")
492
610
  flask_thread = threading.Thread(
493
611
  name='Web App',
@@ -497,31 +615,44 @@ class App:
497
615
  flask_thread.daemon = True
498
616
  flask_thread.start()
499
617
 
500
- number_of_iterations_since_last_orders_check = 1
501
- strategy_orchestrator_service.start(
502
- context=self.context, number_of_iterations=number_of_iterations
618
+ trade_order_evaluator = DefaultTradeOrderEvaluator(
619
+ trade_service=self.container.trade_service(),
620
+ order_service=self.container.order_service(),
621
+ trade_stop_loss_service=self.container
622
+ .trade_stop_loss_service(),
623
+ trade_take_profit_service=self.container
624
+ .trade_take_profit_service(),
625
+ configuration_service=self.container.configuration_service()
626
+ )
627
+ event_loop_service = EventLoopService(
628
+ configuration_service=self.container.configuration_service(),
629
+ portfolio_snapshot_service=self.container
630
+ .portfolio_snapshot_service(),
631
+ context=self.context,
632
+ order_service=self.container.order_service(),
633
+ portfolio_service=self.container.portfolio_service(),
634
+ data_provider_service=self.container.data_provider_service(),
635
+ trade_service=self.container.trade_service(),
636
+ )
637
+ event_loop_service.initialize(
638
+ algorithm, trade_order_evaluator=trade_order_evaluator
503
639
  )
504
640
 
505
641
  try:
506
- while strategy_orchestrator_service.running:
507
- if number_of_iterations_since_last_orders_check == 30:
508
- logger.info("Checking pending orders")
509
- number_of_iterations_since_last_orders_check = 1
510
-
511
- strategy_orchestrator_service.run_pending_jobs()
512
- number_of_iterations_since_last_orders_check += 1
513
- sleep(1)
642
+ event_loop_service.start(
643
+ number_of_iterations=number_of_iterations
644
+ )
514
645
  except KeyboardInterrupt:
515
646
  exit(0)
516
647
  except Exception as e:
517
648
  logger.error(e)
518
649
  raise e
519
650
  finally:
520
- self._run_history = strategy_orchestrator_service.history
521
651
 
522
- try:
523
- strategy_orchestrator_service.stop()
652
+ if event_loop_service is not None:
653
+ self._run_history = event_loop_service.history
524
654
 
655
+ try:
525
656
  # Upload state if state handler is provided
526
657
  if self._state_handler is not None:
527
658
  logger.info("Detected state handler, saving state")
@@ -616,109 +747,540 @@ class App:
616
747
  parameters for web mode and overriding the services with the
617
748
  web services equivalents.
618
749
 
619
- Web has the following implications:
620
- - db
621
- - sqlite
622
- - services
623
- - Flask app
624
- - Investing Algorithm Framework App
625
- - Algorithm
626
- """
627
- configuration_service = self.container.configuration_service()
628
- self._flask_app = create_flask_app(configuration_service)
750
+ Web has the following implications:
751
+ - db
752
+ - sqlite
753
+ - services
754
+ - Flask app
755
+ - Investing Algorithm Framework App
756
+ - Algorithm
757
+ """
758
+ configuration_service = self.container.configuration_service()
759
+ self._flask_app = create_flask_app(configuration_service)
760
+
761
+ def get_portfolio_configurations(self):
762
+ portfolio_configuration_service = self.container \
763
+ .portfolio_configuration_service()
764
+ return portfolio_configuration_service.get_all()
765
+
766
+ def get_market_credential(self, market: str) -> MarketCredential:
767
+ """
768
+ Function to get a market credential from the app. This method
769
+ should be called when you want to get a market credential.
770
+
771
+ Args:
772
+ market (str): The market to get the credential for
773
+
774
+ Returns:
775
+ MarketCredential: Instance of MarketCredential
776
+ """
777
+
778
+ market_credential_service = self.container \
779
+ .market_credential_service()
780
+ market_credential = market_credential_service.get(market)
781
+ if market_credential is None:
782
+ raise OperationalException(
783
+ f"Market credential for {market} not found"
784
+ )
785
+ return market_credential
786
+
787
+ def get_market_credentials(self) -> List[MarketCredential]:
788
+ """
789
+ Function to get all market credentials from the app. This method
790
+ should be called when you want to get all market credentials.
791
+
792
+ Returns:
793
+ List of MarketCredential instances
794
+ """
795
+ market_credential_service = self.container \
796
+ .market_credential_service()
797
+ return market_credential_service.get_all()
798
+
799
+ def check_data_completeness(
800
+ self,
801
+ strategies: List[TradingStrategy],
802
+ backtest_date_range: BacktestDateRange,
803
+ show_progress: bool = True
804
+ ) -> Tuple[bool, Dict[str, Any]]:
805
+ """
806
+ Function to check the data completeness for a set of strategies
807
+ over a given backtest date range. This method checks if all data
808
+ sources required by the strategies have complete data for the
809
+ specified date range.
810
+
811
+ Args:
812
+ strategies (List[TradingStrategy]): List of strategy objects
813
+ to check data completeness for.
814
+ backtest_date_range (BacktestDateRange): The date range to
815
+ check data completeness for.
816
+ show_progress (bool): Whether to show a progress bar when
817
+ checking data completeness.
818
+ Returns:
819
+ Tuple[bool, Dict[str, Any]]: A tuple containing a boolean
820
+ indicating if the data is complete and a dictionary
821
+ with information about missing data for each data source.
822
+ """
823
+ data_sources = []
824
+ missing_data_info = {}
825
+
826
+ for strategy in strategies:
827
+ data_sources.extend(strategy.data_sources)
828
+
829
+ self.initialize_data_sources_backtest(
830
+ data_sources,
831
+ backtest_date_range,
832
+ show_progress=show_progress
833
+ )
834
+ data_provider_service = self.container.data_provider_service()
835
+ unique_data_sources = set(data_sources)
836
+
837
+ for data_source in unique_data_sources:
838
+
839
+ if DataType.OHLCV.equals(data_source.data_type):
840
+ required_start_date = backtest_date_range.start_date - \
841
+ timedelta(
842
+ minutes=TimeFrame.from_value(
843
+ data_source.time_frame
844
+ ).amount_of_minutes * data_source.window_size
845
+ )
846
+ number_of_required_data_points = \
847
+ data_source.get_number_of_required_data_points(
848
+ backtest_date_range.start_date,
849
+ backtest_date_range.end_date
850
+ )
851
+
852
+ try:
853
+ data_provider = data_provider_service.get(data_source)
854
+ number_of_available_data_points = \
855
+ data_provider.get_number_of_data_points(
856
+ backtest_date_range.start_date,
857
+ backtest_date_range.end_date
858
+ )
859
+
860
+ missing_dates = \
861
+ data_provider.get_missing_data_dates(
862
+ required_start_date,
863
+ backtest_date_range.end_date
864
+ )
865
+ if len(missing_dates) > 0:
866
+ missing_data_info[data_source.identifier] = {
867
+ "data_source_id": data_source.identifier,
868
+ "completeness_percentage": (
869
+ (
870
+ number_of_available_data_points /
871
+ number_of_required_data_points
872
+ ) * 100
873
+ ),
874
+ "missing_data_points": len(
875
+ missing_dates
876
+ ),
877
+ "missing_dates": missing_dates,
878
+ "data_source_file_path":
879
+ data_provider.get_data_source_file_path()
880
+ }
881
+
882
+ except Exception as e:
883
+ raise DataError(
884
+ f"Error getting data provider for data source "
885
+ f"{data_source.identifier} "
886
+ f"({data_source.symbol}): {str(e)}"
887
+ )
888
+
889
+ if len(missing_data_info.keys()) > 0:
890
+ return False, missing_data_info
891
+
892
+ return True, missing_data_info
893
+
894
+ def run_vector_backtests(
895
+ self,
896
+ initial_amount,
897
+ strategies: List[TradingStrategy],
898
+ backtest_date_range: BacktestDateRange = None,
899
+ backtest_date_ranges: List[BacktestDateRange] = None,
900
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
901
+ risk_free_rate: Optional[float] = None,
902
+ skip_data_sources_initialization: bool = False,
903
+ show_progress: bool = True,
904
+ market: Optional[str] = None,
905
+ trading_symbol: Optional[str] = None,
906
+ continue_on_error: bool = False,
907
+ ) -> List[Backtest]:
908
+ """
909
+ Run vectorized backtests for a set of strategies. The provided
910
+ set of strategies need to have their 'buy_signal_vectorized' and
911
+ 'sell_signal_vectorized' methods implemented to support vectorized
912
+ backtesting.
913
+
914
+ Args:
915
+ initial_amount: The initial amount to start the backtest with.
916
+ This will be the amount of trading currency that the backtest
917
+ portfolio will start with.
918
+ strategies (List[TradingStrategy]): List of strategy objects
919
+ that need to be backtested. Each strategy should implement
920
+ the 'buy_signal_vectorized' and 'sell_signal_vectorized'
921
+ methods to support vectorized backtesting.
922
+ backtest_date_range: The date range to run the backtest for
923
+ (instance of BacktestDateRange). This is used when
924
+ backtest_date_ranges is not provided.
925
+ backtest_date_ranges: List of date ranges to run the backtests for
926
+ (List of BacktestDateRange instances). If this is provided,
927
+ the backtests will be run for each date range in the list.
928
+ If this is not provided, the backtest_date_range will be used
929
+ snapshot_interval (SnapshotInterval): The snapshot
930
+ interval to use for the backtest. This is used to determine
931
+ how often the portfolio snapshot should be taken during the
932
+ backtest. The default is TRADE_CLOSE, which means that the
933
+ portfolio snapshot will be taken at the end of each trade.
934
+ risk_free_rate (Optional[float]): The risk-free rate to use for
935
+ the backtest. This is used to calculate the Sharpe ratio
936
+ and other performance metrics. If not provided, the default
937
+ risk-free rate will be tried to be fetched from the
938
+ US Treasury website.
939
+ skip_data_sources_initialization (bool): Whether to skip the
940
+ initialization of data sources. This is useful when the data
941
+ sources are already initialized, and you want to skip the
942
+ initialization step. This will speed up the backtesting
943
+ process, but make sure that the data sources are already
944
+ initialized before calling this method.
945
+ show_progress (bool): Whether to show progress bars during
946
+ data source initialization. This is useful for long-running
947
+ initialization processes.
948
+ market (str): The market to use for the backtest. This is used
949
+ to create a portfolio configuration if no portfolio
950
+ configuration is provided in the strategy.
951
+ trading_symbol (str): The trading symbol to use for the backtest.
952
+ This is used to create a portfolio configuration if no
953
+ portfolio configuration is provided in the strategy.
954
+ continue_on_error (bool): Whether to continue running other
955
+ backtests if an error occurs in one of the backtests. If set
956
+ to True, the backtest will return an empty Backtest instance
957
+ in case of an error. If set to False, the error will be raised.
958
+
959
+ Returns:
960
+ List[Backtest]: List of Backtest instances for each strategy
961
+ that was backtested.
962
+ """
963
+ backtests = []
964
+ backtests_ordered_by_strategy = {}
965
+ data_sources = []
966
+
967
+ if backtest_date_range is None and backtest_date_ranges is None:
968
+ raise OperationalException(
969
+ "Either backtest_date_range or backtest_date_ranges must be "
970
+ "provided"
971
+ )
972
+
973
+ for strategy in strategies:
974
+ data_sources.extend(strategy.data_sources)
975
+
976
+ if risk_free_rate is None:
977
+ logger.info("No risk free rate provided, retrieving it...")
978
+ risk_free_rate = get_risk_free_rate_us()
979
+
980
+ if risk_free_rate is None:
981
+ raise OperationalException(
982
+ "Could not retrieve risk free rate for backtest metrics."
983
+ "Please provide a risk free as an argument when running "
984
+ "your backtest or make sure you have an internet "
985
+ "connection"
986
+ )
987
+
988
+ if backtest_date_range is not None:
989
+ if not skip_data_sources_initialization:
990
+ self.initialize_data_sources_backtest(
991
+ data_sources,
992
+ backtest_date_range,
993
+ show_progress=show_progress
994
+ )
995
+
996
+ for strategy in tqdm(
997
+ strategies, colour="green", desc="Running backtests"
998
+ ):
999
+ backtest = self.run_vector_backtest(
1000
+ backtest_date_range=backtest_date_range,
1001
+ initial_amount=initial_amount,
1002
+ strategy=strategy,
1003
+ snapshot_interval=snapshot_interval,
1004
+ risk_free_rate=risk_free_rate,
1005
+ skip_data_sources_initialization=True,
1006
+ market=market,
1007
+ trading_symbol=trading_symbol,
1008
+ continue_on_error=continue_on_error
1009
+ )
1010
+ backtests.append(backtest)
1011
+ else:
1012
+ for backtest_date_range in tqdm(
1013
+ backtest_date_ranges,
1014
+ colour="green",
1015
+ desc="Running backtests for all date ranges"
1016
+ ):
1017
+ if not skip_data_sources_initialization:
1018
+ self.initialize_data_sources_backtest(
1019
+ data_sources,
1020
+ backtest_date_range,
1021
+ show_progress=show_progress
1022
+ )
1023
+ start_date = backtest_date_range.start_date.strftime(
1024
+ '%Y-%m-%d'
1025
+ )
1026
+ end_date = backtest_date_range.end_date.strftime('%Y-%m-%d')
1027
+
1028
+ for strategy in tqdm(
1029
+ strategies,
1030
+ colour="green",
1031
+ desc=f"Running backtests for "
1032
+ f"{start_date} to {end_date}"
1033
+ ):
1034
+
1035
+ if strategy not in backtests_ordered_by_strategy:
1036
+ backtests_ordered_by_strategy[strategy] = []
1037
+
1038
+ backtests_ordered_by_strategy[strategy].append(
1039
+ self.run_vector_backtest(
1040
+ backtest_date_range=backtest_date_range,
1041
+ initial_amount=initial_amount,
1042
+ strategy=strategy,
1043
+ snapshot_interval=snapshot_interval,
1044
+ risk_free_rate=risk_free_rate,
1045
+ skip_data_sources_initialization=True,
1046
+ market=market,
1047
+ trading_symbol=trading_symbol,
1048
+ )
1049
+ )
1050
+
1051
+ for strategy in backtests_ordered_by_strategy:
1052
+ backtests.append(
1053
+ combine_backtests(backtests_ordered_by_strategy[strategy])
1054
+ )
1055
+
1056
+ return backtests
629
1057
 
630
- def _create_resources_if_not_exists(self):
631
- """
632
- Function to create the resources required by the app if they
633
- do not exist. This function will check if the resource directory
634
- exists and check if the database directory exists. If they do
635
- not exist, it will create them.
1058
+ def run_vector_backtest(
1059
+ self,
1060
+ backtest_date_range: BacktestDateRange,
1061
+ strategy: TradingStrategy,
1062
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
1063
+ metadata: Optional[Dict[str, str]] = None,
1064
+ risk_free_rate: Optional[float] = None,
1065
+ skip_data_sources_initialization: bool = False,
1066
+ show_data_initialization_progress: bool = True,
1067
+ initial_amount: float = None,
1068
+ market: str = None,
1069
+ trading_symbol: str = None,
1070
+ continue_on_error: bool = False,
1071
+ ) -> Backtest:
1072
+ """
1073
+ Run vectorized backtests for a strategy. The provided
1074
+ strategy needs to have its 'buy_signal_vectorized' and
1075
+ 'sell_signal_vectorized' methods implemented to support vectorized
1076
+ backtesting.
1077
+
1078
+ Args:
1079
+ backtest_date_range: The date range to run the backtest for
1080
+ (instance of BacktestDateRange)
1081
+ initial_amount: The initial amount to start the backtest with.
1082
+ This will be the amount of trading currency that the backtest
1083
+ portfolio will start with.
1084
+ strategy (TradingStrategy) (Optional): The strategy object
1085
+ that needs to be backtested.
1086
+ snapshot_interval (SnapshotInterval): The snapshot
1087
+ interval to use for the backtest. This is used to determine
1088
+ how often the portfolio snapshot should be taken during the
1089
+ backtest. The default is TRADE_CLOSE, which means that the
1090
+ portfolio snapshot will be taken at the end of each trade.
1091
+ risk_free_rate (Optional[float]): The risk-free rate to use for
1092
+ the backtest. This is used to calculate the Sharpe ratio
1093
+ and other performance metrics. If not provided, the default
1094
+ risk-free rate will be tried to be fetched from the
1095
+ US Treasury website.
1096
+ metadata (Optional[Dict[str, str]]): Metadata to attach to the
1097
+ backtest report. This can be used to store additional
1098
+ information about the backtest, such as the author, version,
1099
+ parameters or any other relevant information.
1100
+ skip_data_sources_initialization (bool): Whether to skip the
1101
+ initialization of data sources. This is useful when the data
1102
+ sources are already initialized, and you want to skip the
1103
+ initialization step. This will speed up the backtesting
1104
+ process, but make sure that the data sources are already
1105
+ initialized before calling this method.
1106
+ show_data_initialization_progress (bool): Whether to show the
1107
+ progress bar when initializing data sources.
1108
+ market (str): The market to use for the backtest. This is used
1109
+ to create a portfolio configuration if no portfolio
1110
+ configuration is provided in the strategy.
1111
+ trading_symbol (str): The trading symbol to use for the backtest.
1112
+ This is used to create a portfolio configuration if no
1113
+ portfolio configuration is provided in the strategy.
1114
+ initial_amount (float): The initial amount to start the
1115
+ backtest with. This will be the amount of trading currency
1116
+ that the portfolio will start with. If not provided,
1117
+ the initial amount from the portfolio configuration will
1118
+ be used.
1119
+ continue_on_error (bool): Whether to continue running other
1120
+ backtests if an error occurs in one of the backtests. If set
1121
+ to True, the backtest will return an empty Backtest instance
1122
+ in case of an error. If set to False, the error will be raised.
636
1123
 
637
1124
  Returns:
638
- None
1125
+ Backtest: Instance of Backtest
639
1126
  """
640
- configuration_service = self.container.configuration_service()
641
- config = configuration_service.get_config()
642
- resource_dir = config[RESOURCE_DIRECTORY]
643
- database_dir = config[DATABASE_DIRECTORY_PATH]
1127
+ # Initialize configuration for vectorized backtesting
1128
+ self.initialize_backtest_config(
1129
+ backtest_date_range=backtest_date_range,
1130
+ snapshot_interval=snapshot_interval,
1131
+ initial_amount=initial_amount
1132
+ )
644
1133
 
645
- if resource_dir is None:
646
- raise OperationalException(
647
- "Resource directory is not specified in the config, please "
648
- "specify the resource directory in the config with the key "
649
- "RESOURCE_DIRECTORY"
1134
+ if not skip_data_sources_initialization:
1135
+ self.initialize_data_sources_backtest(
1136
+ strategy.data_sources,
1137
+ backtest_date_range,
1138
+ show_progress=show_data_initialization_progress
650
1139
  )
651
1140
 
652
- if not os.path.isdir(resource_dir):
653
- try:
654
- os.makedirs(resource_dir)
655
- except OSError as e:
656
- logger.error(e)
1141
+ if risk_free_rate is None:
1142
+ logger.info("No risk free rate provided, retrieving it...")
1143
+ risk_free_rate = get_risk_free_rate_us()
1144
+
1145
+ if risk_free_rate is None:
657
1146
  raise OperationalException(
658
- "Could not create resource directory"
1147
+ "Could not retrieve risk free rate for backtest metrics."
1148
+ "Please provide a risk free as an argument when running "
1149
+ "your backtest or make sure you have an internet "
1150
+ "connection"
659
1151
  )
660
1152
 
661
- if not os.path.isdir(database_dir):
662
- try:
663
- os.makedirs(database_dir)
664
- except OSError as e:
665
- logger.error(e)
666
- raise OperationalException(
667
- "Could not create database directory"
1153
+ backtest_service = self.container.backtest_service()
1154
+ backtest_service.validate_strategy_for_vector_backtest(strategy)
1155
+
1156
+ try:
1157
+ run = backtest_service.create_vector_backtest(
1158
+ strategy=strategy,
1159
+ backtest_date_range=backtest_date_range,
1160
+ risk_free_rate=risk_free_rate,
1161
+ market=market,
1162
+ trading_symbol=trading_symbol,
1163
+ initial_amount=initial_amount
1164
+ )
1165
+ backtest = Backtest(
1166
+ backtest_runs=[run],
1167
+ risk_free_rate=risk_free_rate,
1168
+ backtest_summary=generate_backtest_summary_metrics(
1169
+ [run.backtest_metrics]
1170
+ )
1171
+ )
1172
+ except Exception as e:
1173
+ logger.error(
1174
+ f"Error occurred during vector backtest for strategy "
1175
+ f"{strategy.strategy_id}: {str(e)}"
1176
+ )
1177
+ if continue_on_error:
1178
+ backtest = Backtest(
1179
+ backtest_runs=[],
1180
+ risk_free_rate=risk_free_rate,
668
1181
  )
1182
+ else:
1183
+ raise e
669
1184
 
670
- def get_portfolio_configurations(self):
671
- portfolio_configuration_service = self.container \
672
- .portfolio_configuration_service()
673
- return portfolio_configuration_service.get_all()
1185
+ # Add the metadata to the backtest
1186
+ if metadata is None:
674
1187
 
675
- def get_market_credential(self, market: str) -> MarketCredential:
1188
+ if strategy.metadata is None:
1189
+ backtest.metadata = {}
1190
+ else:
1191
+ backtest.metadata = strategy.metadata
1192
+ else:
1193
+ backtest.metadata = metadata
1194
+
1195
+ return backtest
1196
+
1197
+ def run_backtests(
1198
+ self,
1199
+ backtest_date_ranges,
1200
+ initial_amount=None,
1201
+ strategy: Optional[TradingStrategy] = None,
1202
+ algorithm: Optional[Algorithm] = None,
1203
+ algorithms: Optional[List[Algorithm]] = None,
1204
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
1205
+ risk_free_rate: Optional[float] = None,
1206
+ ) -> List[Backtest]:
676
1207
  """
677
- Function to get a market credential from the app. This method
678
- should be called when you want to get a market credential.
1208
+ Function to run multiple backtests for a list of algorithms over
1209
+ a list of date ranges. This function will run each algorithm
1210
+ for each date range and return a list of backtest reports.
679
1211
 
680
1212
  Args:
681
- market (str): The market to get the credential for
1213
+ algorithms: List of Algorithm instances to run backtests for.
1214
+ backtest_date_ranges (List[BacktestDateRange]): List of date ranges
1215
+ initial_amount (float): The initial amount to start the
1216
+ backtest with. This will be the amount of trading currency
1217
+ that the backtest portfolio will start with.
1218
+ snapshot_interval (SnapshotInterval): The snapshot interval to use
1219
+ for the backtest. This is used to determine how often the
1220
+ portfolio snapshot should be taken during the backtest.
1221
+ risk_free_rate (Optional[float]): The risk-free rate to use for
1222
+ the backtest. This is used to calculate the Sharpe ratio
1223
+ and other performance metrics. If not provided, the default
1224
+ risk-free rate will be tried to be fetched from the
1225
+ US Treasury website.
682
1226
 
683
1227
  Returns:
684
- MarketCredential: Instance of MarketCredential
1228
+ List[Backtest]: List of Backtest instances containing the results
685
1229
  """
1230
+ backtests = []
686
1231
 
687
- market_credential_service = self.container \
688
- .market_credential_service()
689
- market_credential = market_credential_service.get(market)
690
- if market_credential is None:
1232
+ if algorithms is not None:
1233
+ final_algorithms = algorithms
1234
+ elif strategy is not None:
1235
+ algorithm_factory = self.container.algorithm_factory()
1236
+ algorithm = algorithm_factory.create_algorithm(
1237
+ strategy=strategy
1238
+ )
1239
+ final_algorithms = [algorithm]
1240
+ elif algorithm is not None:
1241
+ final_algorithms = [algorithm]
1242
+ else:
691
1243
  raise OperationalException(
692
- f"Market credential for {market} not found"
1244
+ "No algorithms or strategy provided for backtesting"
693
1245
  )
694
- return market_credential
695
1246
 
696
- def get_market_credentials(self) -> List[MarketCredential]:
697
- """
698
- Function to get all market credentials from the app. This method
699
- should be called when you want to get all market credentials.
1247
+ if risk_free_rate is None:
1248
+ logger.info("No risk free rate provided, retrieving it...")
1249
+ risk_free_rate = get_risk_free_rate_us()
700
1250
 
701
- Returns:
702
- List of MarketCredential instances
703
- """
704
- market_credential_service = self.container \
705
- .market_credential_service()
706
- return market_credential_service.get_all()
1251
+ if risk_free_rate is None:
1252
+ raise OperationalException(
1253
+ "Could not retrieve risk free rate for backtest metrics."
1254
+ "Please provide a risk free as an argument when running "
1255
+ "your backtest or make sure you have an internet "
1256
+ "connection"
1257
+ )
1258
+
1259
+ for date_range in backtest_date_ranges:
1260
+ for algorithm in final_algorithms:
1261
+ backtest = self.run_backtest(
1262
+ backtest_date_range=date_range,
1263
+ initial_amount=initial_amount,
1264
+ algorithm=algorithm,
1265
+ snapshot_interval=snapshot_interval,
1266
+ risk_free_rate=risk_free_rate
1267
+ )
1268
+ backtests.append(backtest)
1269
+
1270
+ return backtests
707
1271
 
708
1272
  def run_backtest(
709
1273
  self,
710
1274
  backtest_date_range: BacktestDateRange,
711
1275
  name: str = None,
712
1276
  initial_amount=None,
713
- output_directory=None,
714
1277
  algorithm=None,
715
1278
  strategy=None,
716
1279
  strategies: List = None,
717
- save_strategy=True,
718
- snapshot_interval: SnapshotInterval = SnapshotInterval.TRADE_CLOSE,
719
- strategy_directory_path: Optional[str] = None,
720
- report_name: Optional[str] = None
721
- ) -> BacktestReport:
1280
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
1281
+ risk_free_rate: Optional[float] = None,
1282
+ metadata: Optional[Dict[str, str]] = None,
1283
+ ) -> Backtest:
722
1284
  """
723
1285
  Run a backtest for an algorithm.
724
1286
 
@@ -732,59 +1294,51 @@ class App:
732
1294
  portfolio will start with.
733
1295
  strategy (TradingStrategy) (Optional): The strategy object
734
1296
  that needs to be backtested.
735
- strategies (List[TradingStrategy) (Optional): List of strategy
1297
+ strategies (List[TradingStrategy]) (Optional): List of strategy
736
1298
  objects that need to be backtested
737
- algorithm:
738
- output_directory: str - The directory to
739
- write the backtest report to
740
- save_strategy: bool - Whether to save the strategy
741
- as part of the backtest report. You can only save in-memory
742
- strategies when running multiple backtests. This is because
1299
+ algorithm (Algorithm) (Optional): The algorithm object that needs
1300
+ to be backtested. If this is provided, then the strategies
1301
+ and tasks of the algorithm will be used for the backtest.
743
1302
  snapshot_interval (SnapshotInterval): The snapshot
744
1303
  interval to use for the backtest. This is used to determine
745
1304
  how often the portfolio snapshot should be taken during the
746
1305
  backtest. The default is TRADE_CLOSE, which means that the
747
1306
  portfolio snapshot will be taken at the end of each trade.
748
- strategy_directory_path (Optional[str]): The directory path
749
- where the strategy is located. This is used to save the
750
- strategy if save_strategy is True. If not provided,
751
- the framework tries to determine the path via the
752
- algorithm or strategy object.
753
- report_name (Optional[str]): The name of the report. If not
754
- provided, the framework will generate a name based on the
755
- algorithm name and the backtest date range and the current
756
- date and time.
1307
+ risk_free_rate (Optional[float]): The risk-free rate to use for
1308
+ the backtest. This is used to calculate the Sharpe ratio
1309
+ and other performance metrics. If not provided, the default
1310
+ risk-free rate will be tried to be fetched from the
1311
+ US Treasury website.
1312
+ metadata (Optional[Dict[str, str]]): Metadata to attach to the
1313
+ backtest report. This can be used to store additional
1314
+ information about the backtest, such as the author, version,
1315
+ parameters or any other relevant information.
757
1316
 
758
1317
  Returns:
759
- Instance of BacktestReport
1318
+ Backtest: Instance of Backtest
760
1319
  """
761
-
762
- # Add backtest configuration to the config
763
- self.set_config_with_dict({
764
- ENVIRONMENT: Environment.BACKTEST.value,
765
- BACKTESTING_START_DATE: backtest_date_range.start_date,
766
- BACKTESTING_END_DATE: backtest_date_range.end_date,
767
- DATABASE_NAME: "backtest-database.sqlite3",
768
- DATABASE_DIRECTORY_NAME: "backtest_databases",
769
- BACKTESTING_INITIAL_AMOUNT: initial_amount,
770
- SNAPSHOT_INTERVAL: snapshot_interval.value,
771
- })
772
-
773
- self.initialize_config()
774
- configuration_service = self.container.configuration_service()
775
- config = configuration_service.get_config()
776
- path = os.path.join(
777
- config[DATABASE_DIRECTORY_PATH], config[DATABASE_NAME]
1320
+ self.initialize_backtest_config(
1321
+ backtest_date_range=backtest_date_range,
1322
+ snapshot_interval=snapshot_interval,
1323
+ initial_amount=initial_amount
778
1324
  )
1325
+ self.initialize_storage(remove_database_if_exists=True)
1326
+ self.initialize_backtest_services()
1327
+ self.initialize_backtest_portfolios()
779
1328
 
780
- # Remove the previous backtest db if it exists
781
- if os.path.exists(path):
782
- os.remove(path)
1329
+ if risk_free_rate is None:
1330
+ logger.info("No risk free rate provided, retrieving it...")
1331
+ risk_free_rate = get_risk_free_rate_us()
783
1332
 
784
- self.initialize()
1333
+ if risk_free_rate is None:
1334
+ raise OperationalException(
1335
+ "Could not retrieve risk free rate for backtest metrics."
1336
+ "Please provide a risk free as an argument when running "
1337
+ "your backtest or make sure you have an internet "
1338
+ "connection"
1339
+ )
785
1340
 
786
- algorithm_factory = self.container.algorithm_factory()
787
- algorithm = algorithm_factory.create_algorithm(
1341
+ algorithm = self.container.algorithm_factory().create_algorithm(
788
1342
  name=name if name else self._name,
789
1343
  strategies=(
790
1344
  self._strategies if strategies is None else strategies
@@ -792,247 +1346,264 @@ class App:
792
1346
  algorithm=algorithm,
793
1347
  strategy=strategy,
794
1348
  tasks=self._tasks,
795
- data_sources=self._market_data_sources,
796
1349
  on_strategy_run_hooks=self._on_strategy_run_hooks,
797
1350
  )
798
- self.initialize_data_sources(algorithm)
799
- strategy_orchestrator_service = \
800
- self.container.strategy_orchestrator_service()
801
- strategy_orchestrator_service.initialize(algorithm)
1351
+ self.initialize_data_sources_backtest(
1352
+ algorithm.data_sources, backtest_date_range
1353
+ )
802
1354
  backtest_service = self.container.backtest_service()
803
1355
 
804
- # Setup snapshot service as observer
805
- backtest_service.clear_observers()
806
- portfolio_snapshot_service = \
807
- self.container.portfolio_snapshot_service()
808
- backtest_service.add_observer(portfolio_snapshot_service)
809
- context = self.container.context()
810
- order_service = self.container.order_service()
811
- order_service.clear_observers()
812
- order_service.add_observer(portfolio_snapshot_service)
813
- portfolio_service = self.container.portfolio_service()
814
- portfolio_service.clear_observers()
815
- portfolio_service.add_observer(portfolio_snapshot_service)
1356
+ # Create backtest schedule
1357
+ schedule = backtest_service.generate_schedule(
1358
+ algorithm.strategies,
1359
+ algorithm.tasks,
1360
+ backtest_date_range.start_date,
1361
+ backtest_date_range.end_date
1362
+ )
1363
+
1364
+ # Initialize event loop
1365
+ event_loop_service = EventLoopService(
1366
+ configuration_service=self.container.configuration_service(),
1367
+ portfolio_snapshot_service=self.container
1368
+ .portfolio_snapshot_service(),
1369
+ context=self.context,
1370
+ order_service=self.container.order_service(),
1371
+ portfolio_service=self.container.portfolio_service(),
1372
+ data_provider_service=self.container.data_provider_service(),
1373
+ trade_service=self.container.trade_service(),
1374
+ )
1375
+ trade_order_evaluator = BacktestTradeOrderEvaluator(
1376
+ trade_service=self.container.trade_service(),
1377
+ order_service=self.container.order_service(),
1378
+ trade_stop_loss_service=self.container.trade_stop_loss_service(),
1379
+ trade_take_profit_service=self.container
1380
+ .trade_take_profit_service(),
1381
+ configuration_service=self.container.configuration_service()
1382
+ )
1383
+ event_loop_service.initialize(
1384
+ algorithm=algorithm,
1385
+ trade_order_evaluator=trade_order_evaluator
1386
+ )
1387
+ event_loop_service.start(schedule=schedule, show_progress=True)
1388
+ self._run_history = event_loop_service.history
816
1389
 
817
- # Run the backtest with the backtest_service and collect and
818
- # save the report
819
- results = backtest_service.run_backtest(
1390
+ # Convert the current run to a backtest
1391
+ backtest = backtest_service.create_backtest(
820
1392
  algorithm=algorithm,
821
- context=context,
822
- strategy_orchestrator_service=strategy_orchestrator_service,
823
- initial_amount=initial_amount,
824
- backtest_date_range=backtest_date_range
1393
+ number_of_runs=event_loop_service.total_number_of_runs,
1394
+ backtest_date_range=backtest_date_range,
1395
+ risk_free_rate=risk_free_rate,
825
1396
  )
826
- report = BacktestReport(results=results)
827
1397
 
828
- if output_directory is None:
829
- output_directory = os.path.join(
830
- config[RESOURCE_DIRECTORY], "backtest_reports"
831
- )
1398
+ # Add the metadata to the backtest
1399
+ if metadata is None:
832
1400
 
833
- if report_name is None:
834
- report_name = BacktestService.create_report_directory_name(report)
1401
+ if algorithm.metadata is not None:
1402
+ backtest.metadata = algorithm.metadata
1403
+ else:
1404
+ backtest.metadata = {}
1405
+ else:
1406
+ backtest.metadata = metadata
835
1407
 
836
- output_directory = os.path.join(output_directory, report_name)
837
- report.save(
838
- path=output_directory,
839
- algorithm=algorithm,
840
- strategy_directory_path=strategy_directory_path,
841
- save_strategy=save_strategy
842
- )
843
- # print(report.html_report)
844
- # backtest_service.save_report(
845
- # report=report,
846
- # algorithm=algorithm,
847
- # output_directory=output_directory,
848
- # save_strategy=save_strategy,
849
- # )
850
- return report
1408
+ self.cleanup_backtest_resources()
1409
+ return backtest
851
1410
 
852
- def run_backtests(
1411
+ def run_permutation_test(
853
1412
  self,
854
- algorithms=None,
855
- strategies=None,
856
- initial_amount=None,
857
- backtest_date_ranges: List[BacktestDateRange] = None,
858
- output_directory=None,
859
- checkpoint=False,
860
- save_strategy=False,
861
- ) -> List[BacktestReport]:
862
- """
863
- Run a backtest for a set algorithm. This method should be called when
864
- running a backtest.
1413
+ strategy: TradingStrategy,
1414
+ backtest_date_range: BacktestDateRange,
1415
+ number_of_permutations: int = 100,
1416
+ initial_amount: float = 1000.0,
1417
+ market: str = None,
1418
+ trading_symbol: str = None,
1419
+ risk_free_rate: Optional[float] = None
1420
+ ) -> BacktestPermutationTest:
1421
+ """
1422
+ Run a permutation test for a given strategy over a specified
1423
+ date range. This test is used to determine the statistical
1424
+ significance of the strategy's performance by comparing it
1425
+ against a set of random permutations of the market data.
1426
+
1427
+ The permutation test will run the main backtest and then
1428
+ generate a number of random permutations of the market data
1429
+ to create a distribution of returns. The p value will be
1430
+ calculated based on the performance of the main backtest
1431
+ compared to the distribution of returns from the permutations.
865
1432
 
866
1433
  Args:
867
- algorithms (List[Algorithm]) (Optional): The algorithms to run
868
- backtests for. This param is optional. Either algorithms or
869
- strategies should be provided. If both are provided, then the
870
- algorithms will be used.
871
- strategies (List[TradingStrategy]) (Optional): The strategies to
872
- run backtests for. This param is optional. Either algorithms
873
- or strategies should be provided. If both are provided, then
874
- the algorithms will be used.
875
- backtest_date_ranges: List[BacktestDateRange] - The date ranges
876
- to run the backtests for
877
- initial_amount: The initial amount to start the backtest with.
878
- output_directory: str - The directory to write the backtest
879
- report to.
880
- checkpoint: bool - Whether to checkpoint the backtest,
881
- If True, then it will be checked if for a given algorithm name
882
- and date range, a backtest report already exists. If it does,
883
- then the backtest will not be run again. This is useful
884
- when running backtests for a large number of algorithms
885
- and date ranges where some of the backtests may fail
886
- and you want to re-run only the failed backtests.
887
- save_strategy: bool - Whether to save the strategy as part
888
- of the backtest report. You can only save in-memory strategies
889
- when running multiple backtests. This is because we can't
890
- differentiate between which folders belong to a specific
891
- strategy.
892
-
893
- Returns
894
- List of BacktestReport instances
895
- """
896
- logger.info("Initializing backtests")
897
- reports = []
898
-
899
- if algorithms is None and strategies is None:
900
- raise OperationalException(
901
- "No algorithms or strategies provided for backtest"
902
- )
1434
+ strategy (TradingStrategy): The strategy to test.
1435
+ backtest_date_range (BacktestDateRange): The date range for the
1436
+ backtest.
1437
+ number_of_permutations (int): The number of permutations to run.
1438
+ Default is 100.
1439
+ initial_amount (float): The initial amount for the backtest.
1440
+ Default is 1000.0.
1441
+ risk_free_rate (Optional[float]): The risk-free rate to use for
1442
+ the backtest metrics. If not provided, it will try to fetch
1443
+ the risk-free rate from the US Treasury website.
1444
+ market (str): The market to use for the backtest. This is used
1445
+ to create a portfolio configuration if no portfolio
1446
+ configuration is provided in the strategy. If not provided,
1447
+ the first portfolio configuration found will be used.
1448
+ trading_symbol (str): The trading symbol to use for the backtest.
1449
+ This is used to create a portfolio configuration if no
1450
+ portfolio configuration is provided in the strategy. If not
1451
+ provided, the first trading symbol found in the portfolio
1452
+ configuration will be used.
1453
+
1454
+ Raises:
1455
+ OperationalException: If the risk-free rate cannot be retrieved.
903
1456
 
904
- # Create or validate all algorithms with the algorithm factory
905
- algorithm_factory = self.container.algorithm_factory()
1457
+ Returns:
1458
+ Backtest: The backtest report containing the results of the
1459
+ main backtest and the p value from the permutation test.
1460
+ """
906
1461
 
907
- if algorithms is not None:
908
- to_be_validated_algorithms = algorithms
909
- algorithms = []
1462
+ if risk_free_rate is None:
1463
+ logger.info("No risk free rate provided, retrieving it...")
1464
+ risk_free_rate = get_risk_free_rate_us()
910
1465
 
911
- for algorithm in to_be_validated_algorithms:
912
- algorithm = algorithm_factory.create_algorithm(
913
- algorithm=algorithm
1466
+ if risk_free_rate is None:
1467
+ raise OperationalException(
1468
+ "Could not retrieve risk free rate for backtest metrics."
1469
+ "Please provide a risk free as an argument when running "
1470
+ "your backtest or make sure you have an internet "
1471
+ "connection"
914
1472
  )
915
- algorithms.append(algorithm)
916
- else:
917
- algorithms = []
918
1473
 
919
- if strategies is not None:
920
- for strategy in strategies:
921
- algorithms.append(
922
- algorithm_factory.create_algorithm(strategy=strategy)
923
- )
1474
+ data_provider_service = self.container.data_provider_service()
1475
+ backtest = self.run_vector_backtest(
1476
+ backtest_date_range=backtest_date_range,
1477
+ initial_amount=initial_amount,
1478
+ strategy=strategy,
1479
+ snapshot_interval=SnapshotInterval.DAILY,
1480
+ risk_free_rate=risk_free_rate,
1481
+ market=market,
1482
+ trading_symbol=trading_symbol
1483
+ )
1484
+ backtest_metrics = backtest.get_backtest_metrics(backtest_date_range)
924
1485
 
925
- for date_range in backtest_date_ranges:
926
- print(
927
- f"{COLOR_YELLOW}Running backtests for date "
928
- f"range:{COLOR_RESET} {COLOR_GREEN}{date_range.name} "
929
- f"{date_range.start_date} - "
930
- f"{date_range.end_date} for a "
931
- f"total of {len(algorithms)} algorithms.{COLOR_RESET}"
1486
+ if backtest_metrics.number_of_trades == 0:
1487
+ raise OperationalException(
1488
+ "The strategy did not make any trades during the backtest. "
1489
+ "Cannot perform permutation test."
932
1490
  )
933
1491
 
934
- for algorithm in algorithms:
1492
+ # Select the ohlcv data from the strategy's data sources
1493
+ data_sources = strategy.data_sources
1494
+ original_data_combinations = []
1495
+ permuted_metrics = []
1496
+ permuted_datasets_ordered_by_symbol = {}
1497
+ original_datasets_ordered_by_symbol = {}
935
1498
 
936
- if checkpoint:
937
- backtest_service = self.container.backtest_service()
938
- report = backtest_service.get_report(
939
- algorithm_name=algorithm.name,
940
- backtest_date_range=date_range,
941
- directory=output_directory
1499
+ for data_source in data_sources:
1500
+ if DataType.OHLCV.equals(data_source.data_type):
1501
+ data_provider = data_provider_service.get(data_source)
1502
+ data = data_provider_service.get_data(
1503
+ data_source=data_source,
1504
+ start_date=data_provider._start_date_data_source,
1505
+ end_date=backtest_date_range.end_date
1506
+ )
1507
+ original_data_combinations.append((data_source, data))
1508
+ original_datasets_ordered_by_symbol[data_source.symbol] = \
1509
+ data_provider_service.get_data(
1510
+ data_source=data_source,
1511
+ start_date=data_provider._start_date_data_source,
1512
+ end_date=backtest_date_range.end_date
942
1513
  )
943
1514
 
944
- if report is not None:
945
- print(
946
- f"{COLOR_YELLOW}Backtest already exists "
947
- f"for algorithm {algorithm.name} date "
948
- f"range:{COLOR_RESET} {COLOR_GREEN} "
949
- f"{date_range.name} "
950
- f"{date_range.start_date} - "
951
- f"{date_range.end_date}"
952
- )
953
- reports.append(report)
954
- continue
955
-
956
- report = self.run_backtest(
957
- backtest_date_range=date_range,
958
- initial_amount=initial_amount,
959
- output_directory=output_directory,
960
- algorithm=algorithm,
961
- save_strategy=save_strategy
1515
+ for _ in tqdm(
1516
+ range(number_of_permutations),
1517
+ desc="Running Permutation Test",
1518
+ colour="green"
1519
+ ):
1520
+ permutated_datasets = []
1521
+ data_provider_service.reset()
1522
+
1523
+ for combi in original_data_combinations:
1524
+ # Permute the data for the data source
1525
+ permutated_data = create_ohlcv_permutation(data=combi[1])
1526
+ permutated_datasets.append((combi[0], permutated_data))
1527
+
1528
+ if combi[0].symbol not in permuted_datasets_ordered_by_symbol:
1529
+ permuted_datasets_ordered_by_symbol[combi[0].symbol] = \
1530
+ [permutated_data]
1531
+ else:
1532
+ permuted_datasets_ordered_by_symbol[combi[0].symbol]\
1533
+ .append(permutated_data)
1534
+
1535
+ self._data_providers = []
1536
+
1537
+ for combi in permutated_datasets:
1538
+ data_source = combi[0]
1539
+ data_provider = PandasOHLCVDataProvider(
1540
+ dataframe=combi[1],
1541
+ symbol=data_source.symbol,
1542
+ market=data_source.market,
1543
+ window_size=data_source.window_size,
1544
+ time_frame=data_source.time_frame,
1545
+ data_provider_identifier=data_source
1546
+ .data_provider_identifier,
1547
+ pandas=data_source.pandas,
962
1548
  )
963
- reports.append(report)
964
-
965
- return reports
966
-
967
- def add_data_source(self, data_source) -> None:
968
- """
969
- Function to add a data source to the app. The data source should
970
- be an instance of DataSource.
971
-
972
- Args:
973
- data_source: Instance of DataSource
974
-
975
- Returns:
976
- None
977
- """
978
- if inspect.isclass(data_source):
979
- if not issubclass(data_source, MarketDataSource):
980
- raise OperationalException(
981
- "Data source should be an instance of MarketDataSource"
1549
+ # Add pandas ohlcv data provider to the data provider service
1550
+ data_provider_service.register_data_provider(
1551
+ data_source=data_source,
1552
+ data_provider=data_provider
982
1553
  )
983
1554
 
984
- data_source = data_source()
985
-
986
- # Check if data source is already registered
987
- # for market_data_source in self._market_data_sources:
988
- # if market_data_source.get_identifier() == \
989
- # data_source.get_identifier():
990
- # return
991
-
992
- self._market_data_sources.append(data_source)
993
-
994
- def add_data_sources(self, data_sources) -> None:
995
- """
996
- Function to add a list of data sources to the app. The data sources
997
- should be instances of DataSource.
1555
+ # Run the backtest with the permuted strategy
1556
+ permuted_backtest = self.run_vector_backtest(
1557
+ backtest_date_range=backtest_date_range,
1558
+ initial_amount=initial_amount,
1559
+ strategy=strategy,
1560
+ snapshot_interval=SnapshotInterval.DAILY,
1561
+ risk_free_rate=risk_free_rate,
1562
+ skip_data_sources_initialization=True,
1563
+ market=market,
1564
+ trading_symbol=trading_symbol
1565
+ )
998
1566
 
999
- Args:
1000
- data_sources: List of DataSource
1567
+ # Add the results of the permuted backtest to the main backtest
1568
+ permuted_metrics.append(
1569
+ permuted_backtest.get_backtest_metrics(backtest_date_range)
1570
+ )
1001
1571
 
1002
- Returns:
1003
- None
1004
- """
1005
- for data_source in data_sources:
1006
- self.add_data_source(data_source)
1572
+ # Create a BacktestPermutationTestMetrics object
1573
+ permutation_test_metrics = BacktestPermutationTest(
1574
+ real_metrics=backtest_metrics,
1575
+ permutated_metrics=permuted_metrics,
1576
+ ohlcv_permutated_datasets=permuted_datasets_ordered_by_symbol,
1577
+ ohlcv_original_datasets=original_datasets_ordered_by_symbol,
1578
+ backtest_start_date=backtest_date_range.start_date,
1579
+ backtest_end_date=backtest_date_range.end_date,
1580
+ backtest_date_range_name=backtest_date_range.name
1581
+ )
1582
+ return permutation_test_metrics
1007
1583
 
1008
- def add_market_data_source(self, market_data_source):
1584
+ def add_data_provider(self, data_provider, priority=3) -> None:
1009
1585
  """
1010
- Function to add a market data source to the app. The market data
1011
- source should be an instance of MarketDataSource.
1012
-
1013
- This is a seperate function from the market data source service. This
1014
- is because the market data source service can be re-initialized.
1015
- Therefore, we need a persistent list of market data sources in the app.
1586
+ Function to add a data provider to the app. The data provider should
1587
+ be an instance of DataProvider or a DataProviderClass.
1016
1588
 
1017
1589
  Args:
1018
- market_data_source: Instance of MarketDataSource
1590
+ data_provider: Instance or class of DataProvider
1591
+ priority: Optional priority for the data provider. If not
1592
+ provided, the data provider will be added with the default
1593
+ priority (3).
1019
1594
 
1020
1595
  Returns:
1021
1596
  None
1022
1597
  """
1598
+ if inspect.isclass(data_provider):
1599
+ if not issubclass(data_provider, DataProvider):
1600
+ raise OperationalException(
1601
+ "Data provider should be an instance of DataProvider"
1602
+ )
1023
1603
 
1024
- # Check if the market data source is an instance of MarketDataSource
1025
- if not isinstance(market_data_source, MarketDataSource):
1026
- return
1027
-
1028
- # Check if there is already a market data source with the same
1029
- # identifier
1030
- for existing_market_data_source in self._market_data_sources:
1031
- if existing_market_data_source.get_identifier() == \
1032
- market_data_source.get_identifier():
1033
- return
1604
+ data_provider = data_provider()
1034
1605
 
1035
- self._market_data_sources.append(market_data_source)
1606
+ self._data_providers.append((data_provider, priority))
1036
1607
 
1037
1608
  def add_market_credential(
1038
1609
  self, market_credential: MarketCredential
@@ -1108,7 +1679,7 @@ class App:
1108
1679
  function=None,
1109
1680
  time_unit=TimeUnit.MINUTE,
1110
1681
  interval=10,
1111
- market_data_sources=None
1682
+ data_sources=None
1112
1683
  ):
1113
1684
  """
1114
1685
  Decorator for registering a strategy. This decorator can be used
@@ -1120,7 +1691,7 @@ class App:
1120
1691
  a TradingStrategy
1121
1692
  time_unit (TimeUnit): instance of TimeUnit Enum
1122
1693
  interval (int): interval of the schedule ( interval - TimeUnit )
1123
- market_data_sources (List): List of data sources that the
1694
+ data_sources (List): List of data sources that the
1124
1695
  trading strategy function uses.
1125
1696
 
1126
1697
  Returns:
@@ -1133,7 +1704,7 @@ class App:
1133
1704
  decorated=function,
1134
1705
  time_unit=time_unit,
1135
1706
  interval=interval,
1136
- market_data_sources=market_data_sources
1707
+ data_sources=data_sources
1137
1708
  )
1138
1709
  self.add_strategy(strategy_object)
1139
1710
  return strategy_object
@@ -1145,7 +1716,7 @@ class App:
1145
1716
  decorated=f,
1146
1717
  time_unit=time_unit,
1147
1718
  interval=interval,
1148
- market_data_sources=market_data_sources,
1719
+ data_sources=data_sources,
1149
1720
  worker_id=f.__name__
1150
1721
  )
1151
1722
  )
@@ -1221,10 +1792,6 @@ class App:
1221
1792
  "with the same id in the algorithm"
1222
1793
  )
1223
1794
 
1224
- if strategy.market_data_sources is not None:
1225
- logger.info("Adding market data sources from strategy")
1226
- self.add_data_sources(strategy.market_data_sources)
1227
-
1228
1795
  self._strategies.append(strategy)
1229
1796
 
1230
1797
  def add_state_handler(self, state_handler):
@@ -1360,7 +1927,37 @@ class App:
1360
1927
  portfolio_provider_lookup = self.container.portfolio_provider_lookup()
1361
1928
  return portfolio_provider_lookup.get_all()
1362
1929
 
1363
- def _initialize_portfolios(self):
1930
+ def initialize_order_executors(self):
1931
+ """
1932
+ Function to initialize the order executors. This function will
1933
+ first check if the app is running in backtest mode or not. If it is
1934
+ running in backtest mode, all order executors will be removed and
1935
+ a single BacktestOrderExecutor will be added to the order executors.
1936
+
1937
+ If it is not running in backtest mode, it will add the default
1938
+ CCXTOrderExecutor with a priority 3.
1939
+ """
1940
+ logger.info("Adding order executors")
1941
+ order_executor_lookup = self.container.order_executor_lookup()
1942
+ environment = self.config[ENVIRONMENT]
1943
+
1944
+ if Environment.BACKTEST.equals(environment):
1945
+ # If the app is running in backtest mode,
1946
+ # remove all order executors
1947
+ # and add a single BacktestOrderExecutor
1948
+ order_executor_lookup.reset()
1949
+ order_executor_lookup.add_order_executor(
1950
+ BacktestOrderExecutor(priority=1)
1951
+ )
1952
+ else:
1953
+ order_executor_lookup.add_order_executor(
1954
+ CCXTOrderExecutor(priority=3)
1955
+ )
1956
+
1957
+ for order_executor in order_executor_lookup.get_all():
1958
+ order_executor.config = self.config
1959
+
1960
+ def initialize_portfolios(self):
1364
1961
  """
1365
1962
  Function to initialize the portfolios. This function will
1366
1963
  first check if the app is running in backtest mode or not. If it is
@@ -1370,8 +1967,6 @@ class App:
1370
1967
 
1371
1968
  """
1372
1969
  logger.info("Initializing portfolios")
1373
- config = self.config
1374
-
1375
1970
  portfolio_configuration_service = self.container \
1376
1971
  .portfolio_configuration_service()
1377
1972
  portfolio_service = self.container.portfolio_service()
@@ -1380,178 +1975,187 @@ class App:
1380
1975
  if portfolio_configuration_service.count() == 0:
1381
1976
  raise OperationalException("No portfolios configured")
1382
1977
 
1383
- if Environment.BACKTEST.equals(config[ENVIRONMENT]):
1384
- logger.info("Setting up backtest portfolios")
1385
- initial_backtest_amount = config.get(
1386
- BACKTESTING_INITIAL_AMOUNT, None
1387
- )
1978
+ # Check if there are already existing portfolios
1979
+ portfolios = portfolio_service.get_all()
1980
+ portfolio_configurations = portfolio_configuration_service\
1981
+ .get_all()
1982
+ portfolio_provider_lookup = \
1983
+ self.container.portfolio_provider_lookup()
1388
1984
 
1389
- for portfolio_configuration \
1390
- in portfolio_configuration_service.get_all():
1985
+ if len(portfolios) > 0:
1391
1986
 
1392
- if not portfolio_service.exists(
1393
- {"identifier": portfolio_configuration.identifier}
1394
- ):
1395
- portfolio = (
1396
- portfolio_service.create_portfolio_from_configuration(
1397
- portfolio_configuration,
1398
- initial_amount=initial_backtest_amount,
1399
- )
1987
+ # Check if there are matching portfolio configurations
1988
+ for portfolio in portfolios:
1989
+ logger.info(
1990
+ f"Checking if there is an matching portfolio "
1991
+ "configuration "
1992
+ f"for portfolio {portfolio.identifier}"
1993
+ )
1994
+ portfolio_configuration = \
1995
+ portfolio_configuration_service.get(
1996
+ portfolio.market
1400
1997
  )
1401
- else:
1402
- # Check if there are already existing portfolios
1403
- portfolios = portfolio_service.get_all()
1404
- portfolio_configurations = portfolio_configuration_service\
1405
- .get_all()
1406
-
1407
- if len(portfolios) > 0:
1408
-
1409
- # Check if there are matching portfolio configurations
1410
- for portfolio in portfolios:
1411
- logger.info(
1412
- f"Checking if there is an matching portfolio "
1413
- "configuration "
1414
- f"for portfolio {portfolio.identifier}"
1998
+
1999
+ if portfolio_configuration is None:
2000
+ raise ImproperlyConfigured(
2001
+ f"No matching portfolio configuration found for "
2002
+ f"existing portfolio {portfolio.market}, "
2003
+ f"please make sure that you have configured your "
2004
+ f"app with the right portfolio configurations "
2005
+ f"for the existing portfolios."
2006
+ f"If you want to create a new portfolio, please "
2007
+ f"remove the existing database (WARNING!!: this "
2008
+ f"will remove all existing history of your "
2009
+ f"trading bot.)"
1415
2010
  )
1416
- portfolio_configuration = \
1417
- portfolio_configuration_service.get(
1418
- portfolio.market
1419
- )
1420
2011
 
1421
- if portfolio_configuration is None:
1422
- raise ImproperlyConfigured(
1423
- f"No matching portfolio configuration found for "
1424
- f"existing portfolio {portfolio.market}, "
1425
- f"please make sure that you have configured your "
1426
- f"app with the right portfolio configurations "
1427
- f"for the existing portfolios."
1428
- f"If you want to create a new portfolio, please "
1429
- f"remove the existing database (WARNING!!: this "
1430
- f"will remove all existing history of your "
1431
- f"trading bot.)"
1432
- )
2012
+ # Check if the portfolio configuration is still inline
2013
+ # with the initial balance
1433
2014
 
1434
- # Check if the portfolio configuration is still inline
1435
- # with the initial balance
2015
+ if portfolio_configuration.initial_balance != \
2016
+ portfolio.initial_balance:
2017
+ logger.warning(
2018
+ "The initial balance of the portfolio "
2019
+ "configuration is different from the existing "
2020
+ "portfolio. Checking if the existing portfolio "
2021
+ "can be updated..."
2022
+ )
1436
2023
 
1437
- if portfolio_configuration.initial_balance != \
1438
- portfolio.initial_balance:
1439
- logger.warning(
1440
- "The initial balance of the portfolio "
1441
- "configuration is different from the existing "
1442
- "portfolio. Checking if the existing portfolio "
1443
- "can be updated..."
2024
+ # Register a portfolio provider for the portfolio
2025
+ portfolio_provider_lookup \
2026
+ .register_portfolio_provider_for_market(
2027
+ portfolio_configuration.market
1444
2028
  )
2029
+ initial_balance = portfolio_configuration\
2030
+ .initial_balance
1445
2031
 
1446
- portfolio_provider_lookup = \
1447
- self.container.portfolio_provider_lookup()
1448
- # Register a portfolio provider for the portfolio
1449
- portfolio_provider_lookup \
1450
- .register_portfolio_provider_for_market(
1451
- portfolio_configuration.market
1452
- )
1453
- initial_balance = portfolio_configuration\
1454
- .initial_balance
1455
-
1456
- if initial_balance != portfolio.initial_balance:
1457
- raise ImproperlyConfigured(
1458
- "The initial balance of the portfolio "
1459
- "configuration is different then that of "
1460
- "the existing portfolio. Please make sure "
1461
- "that the initial balance of the portfolio "
1462
- "configuration is the same as that of the "
1463
- "existing portfolio. "
1464
- f"Existing portfolio initial balance: "
1465
- f"{portfolio.initial_balance}, "
1466
- f"Portfolio configuration initial balance: "
1467
- f"{portfolio_configuration.initial_balance}"
1468
- "If this is intentional, please remove "
1469
- "the database and re-run the app. "
1470
- "WARNING!!: this will remove all existing "
1471
- "history of your trading bot."
1472
- )
1473
-
1474
- portfolio_provider_lookup = \
1475
- self.container.portfolio_provider_lookup()
1476
- order_executor_lookup = self.container.order_executor_lookup()
1477
- market_credential_service = \
1478
- self.container.market_credential_service()
1479
- # Register portfolio providers and order executors
1480
- for portfolio_configuration in portfolio_configurations:
1481
-
1482
- # Register a portfolio provider for the portfolio
1483
- portfolio_provider_lookup\
1484
- .register_portfolio_provider_for_market(
1485
- portfolio_configuration.market
1486
- )
2032
+ if initial_balance != portfolio.initial_balance:
2033
+ raise ImproperlyConfigured(
2034
+ "The initial balance of the portfolio "
2035
+ "configuration is different then that of "
2036
+ "the existing portfolio. Please make sure "
2037
+ "that the initial balance of the portfolio "
2038
+ "configuration is the same as that of the "
2039
+ "existing portfolio. "
2040
+ f"Existing portfolio initial balance: "
2041
+ f"{portfolio.initial_balance}, "
2042
+ f"Portfolio configuration initial balance: "
2043
+ f"{portfolio_configuration.initial_balance}"
2044
+ "If this is intentional, please remove "
2045
+ "the database and re-run the app. "
2046
+ "WARNING!!: this will remove all existing "
2047
+ "history of your trading bot."
2048
+ )
1487
2049
 
1488
- # Register an order executor for the portfolio
1489
- order_executor_lookup.register_order_executor_for_market(
2050
+ order_executor_lookup = self.container.order_executor_lookup()
2051
+ market_credential_service = \
2052
+ self.container.market_credential_service()
2053
+ # Register portfolio providers and order executors
2054
+ for portfolio_configuration in portfolio_configurations:
2055
+
2056
+ # Register a portfolio provider for the portfolio
2057
+ portfolio_provider_lookup\
2058
+ .register_portfolio_provider_for_market(
1490
2059
  portfolio_configuration.market
1491
2060
  )
1492
2061
 
1493
- market_credential = \
1494
- market_credential_service.get(
1495
- portfolio_configuration.market
1496
- )
2062
+ # Register an order executor for the portfolio
2063
+ order_executor_lookup.register_order_executor_for_market(
2064
+ portfolio_configuration.market
2065
+ )
1497
2066
 
1498
- if market_credential is None:
1499
- raise ImproperlyConfigured(
1500
- f"No market credential found for existing "
1501
- f"portfolio {portfolio_configuration.market} "
1502
- "with market "
1503
- "Cannot initialize portfolio configuration."
1504
- )
2067
+ market_credential = \
2068
+ market_credential_service.get(
2069
+ portfolio_configuration.market
2070
+ )
1505
2071
 
1506
- if not portfolio_service.exists(
1507
- {"identifier": portfolio_configuration.identifier}
1508
- ):
1509
- portfolio_service.create_portfolio_from_configuration(
1510
- portfolio_configuration
1511
- )
2072
+ if market_credential is None:
2073
+ raise ImproperlyConfigured(
2074
+ f"No market credential found for existing "
2075
+ f"portfolio {portfolio_configuration.market} "
2076
+ "with market "
2077
+ "Cannot initialize portfolio configuration."
2078
+ )
1512
2079
 
1513
- logger.info("Portfolio configurations complete")
1514
- logger.info("Syncing portfolios")
2080
+ if not portfolio_service.exists(
2081
+ {"identifier": portfolio_configuration.identifier}
2082
+ ):
2083
+ portfolio_service.create_portfolio_from_configuration(
2084
+ portfolio_configuration
2085
+ )
1515
2086
 
1516
- portfolio_service = self.container.portfolio_service()
1517
- portfolio_sync_service = self.container.portfolio_sync_service()
2087
+ logger.info("Portfolio configurations complete")
2088
+ logger.info("Syncing portfolios")
2089
+ portfolio_service = self.container.portfolio_service()
2090
+ portfolio_sync_service = self.container.portfolio_sync_service()
1518
2091
 
1519
- for portfolio in portfolio_service.get_all():
1520
- logger.info(f"Syncing portfolio {portfolio.identifier}")
1521
- portfolio_sync_service.sync_unallocated(portfolio)
1522
- portfolio_sync_service.sync_orders(portfolio)
2092
+ for portfolio in portfolio_service.get_all():
2093
+ logger.info(f"Syncing portfolio {portfolio.identifier}")
2094
+ portfolio_sync_service.sync_unallocated(portfolio)
2095
+ portfolio_sync_service.sync_orders(portfolio)
1523
2096
 
1524
- def _initialize_default_portfolio_providers(self):
2097
+ def initialize_backtest_portfolios(self):
1525
2098
  """
1526
- Function to initialize the default portfolio providers.
1527
- This function will create a default portfolio provider for
1528
- each market that is configured in the app. The default portfolio
1529
- provider will be used to create portfolios for the app.
2099
+ Function to initialize the backtest portfolios. This function will
2100
+ create a default portfolio provider for each market that is configured
2101
+ in the app. The default portfolio provider will be used to create
2102
+ portfolios for the app.
1530
2103
 
1531
2104
  Returns:
1532
2105
  None
1533
2106
  """
1534
- logger.info("Adding default portfolio providers")
1535
- portfolio_provider_lookup = self.container.portfolio_provider_lookup()
1536
- portfolio_provider_lookup.add_portfolio_provider(
1537
- CCXTPortfolioProvider(priority=2)
2107
+ logger.info("Initializing backtest portfolios")
2108
+ config = self.config
2109
+ portfolio_configuration_service = self.container \
2110
+ .portfolio_configuration_service()
2111
+ portfolio_service = self.container.portfolio_service()
2112
+
2113
+ # Throw an error if no portfolios are configured
2114
+ if portfolio_configuration_service.count() == 0:
2115
+ raise OperationalException("No portfolios configured")
2116
+
2117
+ logger.info("Setting up backtest portfolios")
2118
+ initial_backtest_amount = config.get(
2119
+ BACKTESTING_INITIAL_AMOUNT, None
1538
2120
  )
1539
2121
 
1540
- def _initialize_default_order_executors(self):
2122
+ for portfolio_configuration \
2123
+ in portfolio_configuration_service.get_all():
2124
+ if not portfolio_service.exists(
2125
+ {"identifier": portfolio_configuration.identifier}
2126
+ ):
2127
+ portfolio_service.create_portfolio_from_configuration(
2128
+ portfolio_configuration,
2129
+ initial_amount=initial_backtest_amount,
2130
+ )
2131
+
2132
+ def initialize_portfolio_providers(self):
1541
2133
  """
1542
- Function to initialize the default order executors.
1543
- This function will create a default order executor for
1544
- each market that is configured in the app. The default order
1545
- executor will be used to create orders for the app.
2134
+ Function to initialize the default portfolio providers.
2135
+ This function will create a default portfolio provider for
2136
+ each market that is configured in the app. The default portfolio
2137
+ provider will be used to create portfolios for the app.
1546
2138
 
1547
2139
  Returns:
1548
2140
  None
1549
2141
  """
1550
- logger.info("Adding default order executors")
1551
- order_executor_lookup = self.container.order_executor_lookup()
1552
- order_executor_lookup.add_order_executor(
1553
- CCXTOrderExecutor(priority=2)
1554
- )
2142
+ logger.info("Adding portfolio providers")
2143
+ portfolio_provider_lookup = self.container\
2144
+ .portfolio_provider_lookup()
2145
+ environment = self.config[ENVIRONMENT]
2146
+
2147
+ if Environment.BACKTEST.equals(environment):
2148
+ # If the app is running in backtest mode,
2149
+ # remove all order executors
2150
+ # and add a single BacktestOrderExecutor
2151
+ portfolio_provider_lookup.reset()
2152
+ else:
2153
+ portfolio_provider_lookup.add_portfolio_provider(
2154
+ CCXTPortfolioProvider(priority=3)
2155
+ )
2156
+
2157
+ for portfolio_provider in portfolio_provider_lookup.get_all():
2158
+ portfolio_provider.config = self.config
1555
2159
 
1556
2160
  def get_run_history(self):
1557
2161
  """
@@ -1594,6 +2198,17 @@ class App:
1594
2198
  name=self._name,
1595
2199
  strategies=self._strategies,
1596
2200
  tasks=self._tasks,
1597
- data_sources=self._market_data_sources,
1598
2201
  on_strategy_run_hooks=self._on_strategy_run_hooks,
1599
2202
  )
2203
+
2204
+ def cleanup_backtest_resources(self):
2205
+ """
2206
+ Clean up the backtest database and remove SQLAlchemy models/tables.
2207
+ """
2208
+ logger.info("Cleaning up backtest resources")
2209
+ config = self.config
2210
+ environment = config[ENVIRONMENT]
2211
+
2212
+ if Environment.BACKTEST.equals(environment):
2213
+ db_uri = config.get(SQLALCHEMY_DATABASE_URI)
2214
+ clear_db(db_uri)