investing-algorithm-framework 3.7.0__py3-none-any.whl → 7.19.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of investing-algorithm-framework might be problematic. Click here for more details.

Files changed (256) hide show
  1. investing_algorithm_framework/__init__.py +168 -45
  2. investing_algorithm_framework/app/__init__.py +32 -1
  3. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  4. investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
  5. investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
  6. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  7. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  8. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  9. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  10. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  11. investing_algorithm_framework/app/app.py +1933 -589
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1725 -0
  14. investing_algorithm_framework/app/eventloop.py +590 -0
  15. investing_algorithm_framework/app/reporting/__init__.py +27 -0
  16. investing_algorithm_framework/app/reporting/ascii.py +921 -0
  17. investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
  18. investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
  19. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  20. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  21. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
  22. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  23. investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
  24. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  25. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
  26. investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
  27. investing_algorithm_framework/app/reporting/generate.py +185 -0
  28. investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
  29. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
  30. investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
  31. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
  32. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  33. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  34. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  35. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  36. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +4 -2
  37. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
  38. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +1 -1
  39. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
  40. investing_algorithm_framework/app/strategy.py +664 -84
  41. investing_algorithm_framework/app/task.py +5 -3
  42. investing_algorithm_framework/app/web/__init__.py +2 -1
  43. investing_algorithm_framework/app/web/create_app.py +4 -2
  44. investing_algorithm_framework/cli/__init__.py +0 -0
  45. investing_algorithm_framework/cli/cli.py +226 -0
  46. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
  47. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  48. investing_algorithm_framework/cli/initialize_app.py +603 -0
  49. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  50. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  51. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  52. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  53. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  54. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  55. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  56. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  57. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  58. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  59. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  60. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  61. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  62. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  63. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  64. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  65. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  66. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  67. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  68. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  69. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  70. investing_algorithm_framework/create_app.py +40 -6
  71. investing_algorithm_framework/dependency_container.py +72 -56
  72. investing_algorithm_framework/domain/__init__.py +71 -47
  73. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  74. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  75. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  76. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  77. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  78. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  79. investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
  80. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  81. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  82. investing_algorithm_framework/domain/config.py +59 -91
  83. investing_algorithm_framework/domain/constants.py +13 -38
  84. investing_algorithm_framework/domain/data_provider.py +334 -0
  85. investing_algorithm_framework/domain/data_structures.py +3 -2
  86. investing_algorithm_framework/domain/exceptions.py +51 -1
  87. investing_algorithm_framework/domain/models/__init__.py +17 -12
  88. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  89. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  90. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  91. investing_algorithm_framework/domain/models/event.py +35 -0
  92. investing_algorithm_framework/domain/models/market/market_credential.py +55 -1
  93. investing_algorithm_framework/domain/models/order/order.py +77 -83
  94. investing_algorithm_framework/domain/models/order/order_status.py +2 -2
  95. investing_algorithm_framework/domain/models/order/order_type.py +1 -3
  96. investing_algorithm_framework/domain/models/portfolio/portfolio.py +81 -3
  97. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +26 -3
  98. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
  99. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  100. investing_algorithm_framework/domain/models/position/position.py +12 -0
  101. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  102. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  103. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  104. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  105. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  106. investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
  107. investing_algorithm_framework/domain/models/time_frame.py +37 -0
  108. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  109. investing_algorithm_framework/domain/models/time_unit.py +66 -2
  110. investing_algorithm_framework/domain/models/trade/__init__.py +8 -1
  111. investing_algorithm_framework/domain/models/trade/trade.py +295 -171
  112. investing_algorithm_framework/domain/models/trade/trade_status.py +9 -2
  113. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
  114. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
  115. investing_algorithm_framework/domain/order_executor.py +112 -0
  116. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  117. investing_algorithm_framework/domain/services/__init__.py +2 -9
  118. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +0 -6
  119. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  120. investing_algorithm_framework/domain/strategy.py +1 -29
  121. investing_algorithm_framework/domain/utils/__init__.py +12 -7
  122. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  123. investing_algorithm_framework/domain/utils/dates.py +57 -0
  124. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  125. investing_algorithm_framework/domain/utils/polars.py +53 -0
  126. investing_algorithm_framework/domain/utils/random.py +29 -0
  127. investing_algorithm_framework/download_data.py +108 -0
  128. investing_algorithm_framework/infrastructure/__init__.py +31 -18
  129. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  130. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
  131. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  132. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  133. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  134. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
  135. investing_algorithm_framework/infrastructure/models/__init__.py +6 -11
  136. investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -1
  137. investing_algorithm_framework/infrastructure/models/order/order.py +35 -49
  138. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  139. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  140. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +1 -1
  141. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -0
  142. investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -5
  143. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  144. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  145. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
  146. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
  147. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  148. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  149. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  150. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  151. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  152. investing_algorithm_framework/infrastructure/repositories/__init__.py +8 -0
  153. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  154. investing_algorithm_framework/infrastructure/repositories/order_repository.py +5 -0
  155. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +1 -1
  156. investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
  157. investing_algorithm_framework/infrastructure/repositories/repository.py +81 -27
  158. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  159. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
  160. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
  161. investing_algorithm_framework/infrastructure/services/__init__.py +4 -4
  162. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  163. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
  164. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  165. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  166. investing_algorithm_framework/services/__init__.py +113 -16
  167. investing_algorithm_framework/services/backtesting/__init__.py +0 -7
  168. investing_algorithm_framework/services/backtesting/backtest_service.py +566 -359
  169. investing_algorithm_framework/services/configuration_service.py +77 -11
  170. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  171. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  172. investing_algorithm_framework/services/market_credential_service.py +16 -1
  173. investing_algorithm_framework/services/metrics/__init__.py +114 -0
  174. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  175. investing_algorithm_framework/services/metrics/beta.py +0 -0
  176. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  177. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  178. investing_algorithm_framework/services/metrics/drawdown.py +181 -0
  179. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  180. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  181. investing_algorithm_framework/services/metrics/generate.py +358 -0
  182. investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
  183. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  184. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  185. investing_algorithm_framework/services/metrics/returns.py +452 -0
  186. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  187. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  188. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  189. investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
  190. investing_algorithm_framework/services/metrics/trades.py +500 -0
  191. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  192. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  193. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  194. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  195. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  196. investing_algorithm_framework/services/order_service/__init__.py +3 -1
  197. investing_algorithm_framework/services/order_service/order_backtest_service.py +76 -89
  198. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  199. investing_algorithm_framework/services/order_service/order_service.py +407 -326
  200. investing_algorithm_framework/services/portfolios/__init__.py +3 -1
  201. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +37 -3
  202. investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +22 -8
  203. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  204. investing_algorithm_framework/services/portfolios/portfolio_service.py +96 -28
  205. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +97 -28
  206. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +116 -313
  207. investing_algorithm_framework/services/positions/__init__.py +7 -0
  208. investing_algorithm_framework/services/positions/position_service.py +210 -0
  209. investing_algorithm_framework/services/repository_service.py +8 -2
  210. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  211. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
  212. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  213. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  214. investing_algorithm_framework/services/trade_service/__init__.py +7 -1
  215. investing_algorithm_framework/services/trade_service/trade_service.py +1013 -315
  216. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  217. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  218. investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
  219. investing_algorithm_framework-7.19.15.dist-info/RECORD +263 -0
  220. investing_algorithm_framework-7.19.15.dist-info/entry_points.txt +3 -0
  221. investing_algorithm_framework/app/algorithm.py +0 -1105
  222. investing_algorithm_framework/domain/graphs.py +0 -382
  223. investing_algorithm_framework/domain/metrics/__init__.py +0 -6
  224. investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -11
  225. investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -43
  226. investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
  227. investing_algorithm_framework/domain/models/backtesting/backtest_report.py +0 -580
  228. investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -243
  229. investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
  230. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  231. investing_algorithm_framework/domain/services/market_data_sources.py +0 -344
  232. investing_algorithm_framework/domain/services/market_service.py +0 -153
  233. investing_algorithm_framework/domain/singleton.py +0 -9
  234. investing_algorithm_framework/domain/utils/backtesting.py +0 -472
  235. investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -12
  236. investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -559
  237. investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -254
  238. investing_algorithm_framework/infrastructure/models/market_data_sources/us_treasury_yield.py +0 -47
  239. investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
  240. investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -455
  241. investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
  242. investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
  243. investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -350
  244. investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py +0 -53
  245. investing_algorithm_framework/services/backtesting/graphs.py +0 -61
  246. investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -8
  247. investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -150
  248. investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -189
  249. investing_algorithm_framework/services/position_service.py +0 -31
  250. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -264
  251. investing_algorithm_framework-3.7.0.dist-info/METADATA +0 -339
  252. investing_algorithm_framework-3.7.0.dist-info/RECORD +0 -147
  253. /investing_algorithm_framework/{domain → services}/metrics/price_efficiency.py +0 -0
  254. /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
  255. {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
  256. {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
@@ -1,30 +1,37 @@
1
1
  import inspect
2
2
  import logging
3
3
  import os
4
- import shutil
5
4
  import threading
6
- from abc import abstractmethod
7
- from distutils.sysconfig import get_python_lib
8
- from time import sleep
9
- from typing import List, Optional
5
+ from datetime import datetime, timezone, timedelta
6
+ from typing import List, Optional, Any, Dict, Tuple
10
7
 
11
8
  from flask import Flask
12
9
 
13
10
  from investing_algorithm_framework.app.algorithm import Algorithm
14
- from investing_algorithm_framework.app.stateless import ActionHandler
11
+ from investing_algorithm_framework.app.strategy import TradingStrategy
15
12
  from investing_algorithm_framework.app.task import Task
16
13
  from investing_algorithm_framework.app.web import create_flask_app
17
14
  from investing_algorithm_framework.domain import DATABASE_NAME, TimeUnit, \
18
15
  DATABASE_DIRECTORY_PATH, RESOURCE_DIRECTORY, ENVIRONMENT, Environment, \
19
- SQLALCHEMY_DATABASE_URI, OperationalException, BACKTESTING_FLAG, \
20
- BACKTESTING_START_DATE, BACKTESTING_END_DATE, BacktestReport, \
21
- BACKTESTING_PENDING_ORDER_CHECK_INTERVAL, APP_MODE, MarketCredential, \
22
- AppMode, BacktestDateRange
16
+ SQLALCHEMY_DATABASE_URI, OperationalException, StateHandler, \
17
+ BACKTESTING_START_DATE, BACKTESTING_END_DATE, APP_MODE, MarketCredential, \
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
23
24
  from investing_algorithm_framework.infrastructure import setup_sqlalchemy, \
24
- create_all_tables
25
+ create_all_tables, CCXTOrderExecutor, CCXTPortfolioProvider, \
26
+ BacktestOrderExecutor, CCXTOHLCVDataProvider, clear_db, \
27
+ PandasOHLCVDataProvider
25
28
  from investing_algorithm_framework.services import OrderBacktestService, \
26
- BacktestMarketDataSourceService, BacktestPortfolioService, \
27
- MarketDataSourceService, MarketCredentialService
29
+ BacktestPortfolioService, BacktestTradeOrderEvaluator, \
30
+ DefaultTradeOrderEvaluator, get_risk_free_rate_us
31
+ from .app_hook import AppHook
32
+ from .eventloop import EventLoopService
33
+ from .analysis import create_ohlcv_permutation
34
+
28
35
 
29
36
  logger = logging.getLogger("investing_algorithm_framework")
30
37
  COLOR_RESET = '\033[0m'
@@ -32,579 +39,673 @@ COLOR_GREEN = '\033[92m'
32
39
  COLOR_YELLOW = '\033[93m'
33
40
 
34
41
 
35
- class AppHook:
36
-
37
- @abstractmethod
38
- def on_run(self, app, algorithm: Algorithm):
39
- raise NotImplementedError()
40
-
41
-
42
42
  class App:
43
-
44
- def __init__(self, stateless=False, web=False):
43
+ """
44
+ Class to represent the app. This class is used to initialize the
45
+ application and run your trading bot.
46
+
47
+ Attributes:
48
+ container: The dependency container for the app. This is used
49
+ to store all the services and repositories for the app.
50
+ _flask_app: The flask app instance. This is used to run the
51
+ web app.
52
+ _state_handler: The state handler for the app. This is used
53
+ to save and load the state of the app.
54
+ _name: The name of the app. This is used to identify the app
55
+ in logs and other places.
56
+ _started: A boolean value that indicates if the app has been
57
+ started or not.
58
+ _tasks (List[Task]): List of task that need to be run by the
59
+ application.
60
+ """
61
+
62
+ def __init__(self, state_handler=None, name=None):
45
63
  self._flask_app: Optional[Flask] = None
46
64
  self.container = None
47
- self._stateless = stateless
48
- self._web = web
49
- self._algorithm: Optional[Algorithm] = None
50
65
  self._started = False
51
66
  self._tasks = []
52
- self._configuration_service = None
53
- self._market_data_source_service: \
54
- Optional[MarketDataSourceService] = None
55
- self._market_credential_service: \
56
- Optional[MarketCredentialService] = None
67
+ self._strategies = []
68
+ self._data_providers: List[Tuple[DataProvider, int]] = []
57
69
  self._on_initialize_hooks = []
70
+ self._on_strategy_run_hooks = []
58
71
  self._on_after_initialize_hooks = []
72
+ self._trade_order_evaluator = None
73
+ self._state_handler = state_handler
74
+ self._run_history = None
75
+ self._name = name
59
76
 
60
- def add_algorithm(self, algorithm: Algorithm) -> None:
77
+ @property
78
+ def context(self):
79
+ return self.container.context()
80
+
81
+ @property
82
+ def resource_directory_path(self):
61
83
  """
62
- Method to add an algorithm to the app. This method should be called
63
- before running the application.
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.
64
87
  """
65
- self._algorithm = algorithm
66
-
67
- def set_config(self, config: dict) -> None:
68
- configuration_service = self.container.configuration_service()
69
- configuration_service.initialize_from_dict(config)
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
+ )
70
102
 
71
- def initialize_services(self) -> None:
72
- self._configuration_service = self.container.configuration_service()
73
- self._market_data_source_service = \
74
- self.container.market_data_source_service()
75
- self._market_credential_service = \
76
- self.container.market_credential_service()
103
+ return resource_directory_path
77
104
 
78
105
  @property
79
- def algorithm(self) -> Algorithm:
80
- return self._algorithm
81
-
82
- @algorithm.setter
83
- def algorithm(self, algorithm: Algorithm) -> None:
84
- self._algorithm = algorithm
85
-
86
- def initialize(self, sync=False):
106
+ def database_directory_path(self):
87
107
  """
88
- Method to initialize the app. This method should be called before
89
- running the algorithm. It initializes the services and the algorithm
90
- and sets up the database if it does not exist.
91
-
92
- Also, it initializes all required services for the algorithm.
93
-
94
- :return: None
108
+ Returns the database directory path from the configuration.
109
+ This directory is used to store database files required by the app.
95
110
  """
96
- if self.algorithm is None:
97
- raise OperationalException("No algorithm registered")
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
+ )
98
128
 
99
- # Check if the algorithm has data sources registered
100
- if len(self.algorithm.data_sources) == 0:
129
+ return database_directory_path
101
130
 
102
- for data_source in self.algorithm.data_sources:
103
- self.add_market_data_source(data_source)
131
+ @property
132
+ def name(self):
133
+ return self._name
104
134
 
105
- self.algorithm.initialize_services(
106
- configuration_service=self.container.configuration_service(),
107
- market_data_source_service=self.container
108
- .market_data_source_service(),
109
- market_credential_service=self.container
110
- .market_credential_service(),
111
- portfolio_service=self.container.portfolio_service(),
112
- position_service=self.container.position_service(),
113
- order_service=self.container.order_service(),
114
- portfolio_configuration_service=self.container
115
- .portfolio_configuration_service(),
116
- market_service=self.container.market_service(),
117
- strategy_orchestrator_service=self.container
118
- .strategy_orchestrator_service(),
119
- trade_service=self.container.trade_service(),
120
- )
135
+ @name.setter
136
+ def name(self, name):
137
+ self._name = name
121
138
 
122
- if APP_MODE not in self.config:
123
- if self._stateless:
124
- self.config[APP_MODE] = AppMode.STATELESS.value
125
- elif self._web:
126
- self.config[APP_MODE] = AppMode.WEB.value
127
- else:
128
- self.config[APP_MODE] = AppMode.DEFAULT.value
139
+ @property
140
+ def started(self):
141
+ return self._started
129
142
 
130
- if AppMode.WEB.from_value(self.config[APP_MODE]):
131
- self._initialize_web()
132
- setup_sqlalchemy(self)
133
- create_all_tables()
134
- elif AppMode.STATELESS.from_value(self.config[APP_MODE]):
135
- self._initialize_stateless()
136
- setup_sqlalchemy(self)
137
- create_all_tables()
138
- else:
139
- self._initialize_standard()
140
- setup_sqlalchemy(self)
141
- create_all_tables()
143
+ @property
144
+ def config(self):
145
+ """
146
+ Function to get a config instance. This allows users when
147
+ having access to the app instance also to read the
148
+ configs of the app.
149
+ """
150
+ configuration_service = self.container.configuration_service()
151
+ return configuration_service.config
142
152
 
143
- # Initialize all portfolios that are registered
144
- portfolio_configuration_service = self.container \
145
- .portfolio_configuration_service()
153
+ @config.setter
154
+ def config(self, config: dict):
155
+ """
156
+ Function to set the configuration for the app.
157
+ Args:
158
+ config (dict): A dictionary containing the configuration
146
159
 
147
- # Throw an error if no portfolios are configured
148
- if portfolio_configuration_service.count() == 0:
149
- raise OperationalException("No portfolios configured")
160
+ Returns:
161
+ None
162
+ """
163
+ configuration_service = self.container.configuration_service()
164
+ configuration_service.initialize_from_dict(config)
150
165
 
151
- # Check if all portfolios are configured
152
- portfolio_service = self.container.portfolio_service()
153
- synced_portfolios = []
166
+ def add_algorithm(self, algorithm: Algorithm) -> None:
167
+ """
168
+ Method to add an algorithm to the app. This method should be called
169
+ before running the application.
154
170
 
155
- for portfolio_configuration \
156
- in portfolio_configuration_service.get_all():
171
+ When adding an algorithm, it will automatically register all
172
+ strategies, data sources, and tasks of the algorithm. The
173
+ algorithm itself is not registered.
157
174
 
158
- if not portfolio_service.exists(
159
- {"identifier": portfolio_configuration.identifier}
160
- ):
161
- portfolio = portfolio_service\
162
- .create_portfolio_from_configuration(
163
- portfolio_configuration
164
- )
165
- self.sync(portfolio)
166
- synced_portfolios.append(portfolio)
175
+ Args:
176
+ algorithm (Algorithm): The algorithm to add to the app.
177
+ This should be an instance of Algorithm.
167
178
 
168
- if sync:
169
- portfolios = portfolio_service.get_all()
179
+ Returns:
180
+ None
181
+ """
182
+ self.add_strategies(algorithm.strategies)
183
+ self.add_tasks(algorithm.tasks)
170
184
 
171
- for portfolio in portfolios:
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.
172
189
 
173
- if portfolio not in synced_portfolios:
174
- self.sync(portfolio)
190
+ Args:
191
+ trade_order_evaluator: The trade order evaluator to add to the app.
192
+ This should be an instance of TradeOrderEvaluator.
175
193
 
176
- def sync(self, portfolio):
177
- """
178
- Sync the portfolio with the exchange. This method should be called
179
- before running the algorithm. It syncs the portfolio with the
180
- exchange by syncing the unallocated balance, positions, orders, and
181
- trades.
194
+ Returns:
195
+ None
182
196
  """
183
- portfolio_sync_service = self.container.portfolio_sync_service()
184
-
185
- # Sync unallocated balance
186
- portfolio_sync_service.sync_unallocated(portfolio)
197
+ self._trade_order_evaluator = trade_order_evaluator
187
198
 
188
- # Sync all positions from exchange with current
189
- # position history
190
- portfolio_sync_service.sync_positions(portfolio)
199
+ def set_config(self, key: str, value: Any) -> None:
200
+ """
201
+ Function to add a key-value pair to the app's configuration.
191
202
 
192
- # Sync all orders from exchange with current order history
193
- portfolio_sync_service.sync_orders(portfolio)
203
+ Args:
204
+ key (string): The key to add to the configuration
205
+ value (any): The value to add to the configuration
194
206
 
195
- # Sync all trades from exchange with current trade history
196
- portfolio_sync_service.sync_trades(portfolio)
207
+ Returns:
208
+ None
209
+ """
210
+ configuration_service = self.container.configuration_service()
211
+ configuration_service.add_value(key, value)
197
212
 
198
- def _initialize_stateless(self):
213
+ def set_config_with_dict(self, config: dict) -> None:
199
214
  """
200
- Initialize the app for stateless mode by setting the configuration
201
- parameters for stateless mode and overriding the services with the
202
- stateless services equivalents.
215
+ Function to set the configuration for the app with a dictionary.
216
+ This is useful for setting multiple configuration values at once.
203
217
 
204
- In stateless mode, sqlalchemy is-setup with an in-memory database.
218
+ Args:
219
+ config (dict): A dictionary containing the configuration
205
220
 
206
- Stateless has the following implications:
207
- db: in-memory
208
- web: False
209
- app: Run with stateless action objects
210
- algorithm: Run with stateless action objects
221
+ Returns:
222
+ None
211
223
  """
212
224
  configuration_service = self.container.configuration_service()
213
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = "sqlite://"
225
+ configuration_service.initialize_from_dict(config)
214
226
 
215
- def _initialize_standard(self):
227
+ def initialize_config(self):
216
228
  """
217
- Initialize the app for standard mode by setting the configuration
218
- parameters for standard mode and overriding the services with the
219
- standard services equivalents.
229
+ Function to initialize the configuration for the app. This method
230
+ should be called before running the algorithm.
220
231
 
221
- Standard has the following implications:
222
- db: sqlite
223
- web: False
224
- app: Standard
225
- algorithm: Standard
232
+ Returns:
233
+ None
226
234
  """
235
+ data = {
236
+ ENVIRONMENT: self.config.get(ENVIRONMENT, Environment.PROD.value),
237
+ DATABASE_DIRECTORY_NAME: "databases",
238
+ LAST_SNAPSHOT_DATETIME: None
239
+ }
227
240
  configuration_service = self.container.configuration_service()
228
- resource_dir = configuration_service.config[RESOURCE_DIRECTORY]
241
+ configuration_service.initialize_from_dict(data)
242
+ config = configuration_service.get_config()
243
+
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
+ )
229
248
 
230
- if resource_dir is None:
231
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = "sqlite://"
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
+ )
232
257
  else:
233
- resource_dir = self._create_resource_directory_if_not_exists()
234
- configuration_service.config[DATABASE_DIRECTORY_PATH] = \
235
- os.path.join(resource_dir, "databases")
236
- configuration_service.config[DATABASE_NAME] \
237
- = "prod-database.sqlite3"
238
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = \
239
- "sqlite:///" + os.path.join(
240
- configuration_service.config[DATABASE_DIRECTORY_PATH],
241
- configuration_service.config[DATABASE_NAME]
242
- )
243
- self._create_database_if_not_exists()
258
+ configuration_service.add_value(
259
+ DATABASE_NAME, "dev-database.sqlite3"
260
+ )
244
261
 
245
- def _initialize_app_for_backtest(
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(
246
279
  self,
247
280
  backtest_date_range: BacktestDateRange,
248
- pending_order_check_interval=None,
249
- ) -> None:
281
+ initial_amount=None,
282
+ snapshot_interval: SnapshotInterval = None
283
+ ):
250
284
  """
251
- Initialize the app for backtesting by setting the configuration
252
- parameters for backtesting and overriding the services with the
253
- backtest services equivalents. This method should only be called
254
- before running a backtest or a set of backtests and should be called
255
- once.
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.
256
289
 
257
290
  Args:
258
- backtest_date_range: instance of BacktestDateRange
259
- pending_order_check_interval: The interval at which to check
260
- pending orders (e.g. 1h, 1d, 1w)
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.
261
299
 
262
- Return None
300
+ Returns:
301
+ None
263
302
  """
264
- # Set all config vars for backtesting
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
+ }
265
319
  configuration_service = self.container.configuration_service()
266
- configuration_service.config[BACKTESTING_FLAG] = True
267
- configuration_service.config[BACKTESTING_START_DATE] = \
268
- backtest_date_range.start_date
269
- configuration_service.config[BACKTESTING_END_DATE] = \
270
- backtest_date_range.end_date
271
-
272
- if pending_order_check_interval is not None:
273
- configuration_service.config[
274
- BACKTESTING_PENDING_ORDER_CHECK_INTERVAL
275
- ] = pending_order_check_interval
320
+ configuration_service.initialize_from_dict(data)
276
321
 
277
- # Create resource dir if not exits
278
- self._create_resource_directory_if_not_exists()
322
+ if snapshot_interval is not None:
323
+ configuration_service.add_value(
324
+ SNAPSHOT_INTERVAL,
325
+ SnapshotInterval.from_value(snapshot_interval).value
326
+ )
279
327
 
280
- def _create_backtest_database_if_not_exists(self):
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.
281
334
  """
282
- Create the backtest database if it does not exist. This method
283
- should be called before running a backtest for an algorithm.
284
- It creates the database if it does not exist.
335
+ resource_directory_path = self.resource_directory_path
285
336
 
286
- Args:
287
- None
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
+ )
288
342
 
289
- Returns
290
- None
291
- """
292
- configuration_service = self.container.configuration_service()
293
- resource_dir = configuration_service.config[RESOURCE_DIRECTORY]
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
+ )
294
350
 
295
- # Create the database if not exists
296
- configuration_service.config[DATABASE_DIRECTORY_PATH] = \
297
- os.path.join(resource_dir, "databases")
298
- configuration_service.config[DATABASE_NAME] = \
299
- "backtest-database.sqlite3"
300
351
  database_path = os.path.join(
301
- configuration_service.config[DATABASE_DIRECTORY_PATH],
302
- configuration_service.config[DATABASE_NAME]
352
+ database_directory_path, self.config[DATABASE_NAME]
303
353
  )
304
354
 
305
- if os.path.exists(database_path):
306
- os.remove(database_path)
355
+ if remove_database_if_exists:
307
356
 
308
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = \
309
- "sqlite:///" + os.path.join(
310
- configuration_service.config[DATABASE_DIRECTORY_PATH],
311
- configuration_service.config[DATABASE_NAME]
312
- )
313
- self._create_database_if_not_exists()
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
314
368
  setup_sqlalchemy(self)
315
369
  create_all_tables()
316
370
 
317
- def _initialize_backtest_data_sources(self, algorithm):
371
+ def initialize_data_sources(
372
+ self,
373
+ data_sources: List[DataSource],
374
+ ):
318
375
  """
319
- Initialize the backtest data sources for the algorithm. This method
320
- should be called before running a backtest. It initializes the
321
- backtest data sources for the algorithm. It takes all registered
322
- data sources and converts them to backtest equivalents
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.
323
379
 
324
380
  Args:
325
- algorithm: The algorithm to initialize for backtesting
381
+ data_sources (List[DataSource]): The data sources to initialize.
382
+ This should be a list of DataSource instances.
326
383
 
327
- Returns
384
+ Returns:
328
385
  None
329
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
+ )
330
399
 
331
- market_data_sources = self._market_data_source_service \
332
- .get_market_data_sources()
333
- backtest_market_data_sources = []
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.
334
417
 
335
- if algorithm.data_sources is not None \
336
- and len(algorithm.data_sources) > 0:
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.
337
424
 
338
- for data_source in algorithm.data_sources:
339
- self.add_market_data_source(data_source)
425
+ Returns:
426
+ None
427
+ """
428
+ logger.info("Initializing data sources for backtest")
340
429
 
341
- if market_data_sources is not None:
342
- backtest_market_data_sources = [
343
- market_data_source.to_backtest_market_data_source()
344
- for market_data_source in market_data_sources
345
- if market_data_source is not None
346
- ]
430
+ if data_sources is None or len(data_sources) == 0:
431
+ return
347
432
 
348
- for market_data_source in backtest_market_data_sources:
349
- if market_data_source is not None:
350
- market_data_source.config = self.config
433
+ data_provider_service = self.container.data_provider_service()
434
+ data_provider_service.reset()
351
435
 
352
- # Override the market data source service with the backtest market
353
- # data source service
354
- self.container.market_data_source_service.override(
355
- BacktestMarketDataSourceService(
356
- market_data_sources=backtest_market_data_sources,
357
- market_service=self.container.market_service(),
358
- market_credential_service=self.container
359
- .market_credential_service(),
360
- configuration_service=self.container
361
- .configuration_service(),
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]
362
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
363
447
  )
364
448
 
365
- # Set all data sources to the algorithm
366
- algorithm.add_data_sources(backtest_market_data_sources)
449
+ description = "Preparing backtest data for all data sources"
450
+ data_providers = data_provider_service.data_provider_index.get_all()
367
451
 
368
- def _initialize_algorithm_for_backtest(self, algorithm):
369
- """
370
- Function to initialize the algorithm for backtesting. This method
371
- should be called before running a backtest. It initializes the
372
- all data sources to backtest data sources and overrides the services
373
- with the backtest services equivalents.
452
+ # Prepare the backtest data for each data provider
453
+ if not show_progress:
454
+ for _, data_provider in data_providers:
374
455
 
375
- Args:
376
- algorithm: The algorithm to initialize for backtesting
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
+ )
377
470
 
378
- Return None
471
+ def initialize_backtest_services(self):
379
472
  """
380
- self._create_backtest_database_if_not_exists()
381
- self._initialize_backtest_data_sources(algorithm)
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.
382
476
 
477
+ Returns:
478
+ None
479
+ """
480
+ configuration_service = self.container.configuration_service()
481
+ self.initialize_order_executors()
482
+ self.initialize_portfolio_providers()
483
+ portfolio_conf_service = self.container \
484
+ .portfolio_configuration_service()
485
+ portfolio_snap_service = self.container \
486
+ .portfolio_snapshot_service()
487
+ market_cred_service = self.container.market_credential_service()
488
+ portfolio_provider_lookup = \
489
+ self.container.portfolio_provider_lookup()
383
490
  # Override the portfolio service with the backtest portfolio service
384
491
  self.container.portfolio_service.override(
385
492
  BacktestPortfolioService(
386
- configuration_service=self.container.configuration_service(),
387
- market_credential_service=self.container
388
- .market_credential_service(),
389
- market_service=self.container.market_service(),
493
+ configuration_service=configuration_service,
494
+ market_credential_service=market_cred_service,
390
495
  position_service=self.container.position_service(),
391
496
  order_service=self.container.order_service(),
392
497
  portfolio_repository=self.container.portfolio_repository(),
393
- portfolio_configuration_service=self.container
394
- .portfolio_configuration_service(),
395
- portfolio_snapshot_service=self.container
396
- .portfolio_snapshot_service(),
498
+ portfolio_configuration_service=portfolio_conf_service,
499
+ portfolio_snapshot_service=portfolio_snap_service,
500
+ portfolio_provider_lookup=portfolio_provider_lookup
397
501
  )
398
502
  )
399
503
 
504
+ portfolio_conf_service = self.container. \
505
+ portfolio_configuration_service()
506
+ portfolio_snap_service = self.container. \
507
+ portfolio_snapshot_service()
508
+ configuration_service = self.container.configuration_service()
400
509
  # Override the order service with the backtest order service
401
- market_data_source_service = self.container \
402
- .market_data_source_service()
403
510
  self.container.order_service.override(
404
511
  OrderBacktestService(
512
+ trade_service=self.container.trade_service(),
405
513
  order_repository=self.container.order_repository(),
406
- position_repository=self.container.position_repository(),
514
+ position_service=self.container.position_service(),
407
515
  portfolio_repository=self.container.portfolio_repository(),
408
- portfolio_configuration_service=self.container
409
- .portfolio_configuration_service(),
410
- portfolio_snapshot_service=self.container
411
- .portfolio_snapshot_service(),
412
- configuration_service=self.container.configuration_service(),
413
- market_data_source_service=market_data_source_service
516
+ portfolio_configuration_service=portfolio_conf_service,
517
+ portfolio_snapshot_service=portfolio_snap_service,
518
+ configuration_service=configuration_service,
414
519
  )
415
520
  )
416
521
 
417
- portfolio_configuration_service = self.container \
418
- .portfolio_configuration_service()
522
+ def initialize_services(self):
523
+ """
524
+ Method to initialize the app. This method should be called before
525
+ running the algorithm. It initializes the services and the algorithm
526
+ and sets up the database if it does not exist.
419
527
 
420
- # Re-init the market service because the portfolio configuration
421
- # service is a singleton
422
- portfolio_configuration_service.market_service \
423
- = self.container.market_service()
528
+ Also, it initializes all required services for the algorithm.
424
529
 
425
- if portfolio_configuration_service.count() == 0:
426
- raise OperationalException("No portfolios configured")
530
+ Returns:
531
+ None
532
+ """
533
+ logger.info("Initializing app")
534
+ self.initialize_order_executors()
535
+ self.initialize_portfolio_providers()
427
536
 
428
- strategy_orchestrator_service = \
429
- self.container.strategy_orchestrator_service()
537
+ # Initialize all market credentials
430
538
  market_credential_service = self.container.market_credential_service()
431
- market_data_source_service = \
432
- self.container.market_data_source_service()
433
- # Initialize all services in the algorithm
434
- algorithm.initialize_services(
435
- configuration_service=self.container.configuration_service(),
436
- portfolio_configuration_service=self.container
437
- .portfolio_configuration_service(),
438
- portfolio_service=self.container.portfolio_service(),
439
- position_service=self.container.position_service(),
440
- order_service=self.container.order_service(),
441
- market_service=self.container.market_service(),
442
- strategy_orchestrator_service=strategy_orchestrator_service,
443
- market_credential_service=market_credential_service,
444
- market_data_source_service=market_data_source_service,
445
- trade_service=self.container.trade_service(),
446
- )
447
-
448
- # Create all portfolios
539
+ market_credential_service.initialize()
449
540
  portfolio_configuration_service = self.container \
450
541
  .portfolio_configuration_service()
451
- portfolio_configurations = portfolio_configuration_service.get_all()
452
- portfolio_service = self.container.portfolio_service()
453
-
454
- for portfolio_configuration in portfolio_configurations:
455
- portfolio_service.create_portfolio_from_configuration(
456
- portfolio_configuration
457
- )
458
542
 
459
- def _initialize_management_commands(self):
543
+ if portfolio_configuration_service.count() == 0:
544
+ raise OperationalException("No portfolios configured")
460
545
 
461
- if not Environment.TEST.equals(self.config.get(ENVIRONMENT)):
462
- # Copy the template manage.py file to the resource directory of the
463
- # algorithm
464
- management_commands_template = os.path.join(
465
- get_python_lib(),
466
- "investing_algorithm_framework/templates/manage.py"
467
- )
468
- destination = os.path.join(
469
- self.config.get(RESOURCE_DIRECTORY), "manage.py"
470
- )
546
+ configuration_service = self.container.configuration_service()
547
+ config = configuration_service.get_config()
471
548
 
472
- if not os.path.exists(destination):
473
- shutil.copy(management_commands_template, destination)
549
+ if AppMode.WEB.equals(config[APP_MODE]):
550
+ configuration_service.add_value(APP_MODE, AppMode.WEB.value)
551
+ self._initialize_web()
474
552
 
475
- def run(
476
- self,
477
- payload: dict = None,
478
- number_of_iterations: int = None,
479
- sync=False
480
- ):
553
+ def run(self, number_of_iterations: int = None):
481
554
  """
482
555
  Entry point to run the application. This method should be called to
483
- start the algorithm. The method runs the algorithm for the specified
484
- number of iterations and handles the payload if the app is running in
485
- stateless mode.
486
-
487
- First the app checks if there is an algorithm registered. If not, it
488
- raises an OperationalException. Then it initializes the algorithm
489
- with the services and the configuration.
490
-
491
- If the app is running in stateless mode, it handles the
492
- payload. If the app is running in web mode, it starts the web app in a
493
- separate thread.
556
+ start the trading bot. This method can be called in three modes:
557
+
558
+ - Without any params: In this mode, the app runs until a keyboard
559
+ interrupt is received. This mode is useful when running the app in
560
+ a loop.
561
+ - With a payload: In this mode, the app runs only once with the
562
+ payload provided. This mode is useful when running the app in a
563
+ one-off mode, such as running the app from the command line or
564
+ on a schedule. Payload is a dictionary that contains the data to
565
+ handle for the algorithm. This data should look like this:
566
+ {
567
+ "action": "RUN_STRATEGY",
568
+ }
569
+ - With a number of iterations: In this mode, the app runs for the
570
+ number of iterations provided. This mode is useful when running the
571
+ app in a loop for a fixed number of iterations.
572
+
573
+ This function first checks if there is an algorithm registered.
574
+ If not, it raises an OperationalException. Then it
575
+ initializes the algorithm with the services and the configuration.
494
576
 
495
577
  Args:
496
- payload: The payload to handle if the app is running in
497
- stateless mode
498
- number_of_iterations: The number of iterations to run the
499
- algorithm for
500
- sync: Whether to sync the portfolio with the exchange
578
+ number_of_iterations (int): The number of iterations to run the
579
+ algorithm for
501
580
 
502
581
  Returns:
503
582
  None
504
583
  """
584
+ self.initialize_config()
585
+ self.initialize_storage()
586
+ event_loop_service = None
505
587
 
506
- # Run all on_initialize hooks
507
- for hook in self._on_after_initialize_hooks:
508
- hook.on_run(self, self.algorithm)
509
-
510
- self.initialize(sync=sync)
511
-
512
- # Run all on_initialize hooks
513
- for hook in self._on_initialize_hooks:
514
- hook.on_run(self, self.algorithm)
588
+ try:
515
589
 
516
- self.algorithm.start(
517
- number_of_iterations=number_of_iterations,
518
- stateless=self.stateless
519
- )
590
+ # Load the state if a state handler is provided
591
+ if self._state_handler is not None:
592
+ logger.info("Detected state handler, loading state")
593
+ self._state_handler.initialize()
594
+ config = self.container.configuration_service().get_config()
595
+ self._state_handler.load(config[RESOURCE_DIRECTORY])
596
+
597
+ logger.info("App initialization complete")
598
+
599
+ # Run all on_initialize hooks
600
+ for hook in self._on_initialize_hooks:
601
+ hook.on_run(self.context)
602
+
603
+ algorithm = self.get_algorithm()
604
+ self.initialize_data_sources(algorithm.data_sources)
605
+ self.initialize_services()
606
+ self.initialize_portfolios()
607
+
608
+ if AppMode.WEB.equals(self.config[APP_MODE]):
609
+ logger.info("Running web")
610
+ flask_thread = threading.Thread(
611
+ name='Web App',
612
+ target=self._flask_app.run,
613
+ kwargs={"port": 8080}
614
+ )
615
+ flask_thread.daemon = True
616
+ flask_thread.start()
520
617
 
521
- if AppMode.STATELESS.equals(self.config[APP_MODE]):
522
- logger.info("Running stateless")
523
- action_handler = ActionHandler.of(payload)
524
- return action_handler.handle(
525
- payload=payload, algorithm=self.algorithm
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(),
526
636
  )
527
- elif AppMode.WEB.equals(self.config[APP_MODE]):
528
- logger.info("Running web")
529
- flask_thread = threading.Thread(
530
- name='Web App',
531
- target=self._flask_app.run,
532
- kwargs={"port": 8080}
637
+ event_loop_service.initialize(
638
+ algorithm, trade_order_evaluator=trade_order_evaluator
533
639
  )
534
- flask_thread.setDaemon(True)
535
- flask_thread.start()
536
640
 
537
- number_of_iterations_since_last_orders_check = 1
538
- self.algorithm.check_pending_orders()
539
-
540
- try:
541
- while self.algorithm.running:
542
- if number_of_iterations_since_last_orders_check == 30:
543
- logger.info("Checking pending orders")
544
- number_of_iterations_since_last_orders_check = 1
641
+ try:
642
+ event_loop_service.start(
643
+ number_of_iterations=number_of_iterations
644
+ )
645
+ except KeyboardInterrupt:
646
+ exit(0)
647
+ except Exception as e:
648
+ logger.error(e)
649
+ raise e
650
+ finally:
545
651
 
546
- self.algorithm.run_jobs()
547
- number_of_iterations_since_last_orders_check += 1
548
- sleep(1)
549
- except KeyboardInterrupt:
550
- exit(0)
652
+ if event_loop_service is not None:
653
+ self._run_history = event_loop_service.history
551
654
 
552
- @property
553
- def started(self):
554
- return self._started
655
+ try:
656
+ # Upload state if state handler is provided
657
+ if self._state_handler is not None:
658
+ logger.info("Detected state handler, saving state")
659
+ config = \
660
+ self.container.configuration_service().get_config()
661
+ self._state_handler.save(config[RESOURCE_DIRECTORY])
662
+ except Exception as e:
663
+ logger.error(e)
555
664
 
556
- @property
557
- def config(self):
558
- """
559
- Function to get a config instance. This allows users when
560
- having access to the app instance also to read the
561
- configs of the app.
665
+ def add_portfolio_configuration(self, portfolio_configuration):
562
666
  """
563
- configuration_service = self.container.configuration_service()
564
- return configuration_service.config
667
+ Function to add a portfolio configuration to the app. The portfolio
668
+ configuration should be an instance of PortfolioConfiguration.
565
669
 
566
- @config.setter
567
- def config(self, config: dict):
568
- configuration_service = self.container.configuration_service()
569
- configuration_service.initialize_from_dict(config)
570
-
571
- def reset(self):
572
- self._started = False
573
- self.algorithm.reset()
670
+ Args:
671
+ portfolio_configuration: Instance of PortfolioConfiguration
574
672
 
575
- def add_portfolio_configuration(self, portfolio_configuration):
673
+ Returns:
674
+ None
675
+ """
576
676
  portfolio_configuration_service = self.container \
577
677
  .portfolio_configuration_service()
578
678
  portfolio_configuration_service.add(portfolio_configuration)
579
679
 
580
- @property
581
- def stateless(self):
582
- return self._stateless
583
-
584
- @property
585
- def web(self):
586
- return self._web
587
-
588
- @property
589
- def running(self):
590
- return self.algorithm.running
591
-
592
680
  def task(
593
681
  self,
594
682
  function=None,
595
683
  time_unit: TimeUnit = TimeUnit.MINUTE,
596
684
  interval=10,
597
685
  ):
686
+ """
687
+ Function to add a task to the application.
688
+
689
+ Args:
690
+ function:
691
+ time_unit:
692
+ interval:
693
+
694
+ Returns:
695
+ Union(Task, Function): the task
696
+ """
697
+
598
698
  if function:
599
699
  task = Task(
600
700
  decorated=function,
601
701
  time_unit=time_unit,
602
702
  interval=interval,
603
703
  )
604
- self.algorithm.add_task(task)
704
+ self._tasks.append(task)
705
+ return task
605
706
  else:
606
707
  def wrapper(f):
607
- self.algorithm.add_task(
708
+ self._tasks.append(
608
709
  Task(
609
710
  decorated=f,
610
711
  time_unit=time_unit,
@@ -615,6 +716,31 @@ class App:
615
716
 
616
717
  return wrapper
617
718
 
719
+ def add_task(self, task):
720
+ if inspect.isclass(task):
721
+ task = task()
722
+
723
+ assert isinstance(task, Task), \
724
+ OperationalException(
725
+ "Task object is not an instance of a Task"
726
+ )
727
+
728
+ self._tasks.append(task)
729
+
730
+ def add_tasks(self, tasks: List[Task]):
731
+ """
732
+ Function to add a list of tasks to the app. The tasks should be
733
+ instances of Task.
734
+
735
+ Args:
736
+ tasks: List of Task instances
737
+
738
+ Returns:
739
+ None
740
+ """
741
+ for task in tasks:
742
+ self.add_task(task)
743
+
618
744
  def _initialize_web(self):
619
745
  """
620
746
  Initialize the app for web mode by setting the configuration
@@ -630,241 +756,1459 @@ class App:
630
756
  - Algorithm
631
757
  """
632
758
  configuration_service = self.container.configuration_service()
633
- resource_dir = configuration_service.config[RESOURCE_DIRECTORY]
759
+ self._flask_app = create_flask_app(configuration_service)
634
760
 
635
- if resource_dir is None:
636
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = "sqlite://"
637
- else:
638
- resource_dir = self._create_resource_directory_if_not_exists()
639
- configuration_service.config[DATABASE_DIRECTORY_PATH] = \
640
- os.path.join(resource_dir, "databases")
641
- configuration_service.config[DATABASE_NAME] \
642
- = "prod-database.sqlite3"
643
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = \
644
- "sqlite:///" + os.path.join(
645
- configuration_service.config[DATABASE_DIRECTORY_PATH],
646
- configuration_service.config[DATABASE_NAME]
647
- )
648
- self._create_database_if_not_exists()
761
+ def get_portfolio_configurations(self):
762
+ portfolio_configuration_service = self.container \
763
+ .portfolio_configuration_service()
764
+ return portfolio_configuration_service.get_all()
649
765
 
650
- self._flask_app = create_flask_app(configuration_service.config)
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.
651
770
 
652
- def _create_resource_directory_if_not_exists(self):
771
+ Args:
772
+ market (str): The market to get the credential for
653
773
 
654
- if self._stateless:
655
- return
774
+ Returns:
775
+ MarketCredential: Instance of MarketCredential
776
+ """
656
777
 
657
- configuration_service = self.container.configuration_service()
658
- resource_dir = configuration_service.config.get(
659
- RESOURCE_DIRECTORY, None
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
660
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:
661
838
 
662
- if resource_dir is None:
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:
663
968
  raise OperationalException(
664
- "Resource directory is not specified. "
665
- "A resource directory is required for running a backtest."
969
+ "Either backtest_date_range or backtest_date_ranges must be "
970
+ "provided"
666
971
  )
667
972
 
668
- if not os.path.isdir(resource_dir):
669
- try:
670
- os.makedirs(resource_dir)
671
- except OSError as e:
672
- logger.error(e)
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:
673
981
  raise OperationalException(
674
- "Could not create resource directory"
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"
675
986
  )
676
987
 
677
- return resource_dir
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
+ )
678
995
 
679
- def _create_database_if_not_exists(self):
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
+ )
680
1050
 
681
- if self._stateless:
682
- return
1051
+ for strategy in backtests_ordered_by_strategy:
1052
+ backtests.append(
1053
+ combine_backtests(backtests_ordered_by_strategy[strategy])
1054
+ )
683
1055
 
684
- configuration_service = self.container.configuration_service()
685
- database_dir = configuration_service.config \
686
- .get(DATABASE_DIRECTORY_PATH, None)
1056
+ return backtests
687
1057
 
688
- if database_dir is None:
689
- return
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.
690
1077
 
691
- database_name = configuration_service.config.get(DATABASE_NAME, None)
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.
692
1123
 
693
- if database_name is None:
694
- return
1124
+ Returns:
1125
+ Backtest: Instance of Backtest
1126
+ """
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
+ )
695
1133
 
696
- database_path = os.path.join(database_dir, database_name)
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
1139
+ )
697
1140
 
698
- if not os.path.exists(database_path):
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()
699
1144
 
700
- if not os.path.isdir(database_dir):
701
- os.makedirs(database_dir)
1145
+ if risk_free_rate is None:
1146
+ raise OperationalException(
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"
1151
+ )
702
1152
 
703
- try:
704
- open(database_path, 'w').close()
705
- except OSError as e:
706
- logger.error(e)
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,
1181
+ )
1182
+ else:
1183
+ raise e
1184
+
1185
+ # Add the metadata to the backtest
1186
+ if metadata is None:
1187
+
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]:
1207
+ """
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.
1211
+
1212
+ Args:
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.
1226
+
1227
+ Returns:
1228
+ List[Backtest]: List of Backtest instances containing the results
1229
+ """
1230
+ backtests = []
1231
+
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:
1243
+ raise OperationalException(
1244
+ "No algorithms or strategy provided for backtesting"
1245
+ )
1246
+
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()
1250
+
1251
+ if risk_free_rate is None:
707
1252
  raise OperationalException(
708
- "Could not create database directory"
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"
709
1257
  )
710
1258
 
711
- def get_portfolio_configurations(self):
712
- return self.algorithm.get_portfolio_configurations()
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
713
1271
 
714
1272
  def run_backtest(
715
1273
  self,
716
- algorithm,
717
1274
  backtest_date_range: BacktestDateRange,
718
- pending_order_check_interval=None,
719
- output_directory=None
720
- ) -> BacktestReport:
1275
+ name: str = None,
1276
+ initial_amount=None,
1277
+ algorithm=None,
1278
+ strategy=None,
1279
+ strategies: List = None,
1280
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
1281
+ risk_free_rate: Optional[float] = None,
1282
+ metadata: Optional[Dict[str, str]] = None,
1283
+ ) -> Backtest:
721
1284
  """
722
- Run a backtest for an algorithm. This method should be called when
723
- running a backtest.
1285
+ Run a backtest for an algorithm.
724
1286
 
725
1287
  Args:
726
- algorithm: The algorithm to run a backtest for (instance of
727
- Algorithm)
728
1288
  backtest_date_range: The date range to run the backtest for
729
1289
  (instance of BacktestDateRange)
730
- pending_order_check_interval: The interval at which to check
731
- pending orders
732
- output_directory: The directory to write the backtest report to
1290
+ name: The name of the backtest. This is used to identify the
1291
+ backtest report in the output directory.
1292
+ initial_amount: The initial amount to start the backtest with.
1293
+ This will be the amount of trading currency that the backtest
1294
+ portfolio will start with.
1295
+ strategy (TradingStrategy) (Optional): The strategy object
1296
+ that needs to be backtested.
1297
+ strategies (List[TradingStrategy]) (Optional): List of strategy
1298
+ objects that need to be backtested
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.
1302
+ snapshot_interval (SnapshotInterval): The snapshot
1303
+ interval to use for the backtest. This is used to determine
1304
+ how often the portfolio snapshot should be taken during the
1305
+ backtest. The default is TRADE_CLOSE, which means that the
1306
+ portfolio snapshot will be taken at the end of each trade.
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.
733
1316
 
734
1317
  Returns:
735
- Instance of BacktestReport
1318
+ Backtest: Instance of Backtest
736
1319
  """
737
- logger.info("Initializing backtest")
738
- self.algorithm = algorithm
739
-
740
- self._initialize_app_for_backtest(
1320
+ self.initialize_backtest_config(
741
1321
  backtest_date_range=backtest_date_range,
742
- pending_order_check_interval=pending_order_check_interval,
1322
+ snapshot_interval=snapshot_interval,
1323
+ initial_amount=initial_amount
743
1324
  )
1325
+ self.initialize_storage(remove_database_if_exists=True)
1326
+ self.initialize_backtest_services()
1327
+ self.initialize_backtest_portfolios()
1328
+
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()
744
1332
 
745
- self._initialize_algorithm_for_backtest(
746
- algorithm=self.algorithm
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
+ )
1340
+
1341
+ algorithm = self.container.algorithm_factory().create_algorithm(
1342
+ name=name if name else self._name,
1343
+ strategies=(
1344
+ self._strategies if strategies is None else strategies
1345
+ ),
1346
+ algorithm=algorithm,
1347
+ strategy=strategy,
1348
+ tasks=self._tasks,
1349
+ on_strategy_run_hooks=self._on_strategy_run_hooks,
747
1350
  )
748
- backtest_service = self.container.backtest_service()
749
- backtest_service.resource_directory = self.config.get(
750
- RESOURCE_DIRECTORY
1351
+ self.initialize_data_sources_backtest(
1352
+ algorithm.data_sources, backtest_date_range
751
1353
  )
1354
+ backtest_service = self.container.backtest_service()
752
1355
 
753
- # Run the backtest with the backtest_service and collect the report
754
- report = backtest_service.run_backtest(
755
- algorithm=self.algorithm, backtest_date_range=backtest_date_range
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
756
1362
  )
757
- backtest_report_writer_service = self.container \
758
- .backtest_report_writer_service()
759
1363
 
760
- if output_directory is None:
761
- output_directory = os.path.join(
762
- self.config.get(RESOURCE_DIRECTORY),
763
- "backtest_reports"
764
- )
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
765
1389
 
766
- backtest_report_writer_service.write_report_to_json(
767
- report=report, output_directory=output_directory
1390
+ # Convert the current run to a backtest
1391
+ backtest = backtest_service.create_backtest(
1392
+ algorithm=algorithm,
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,
768
1396
  )
769
1397
 
770
- return report
1398
+ # Add the metadata to the backtest
1399
+ if metadata is None:
771
1400
 
772
- def run_backtests(
1401
+ if algorithm.metadata is not None:
1402
+ backtest.metadata = algorithm.metadata
1403
+ else:
1404
+ backtest.metadata = {}
1405
+ else:
1406
+ backtest.metadata = metadata
1407
+
1408
+ self.cleanup_backtest_resources()
1409
+ return backtest
1410
+
1411
+ def run_permutation_test(
773
1412
  self,
774
- algorithms,
775
- date_ranges: List[BacktestDateRange] = None,
776
- pending_order_check_interval=None,
777
- output_directory=None
778
- ) -> List[BacktestReport]:
779
- """
780
- Run a backtest for a set algorithm. This method should be called when
781
- running a backtest.
782
-
783
- :param algorithms: The algorithms to run backtests for (list of
784
- Algorithm instances)
785
- :param pending_order_check_interval: The interval at which to check
786
- :param date_ranges: The date ranges to run the backtests for (list of
787
- BacktestDateRange instances representing a start and end date)
788
- pending orders
789
- :param output_directory: The directory to write the backtest report to
790
- :return: List of BacktestReport intances
791
- """
792
- logger.info("Initializing backtests")
793
- reports = []
794
-
795
- for date_range in date_ranges:
796
- date_range: BacktestDateRange = date_range
797
- self._initialize_app_for_backtest(
798
- backtest_date_range=date_range,
799
- pending_order_check_interval=pending_order_check_interval,
800
- )
801
-
802
- print(
803
- f"{COLOR_YELLOW}Running backtests for date "
804
- f"range:{COLOR_RESET} {COLOR_GREEN}{date_range.name} "
805
- f"{date_range.start_date} - "
806
- f"{date_range.end_date} for a "
807
- f"total of {len(algorithms)} algorithms.{COLOR_RESET}"
808
- )
809
- for algorithm in algorithms:
810
- self._initialize_algorithm_for_backtest(algorithm)
811
- backtest_service = self.container.backtest_service()
812
- backtest_service.resource_directory = self.config.get(
813
- RESOURCE_DIRECTORY
814
- )
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.
1432
+
1433
+ Args:
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.
815
1456
 
816
- # Run the backtest with the backtest_service
817
- # and collect the report
818
- report = backtest_service.run_backtest(
819
- algorithm=algorithm, backtest_date_range=date_range
1457
+ Returns:
1458
+ Backtest: The backtest report containing the results of the
1459
+ main backtest and the p value from the permutation test.
1460
+ """
1461
+
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()
1465
+
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"
820
1472
  )
821
1473
 
822
- # Add date range name to report if present
823
- if date_range.name is not None:
824
- report.date_range_name = date_range.name
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)
825
1485
 
826
- backtest_report_writer_service = self.container \
827
- .backtest_report_writer_service()
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."
1490
+ )
828
1491
 
829
- if output_directory is None:
830
- output_directory = os.path.join(
831
- self.config.get(RESOURCE_DIRECTORY),
832
- "backtest_reports"
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 = {}
1498
+
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
833
1513
  )
834
1514
 
835
- backtest_report_writer_service.write_report_to_json(
836
- report=report, output_directory=output_directory
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,
1548
+ )
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
837
1553
  )
838
- reports.append(report)
839
1554
 
840
- return reports
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
+ )
841
1566
 
842
- def add_market_data_source(self, market_data_source):
843
- market_data_source.config = self.config
844
- self._market_data_source_service.add(market_data_source)
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
+ )
845
1571
 
846
- def add_market_credential(self, market_credential: MarketCredential):
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
1583
+
1584
+ def add_data_provider(self, data_provider, priority=3) -> None:
1585
+ """
1586
+ Function to add a data provider to the app. The data provider should
1587
+ be an instance of DataProvider or a DataProviderClass.
1588
+
1589
+ Args:
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).
1594
+
1595
+ Returns:
1596
+ None
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
+ )
1603
+
1604
+ data_provider = data_provider()
1605
+
1606
+ self._data_providers.append((data_provider, priority))
1607
+
1608
+ def add_market_credential(
1609
+ self, market_credential: MarketCredential
1610
+ ) -> None:
1611
+ """
1612
+ Function to add a market credential to the app. The market
1613
+ credential should be an instance of MarketCredential.
1614
+
1615
+ Args:
1616
+ market_credential:
1617
+
1618
+ Returns:
1619
+ None
1620
+ """
847
1621
  market_credential.market = market_credential.market.upper()
848
- self._market_credential_service.add(market_credential)
1622
+ market_credential_service = self.container \
1623
+ .market_credential_service()
1624
+ market_credential_service.add(market_credential)
849
1625
 
850
- def on_initialize(self, app_hook: AppHook):
1626
+ def on_initialize(self, app_hook):
851
1627
  """
852
1628
  Function to add a hook that runs when the app is initialized. The hook
853
1629
  should be an instance of AppHook.
1630
+
1631
+ Args:
1632
+ app_hook: Instance of AppHook
1633
+
1634
+ Returns:
1635
+ None
854
1636
  """
855
1637
 
1638
+ # Check if the app_hook inherits from AppHook
1639
+ if not issubclass(app_hook, AppHook):
1640
+ raise OperationalException(
1641
+ "App hook should be an instance of AppHook"
1642
+ )
1643
+
856
1644
  if inspect.isclass(app_hook):
857
1645
  app_hook = app_hook()
858
1646
 
859
1647
  self._on_initialize_hooks.append(app_hook)
860
1648
 
861
- def after_initialize(self, app_hook: AppHook):
1649
+ def on_strategy_run(self, app_hook):
862
1650
  """
863
- Function to add a hook that runs after the app is initialized. The hook
1651
+ Function to add a hook that runs when a strategy is run. The hook
864
1652
  should be an instance of AppHook.
865
1653
  """
866
1654
 
1655
+ # Check if the app_hook inherits from AppHook
1656
+ if inspect.isclass(app_hook) and not issubclass(app_hook, AppHook):
1657
+ raise OperationalException(
1658
+ "App hook should be an instance of AppHook"
1659
+ )
1660
+
1661
+ if inspect.isclass(app_hook):
1662
+ app_hook = app_hook()
1663
+
1664
+ self._on_strategy_run_hooks.append(app_hook)
1665
+
1666
+ def after_initialize(self, app_hook: AppHook):
1667
+ """
1668
+ Function to add a hook that runs after the app is initialized.
1669
+ The hook should be an instance of AppHook.
1670
+ """
1671
+
867
1672
  if inspect.isclass(app_hook):
868
1673
  app_hook = app_hook()
869
1674
 
870
1675
  self._on_after_initialize_hooks.append(app_hook)
1676
+
1677
+ def strategy(
1678
+ self,
1679
+ function=None,
1680
+ time_unit=TimeUnit.MINUTE,
1681
+ interval=10,
1682
+ data_sources=None
1683
+ ):
1684
+ """
1685
+ Decorator for registering a strategy. This decorator can be used
1686
+ to define a trading strategy function and register it in your
1687
+ application.
1688
+
1689
+ Args:
1690
+ function: The wrapped function to should be converted to
1691
+ a TradingStrategy
1692
+ time_unit (TimeUnit): instance of TimeUnit Enum
1693
+ interval (int): interval of the schedule ( interval - TimeUnit )
1694
+ data_sources (List): List of data sources that the
1695
+ trading strategy function uses.
1696
+
1697
+ Returns:
1698
+ Function
1699
+ """
1700
+ from .strategy import TradingStrategy
1701
+
1702
+ if function:
1703
+ strategy_object = TradingStrategy(
1704
+ decorated=function,
1705
+ time_unit=time_unit,
1706
+ interval=interval,
1707
+ data_sources=data_sources
1708
+ )
1709
+ self.add_strategy(strategy_object)
1710
+ return strategy_object
1711
+ else:
1712
+
1713
+ def wrapper(f):
1714
+ self.add_strategy(
1715
+ TradingStrategy(
1716
+ decorated=f,
1717
+ time_unit=time_unit,
1718
+ interval=interval,
1719
+ data_sources=data_sources,
1720
+ worker_id=f.__name__
1721
+ )
1722
+ )
1723
+ return f
1724
+
1725
+ return wrapper
1726
+
1727
+ def add_strategies(self, strategies, throw_exception=True) -> None:
1728
+ """
1729
+ Function to add strategies to the app
1730
+ Args:
1731
+ strategies (List(TradingStrategy)): List of trading strategies that
1732
+ need to be registered.
1733
+ throw_exception (boolean): Flag to specify if an exception
1734
+ can be thrown if the strategies are not in the format or type
1735
+ that the application expects
1736
+
1737
+ Returns:
1738
+ None
1739
+ """
1740
+
1741
+ if strategies is not None:
1742
+ for strategy in strategies:
1743
+ self.add_strategy(strategy, throw_exception=throw_exception)
1744
+
1745
+ def add_strategy(self, strategy, throw_exception=True) -> None:
1746
+ """
1747
+ Function to add a strategy to the app. The strategy should be an
1748
+ instance of TradingStrategy or a subclass based on the TradingStrategy
1749
+ class.
1750
+
1751
+ Args:
1752
+ strategy: Instance of TradingStrategy
1753
+ throw_exception: Flag to allow for throwing an exception when
1754
+ the provided strategy is not inline with what the application
1755
+ expects.
1756
+
1757
+ Returns:
1758
+ None
1759
+ """
1760
+
1761
+ logger.info("Adding strategy")
1762
+
1763
+ if inspect.isclass(strategy):
1764
+
1765
+ if not issubclass(strategy, TradingStrategy):
1766
+ raise OperationalException(
1767
+ "The strategy must be a subclass of TradingStrategy"
1768
+ )
1769
+
1770
+ strategy = strategy()
1771
+
1772
+ if not isinstance(strategy, TradingStrategy):
1773
+
1774
+ if throw_exception:
1775
+ raise OperationalException(
1776
+ "Strategy should be an instance of TradingStrategy"
1777
+ )
1778
+ else:
1779
+ return
1780
+
1781
+ has_duplicates = False
1782
+
1783
+ for i in range(len(self._strategies)):
1784
+ for j in range(i + 1, len(self._strategies)):
1785
+ if self._strategies[i].worker_id == strategy.worker_id:
1786
+ has_duplicates = True
1787
+ break
1788
+
1789
+ if has_duplicates:
1790
+ raise OperationalException(
1791
+ "Can't add strategy, there already exists a strategy "
1792
+ "with the same id in the algorithm"
1793
+ )
1794
+
1795
+ self._strategies.append(strategy)
1796
+
1797
+ def add_state_handler(self, state_handler):
1798
+ """
1799
+ Function to add a state handler to the app. The state handler should
1800
+ be an instance of StateHandler.
1801
+
1802
+ Args:
1803
+ state_handler: Instance of StateHandler
1804
+
1805
+ Returns:
1806
+ None
1807
+ """
1808
+
1809
+ if inspect.isclass(state_handler):
1810
+ state_handler = state_handler()
1811
+
1812
+ if not isinstance(state_handler, StateHandler):
1813
+ raise OperationalException(
1814
+ "State handler should be an instance of StateHandler"
1815
+ )
1816
+
1817
+ self._state_handler = state_handler
1818
+
1819
+ def add_market(
1820
+ self,
1821
+ market,
1822
+ trading_symbol,
1823
+ api_key=None,
1824
+ secret_key=None,
1825
+ initial_balance=None
1826
+ ):
1827
+ """
1828
+ Function to add a market to the app. This function is a utility
1829
+ function to add a portfolio configuration and market credential
1830
+ to the app.
1831
+
1832
+ Args:
1833
+ market: String representing the market name
1834
+ trading_symbol: Trading symbol for the portfolio
1835
+ api_key: API key for the market
1836
+ secret_key: Secret key for the market
1837
+ initial_balance: Initial balance for the market
1838
+
1839
+ Returns:
1840
+ None
1841
+ """
1842
+
1843
+ portfolio_configuration = PortfolioConfiguration(
1844
+ market=market,
1845
+ trading_symbol=trading_symbol,
1846
+ initial_balance=initial_balance
1847
+ )
1848
+
1849
+ self.add_portfolio_configuration(portfolio_configuration)
1850
+ market_credential = MarketCredential(
1851
+ market=market,
1852
+ api_key=api_key,
1853
+ secret_key=secret_key
1854
+ )
1855
+ self.add_market_credential(market_credential)
1856
+
1857
+ def add_order_executor(self, order_executor):
1858
+ """
1859
+ Function to add an order executor to the app. The order executor
1860
+ should be an instance of OrderExecutor.
1861
+
1862
+ Args:
1863
+ order_executor: Instance of OrderExecutor
1864
+
1865
+ Returns:
1866
+ None
1867
+ """
1868
+
1869
+ if inspect.isclass(order_executor):
1870
+ order_executor = order_executor()
1871
+
1872
+ if not isinstance(order_executor, OrderExecutor):
1873
+ raise OperationalException(
1874
+ "Order executor should be an instance of OrderExecutor"
1875
+ )
1876
+
1877
+ order_executor_lookup = self.container.order_executor_lookup()
1878
+ order_executor_lookup.add_order_executor(
1879
+ order_executor=order_executor
1880
+ )
1881
+
1882
+ def get_order_executors(self):
1883
+ """
1884
+ Function to get all order executors from the app. This method
1885
+ should be called when you want to get all order executors.
1886
+
1887
+ Returns:
1888
+ List of OrderExecutor instances
1889
+ """
1890
+ order_executor_lookup = self.container.order_executor_lookup()
1891
+ return order_executor_lookup.get_all()
1892
+
1893
+ def add_portfolio_provider(self, portfolio_provider):
1894
+ """
1895
+ Function to add a portfolio provider to the app. The portfolio
1896
+ provider should be an instance of PortfolioProvider.
1897
+
1898
+ Args:
1899
+ portfolio_provider: Instance of PortfolioProvider
1900
+
1901
+ Returns:
1902
+ None
1903
+ """
1904
+
1905
+ if inspect.isclass(portfolio_provider):
1906
+ portfolio_provider = portfolio_provider()
1907
+
1908
+ if not isinstance(portfolio_provider, PortfolioProvider):
1909
+ raise OperationalException(
1910
+ "Portfolio provider should be an instance of "
1911
+ "PortfolioProvider"
1912
+ )
1913
+
1914
+ portfolio_provider_lookup = self.container.portfolio_provider_lookup()
1915
+ portfolio_provider_lookup.add_portfolio_provider(
1916
+ portfolio_provider=portfolio_provider
1917
+ )
1918
+
1919
+ def get_portfolio_providers(self):
1920
+ """
1921
+ Function to get all portfolio providers from the app. This method
1922
+ should be called when you want to get all portfolio providers.
1923
+
1924
+ Returns:
1925
+ List of PortfolioProvider instances
1926
+ """
1927
+ portfolio_provider_lookup = self.container.portfolio_provider_lookup()
1928
+ return portfolio_provider_lookup.get_all()
1929
+
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):
1961
+ """
1962
+ Function to initialize the portfolios. This function will
1963
+ first check if the app is running in backtest mode or not. If it is
1964
+ running in backtest mode, it will create the portfolios with the
1965
+ initial amount specified in the config. If it is not running in
1966
+ backtest mode, it will check if there are
1967
+
1968
+ """
1969
+ logger.info("Initializing portfolios")
1970
+ portfolio_configuration_service = self.container \
1971
+ .portfolio_configuration_service()
1972
+ portfolio_service = self.container.portfolio_service()
1973
+
1974
+ # Throw an error if no portfolios are configured
1975
+ if portfolio_configuration_service.count() == 0:
1976
+ raise OperationalException("No portfolios configured")
1977
+
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()
1984
+
1985
+ if len(portfolios) > 0:
1986
+
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
1997
+ )
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.)"
2010
+ )
2011
+
2012
+ # Check if the portfolio configuration is still inline
2013
+ # with the initial balance
2014
+
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
+ )
2023
+
2024
+ # Register a portfolio provider for the portfolio
2025
+ portfolio_provider_lookup \
2026
+ .register_portfolio_provider_for_market(
2027
+ portfolio_configuration.market
2028
+ )
2029
+ initial_balance = portfolio_configuration\
2030
+ .initial_balance
2031
+
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
+ )
2049
+
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(
2059
+ portfolio_configuration.market
2060
+ )
2061
+
2062
+ # Register an order executor for the portfolio
2063
+ order_executor_lookup.register_order_executor_for_market(
2064
+ portfolio_configuration.market
2065
+ )
2066
+
2067
+ market_credential = \
2068
+ market_credential_service.get(
2069
+ portfolio_configuration.market
2070
+ )
2071
+
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
+ )
2079
+
2080
+ if not portfolio_service.exists(
2081
+ {"identifier": portfolio_configuration.identifier}
2082
+ ):
2083
+ portfolio_service.create_portfolio_from_configuration(
2084
+ portfolio_configuration
2085
+ )
2086
+
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()
2091
+
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)
2096
+
2097
+ def initialize_backtest_portfolios(self):
2098
+ """
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.
2103
+
2104
+ Returns:
2105
+ None
2106
+ """
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
2120
+ )
2121
+
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):
2133
+ """
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.
2138
+
2139
+ Returns:
2140
+ None
2141
+ """
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
2159
+
2160
+ def get_run_history(self):
2161
+ """
2162
+ Function to get the run history of the app. This function will
2163
+ return the history of the run schedule of all the strategies,
2164
+ and tasks that have been registered in the app.
2165
+
2166
+ Returns:
2167
+ dict: The run history of the app
2168
+ """
2169
+ return self._run_history
2170
+
2171
+ def has_run(self, worker_id) -> bool:
2172
+ """
2173
+ Function to check if a worker has run in the app. This function
2174
+ will check if the worker_id is present in the run history of the app.
2175
+
2176
+ Args:
2177
+ worker_id:
2178
+
2179
+ Returns:
2180
+ Boolean: True if the worker has run, False otherwise
2181
+ """
2182
+ if self._run_history is None:
2183
+ return False
2184
+
2185
+ return worker_id in self._run_history
2186
+
2187
+ def get_algorithm(self):
2188
+ """
2189
+ Function to get the algorithm that is currently running in the app.
2190
+ This function will return the algorithm that is currently running
2191
+ in the app.
2192
+
2193
+ Returns:
2194
+ Algorithm: The algorithm that is currently running in the app
2195
+ """
2196
+ algorithm_factory = self.container.algorithm_factory()
2197
+ return algorithm_factory.create_algorithm(
2198
+ name=self._name,
2199
+ strategies=self._strategies,
2200
+ tasks=self._tasks,
2201
+ on_strategy_run_hooks=self._on_strategy_run_hooks,
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)