investing-algorithm-framework 7.19.14__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (260) hide show
  1. investing_algorithm_framework/__init__.py +197 -0
  2. investing_algorithm_framework/app/__init__.py +47 -0
  3. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  4. investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
  5. investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
  6. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  7. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  8. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  9. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  10. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  11. investing_algorithm_framework/app/app.py +2204 -0
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1667 -0
  14. investing_algorithm_framework/app/eventloop.py +590 -0
  15. investing_algorithm_framework/app/reporting/__init__.py +27 -0
  16. investing_algorithm_framework/app/reporting/ascii.py +921 -0
  17. investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
  18. investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
  19. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  20. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  21. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
  22. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  23. investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
  24. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  25. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
  26. investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
  27. investing_algorithm_framework/app/reporting/generate.py +185 -0
  28. investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
  29. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
  30. investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
  31. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
  32. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  33. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  34. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  35. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  36. investing_algorithm_framework/app/stateless/__init__.py +35 -0
  37. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
  38. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
  39. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
  40. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
  41. investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
  42. investing_algorithm_framework/app/strategy.py +675 -0
  43. investing_algorithm_framework/app/task.py +41 -0
  44. investing_algorithm_framework/app/web/__init__.py +5 -0
  45. investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
  46. investing_algorithm_framework/app/web/controllers/orders.py +20 -0
  47. investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
  48. investing_algorithm_framework/app/web/controllers/positions.py +18 -0
  49. investing_algorithm_framework/app/web/create_app.py +20 -0
  50. investing_algorithm_framework/app/web/error_handler.py +59 -0
  51. investing_algorithm_framework/app/web/responses.py +20 -0
  52. investing_algorithm_framework/app/web/run_strategies.py +4 -0
  53. investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
  54. investing_algorithm_framework/app/web/schemas/order.py +12 -0
  55. investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
  56. investing_algorithm_framework/app/web/schemas/position.py +15 -0
  57. investing_algorithm_framework/app/web/setup_cors.py +6 -0
  58. investing_algorithm_framework/cli/__init__.py +0 -0
  59. investing_algorithm_framework/cli/cli.py +207 -0
  60. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
  61. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  62. investing_algorithm_framework/cli/initialize_app.py +603 -0
  63. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  64. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  65. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  66. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  67. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  68. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  69. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  70. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  71. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  72. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  73. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  74. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  75. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  76. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  77. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  78. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  79. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  80. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  81. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  82. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  83. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  84. investing_algorithm_framework/create_app.py +54 -0
  85. investing_algorithm_framework/dependency_container.py +155 -0
  86. investing_algorithm_framework/domain/__init__.py +148 -0
  87. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  88. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  89. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  90. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  91. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  92. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  93. investing_algorithm_framework/domain/backtesting/backtest_run.py +435 -0
  94. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  95. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  96. investing_algorithm_framework/domain/config.py +111 -0
  97. investing_algorithm_framework/domain/constants.py +83 -0
  98. investing_algorithm_framework/domain/data_provider.py +334 -0
  99. investing_algorithm_framework/domain/data_structures.py +42 -0
  100. investing_algorithm_framework/domain/decimal_parsing.py +40 -0
  101. investing_algorithm_framework/domain/exceptions.py +112 -0
  102. investing_algorithm_framework/domain/models/__init__.py +43 -0
  103. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  104. investing_algorithm_framework/domain/models/base_model.py +25 -0
  105. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  106. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  107. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  108. investing_algorithm_framework/domain/models/event.py +35 -0
  109. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  110. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  111. investing_algorithm_framework/domain/models/order/__init__.py +6 -0
  112. investing_algorithm_framework/domain/models/order/order.py +384 -0
  113. investing_algorithm_framework/domain/models/order/order_side.py +36 -0
  114. investing_algorithm_framework/domain/models/order/order_status.py +37 -0
  115. investing_algorithm_framework/domain/models/order/order_type.py +30 -0
  116. investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
  117. investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
  118. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
  119. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
  120. investing_algorithm_framework/domain/models/position/__init__.py +4 -0
  121. investing_algorithm_framework/domain/models/position/position.py +68 -0
  122. investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
  123. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  124. investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
  125. investing_algorithm_framework/domain/models/time_frame.py +153 -0
  126. investing_algorithm_framework/domain/models/time_interval.py +124 -0
  127. investing_algorithm_framework/domain/models/time_unit.py +149 -0
  128. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  129. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  130. investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
  131. investing_algorithm_framework/domain/models/trade/trade.py +388 -0
  132. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
  133. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  134. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
  135. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
  136. investing_algorithm_framework/domain/order_executor.py +112 -0
  137. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  138. investing_algorithm_framework/domain/positions/__init__.py +4 -0
  139. investing_algorithm_framework/domain/positions/position_size.py +41 -0
  140. investing_algorithm_framework/domain/services/__init__.py +11 -0
  141. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  142. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  143. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  144. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  145. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  146. investing_algorithm_framework/domain/stateless_actions.py +7 -0
  147. investing_algorithm_framework/domain/strategy.py +44 -0
  148. investing_algorithm_framework/domain/utils/__init__.py +27 -0
  149. investing_algorithm_framework/domain/utils/csv.py +104 -0
  150. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  151. investing_algorithm_framework/domain/utils/dates.py +57 -0
  152. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  153. investing_algorithm_framework/domain/utils/polars.py +53 -0
  154. investing_algorithm_framework/domain/utils/random.py +41 -0
  155. investing_algorithm_framework/domain/utils/signatures.py +17 -0
  156. investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
  157. investing_algorithm_framework/domain/utils/synchronized.py +12 -0
  158. investing_algorithm_framework/download_data.py +108 -0
  159. investing_algorithm_framework/infrastructure/__init__.py +50 -0
  160. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  161. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
  162. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  163. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  164. investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
  165. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
  166. investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
  167. investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
  168. investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
  169. investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
  170. investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
  171. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  172. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  173. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
  174. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
  175. investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
  176. investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
  177. investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
  178. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
  179. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  180. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  181. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
  182. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
  183. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  184. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  185. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  186. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  187. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  188. investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
  189. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  190. investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
  191. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
  192. investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
  193. investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
  194. investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
  195. investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
  196. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  197. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
  198. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
  199. investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
  200. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  201. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
  202. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  203. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  204. investing_algorithm_framework/services/__init__.py +132 -0
  205. investing_algorithm_framework/services/backtesting/__init__.py +5 -0
  206. investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
  207. investing_algorithm_framework/services/configuration_service.py +96 -0
  208. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  209. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  210. investing_algorithm_framework/services/market_credential_service.py +40 -0
  211. investing_algorithm_framework/services/metrics/__init__.py +114 -0
  212. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  213. investing_algorithm_framework/services/metrics/beta.py +0 -0
  214. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  215. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  216. investing_algorithm_framework/services/metrics/drawdown.py +181 -0
  217. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  218. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  219. investing_algorithm_framework/services/metrics/generate.py +358 -0
  220. investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
  221. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  222. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  223. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  224. investing_algorithm_framework/services/metrics/returns.py +452 -0
  225. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  226. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  227. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  228. investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
  229. investing_algorithm_framework/services/metrics/trades.py +500 -0
  230. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  231. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  232. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  233. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  234. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  235. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  236. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  237. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  238. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  239. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  240. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  241. investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
  242. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  243. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  244. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  245. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  246. investing_algorithm_framework/services/positions/__init__.py +7 -0
  247. investing_algorithm_framework/services/positions/position_service.py +210 -0
  248. investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
  249. investing_algorithm_framework/services/repository_service.py +40 -0
  250. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  251. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
  252. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
  253. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
  254. investing_algorithm_framework/services/trade_service/__init__.py +3 -0
  255. investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
  256. investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
  257. investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
  258. investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
  259. investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
  260. investing_algorithm_framework-7.19.14.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,2204 @@
1
+ import inspect
2
+ import logging
3
+ import os
4
+ import threading
5
+ from datetime import datetime, timezone, timedelta
6
+ from typing import List, Optional, Any, Dict, Tuple
7
+
8
+ from flask import Flask
9
+
10
+ from investing_algorithm_framework.app.algorithm import Algorithm
11
+ from investing_algorithm_framework.app.strategy import TradingStrategy
12
+ from investing_algorithm_framework.app.task import Task
13
+ from investing_algorithm_framework.app.web import create_flask_app
14
+ from investing_algorithm_framework.domain import DATABASE_NAME, TimeUnit, \
15
+ DATABASE_DIRECTORY_PATH, RESOURCE_DIRECTORY, ENVIRONMENT, Environment, \
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
24
+ from investing_algorithm_framework.infrastructure import setup_sqlalchemy, \
25
+ create_all_tables, CCXTOrderExecutor, CCXTPortfolioProvider, \
26
+ BacktestOrderExecutor, CCXTOHLCVDataProvider, clear_db, \
27
+ PandasOHLCVDataProvider
28
+ from investing_algorithm_framework.services import OrderBacktestService, \
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
+
35
+
36
+ logger = logging.getLogger("investing_algorithm_framework")
37
+ COLOR_RESET = '\033[0m'
38
+ COLOR_GREEN = '\033[92m'
39
+ COLOR_YELLOW = '\033[93m'
40
+
41
+
42
+ class App:
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):
63
+ self._flask_app: Optional[Flask] = None
64
+ self.container = None
65
+ self._started = False
66
+ self._tasks = []
67
+ self._strategies = []
68
+ self._data_providers: List[Tuple[DataProvider, int]] = []
69
+ self._on_initialize_hooks = []
70
+ self._on_strategy_run_hooks = []
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
76
+
77
+ @property
78
+ def context(self):
79
+ return self.container.context()
80
+
81
+ @property
82
+ def resource_directory_path(self):
83
+ """
84
+ Returns the resource directory path from the configuration.
85
+ This directory is used to store resources such as market data,
86
+ database files, and other resources required by the app.
87
+ """
88
+ config = self.config
89
+ resource_directory_path = config.get(RESOURCE_DIRECTORY, None)
90
+
91
+ # Check if the resource directory is set
92
+ if resource_directory_path is None:
93
+ logger.info(
94
+ "Resource directory not set, setting" +
95
+ " to current working directory"
96
+ )
97
+ resource_directory_path = os.path.join(os.getcwd(), "resources")
98
+ configuration_service = self.container.configuration_service()
99
+ configuration_service.add_value(
100
+ RESOURCE_DIRECTORY, resource_directory_path
101
+ )
102
+
103
+ return resource_directory_path
104
+
105
+ @property
106
+ def database_directory_path(self):
107
+ """
108
+ Returns the database directory path from the configuration.
109
+ This directory is used to store database files required by the app.
110
+ """
111
+ config = self.config
112
+ database_directory_path = config.get(DATABASE_DIRECTORY_PATH, None)
113
+
114
+ # Check if the database directory is set
115
+ if database_directory_path is None:
116
+ logger.info(
117
+ "Database directory not set, setting" +
118
+ " to current working directory"
119
+ )
120
+ resource_directory_path = self.resource_directory_path
121
+ database_directory_path = os.path.join(
122
+ resource_directory_path, "databases"
123
+ )
124
+ configuration_service = self.container.configuration_service()
125
+ configuration_service.add_value(
126
+ DATABASE_DIRECTORY_PATH, database_directory_path
127
+ )
128
+
129
+ return database_directory_path
130
+
131
+ @property
132
+ def name(self):
133
+ return self._name
134
+
135
+ @name.setter
136
+ def name(self, name):
137
+ self._name = name
138
+
139
+ @property
140
+ def started(self):
141
+ return self._started
142
+
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
152
+
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
159
+
160
+ Returns:
161
+ None
162
+ """
163
+ configuration_service = self.container.configuration_service()
164
+ configuration_service.initialize_from_dict(config)
165
+
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.
170
+
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.
174
+
175
+ Args:
176
+ algorithm (Algorithm): The algorithm to add to the app.
177
+ This should be an instance of Algorithm.
178
+
179
+ Returns:
180
+ None
181
+ """
182
+ self.add_strategies(algorithm.strategies)
183
+ self.add_tasks(algorithm.tasks)
184
+
185
+ def add_trade_order_evaluator(self, trade_order_evaluator):
186
+ """
187
+ Function to add a trade order evaluator to the app. This is used
188
+ to evaluate trades and orders based on OHLCV data.
189
+
190
+ Args:
191
+ trade_order_evaluator: The trade order evaluator to add to the app.
192
+ This should be an instance of TradeOrderEvaluator.
193
+
194
+ Returns:
195
+ None
196
+ """
197
+ self._trade_order_evaluator = trade_order_evaluator
198
+
199
+ def set_config(self, key: str, value: Any) -> None:
200
+ """
201
+ Function to add a key-value pair to the app's configuration.
202
+
203
+ Args:
204
+ key (string): The key to add to the configuration
205
+ value (any): The value to add to the configuration
206
+
207
+ Returns:
208
+ None
209
+ """
210
+ configuration_service = self.container.configuration_service()
211
+ configuration_service.add_value(key, value)
212
+
213
+ def set_config_with_dict(self, config: dict) -> None:
214
+ """
215
+ Function to set the configuration for the app with a dictionary.
216
+ This is useful for setting multiple configuration values at once.
217
+
218
+ Args:
219
+ config (dict): A dictionary containing the configuration
220
+
221
+ Returns:
222
+ None
223
+ """
224
+ configuration_service = self.container.configuration_service()
225
+ configuration_service.initialize_from_dict(config)
226
+
227
+ def initialize_config(self):
228
+ """
229
+ Function to initialize the configuration for the app. This method
230
+ should be called before running the algorithm.
231
+
232
+ Returns:
233
+ None
234
+ """
235
+ data = {
236
+ ENVIRONMENT: self.config.get(ENVIRONMENT, Environment.PROD.value),
237
+ DATABASE_DIRECTORY_NAME: "databases",
238
+ LAST_SNAPSHOT_DATETIME: None
239
+ }
240
+ configuration_service = self.container.configuration_service()
241
+ configuration_service.initialize_from_dict(data)
242
+ config = configuration_service.get_config()
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
+ )
248
+
249
+ if Environment.TEST.equals(config[ENVIRONMENT]):
250
+ configuration_service.add_value(
251
+ DATABASE_NAME, "test-database.sqlite3"
252
+ )
253
+ elif Environment.PROD.equals(config[ENVIRONMENT]):
254
+ configuration_service.add_value(
255
+ DATABASE_NAME, "prod-database.sqlite3"
256
+ )
257
+ else:
258
+ configuration_service.add_value(
259
+ DATABASE_NAME, "dev-database.sqlite3"
260
+ )
261
+
262
+ resource_dir = config[RESOURCE_DIRECTORY]
263
+ database_dir_name = config.get(DATABASE_DIRECTORY_NAME)
264
+ configuration_service.add_value(
265
+ DATABASE_DIRECTORY_PATH,
266
+ os.path.join(resource_dir, database_dir_name)
267
+ )
268
+ config = configuration_service.get_config()
269
+
270
+ if SQLALCHEMY_DATABASE_URI not in config \
271
+ or config[SQLALCHEMY_DATABASE_URI] is None:
272
+ path = "sqlite:///" + os.path.join(
273
+ configuration_service.config[DATABASE_DIRECTORY_PATH],
274
+ configuration_service.config[DATABASE_NAME]
275
+ )
276
+ configuration_service.add_value(SQLALCHEMY_DATABASE_URI, path)
277
+
278
+ def initialize_backtest_config(
279
+ self,
280
+ backtest_date_range: BacktestDateRange,
281
+ initial_amount=None,
282
+ snapshot_interval: SnapshotInterval = None
283
+ ):
284
+ """
285
+ Function to initialize the configuration for the app in backtest mode.
286
+ This method should be called before running the algorithm in backtest
287
+ mode. It sets the environment to BACKTEST and initializes the
288
+ configuration accordingly.
289
+
290
+ Args:
291
+ backtest_date_range (BacktestDateRange): The date range for the
292
+ backtest. This should be an instance of BacktestDateRange.
293
+ initial_amount (float): The initial amount to start the backtest
294
+ with. This will be the amount of trading currency that the
295
+ backtest portfolio will start with.
296
+ snapshot_interval (SnapshotInterval): The snapshot interval to
297
+ use for the backtest. This is used to determine how often the
298
+ portfolio snapshot should be taken during the backtest.
299
+
300
+ Returns:
301
+ None
302
+ """
303
+ logger.info("Initializing backtest configuration")
304
+ data = {
305
+ ENVIRONMENT: Environment.BACKTEST.value,
306
+ BACKTESTING_START_DATE: backtest_date_range.start_date,
307
+ BACKTESTING_END_DATE: backtest_date_range.end_date,
308
+ DATABASE_NAME: "backtest-database.sqlite3",
309
+ DATABASE_DIRECTORY_NAME: "backtest_databases",
310
+ DATABASE_DIRECTORY_PATH: os.path.join(
311
+ self.resource_directory_path,
312
+ "backtest_databases"
313
+ ),
314
+ BACKTESTING_INITIAL_AMOUNT: initial_amount,
315
+ INDEX_DATETIME: backtest_date_range.start_date,
316
+ LAST_SNAPSHOT_DATETIME: None,
317
+ BACKTESTING_FLAG: True
318
+ }
319
+ configuration_service = self.container.configuration_service()
320
+ configuration_service.initialize_from_dict(data)
321
+
322
+ if snapshot_interval is not None:
323
+ configuration_service.add_value(
324
+ SNAPSHOT_INTERVAL,
325
+ SnapshotInterval.from_value(snapshot_interval).value
326
+ )
327
+
328
+ def initialize_storage(self, remove_database_if_exists: bool = False):
329
+ """
330
+ Function to initialize the storage for the app. The given
331
+ resource directory will be created if it does not exist.
332
+ The database directory will also be created if it does not
333
+ exist.
334
+ """
335
+ resource_directory_path = self.resource_directory_path
336
+
337
+ if not os.path.exists(resource_directory_path):
338
+ os.makedirs(resource_directory_path)
339
+ logger.info(
340
+ f"Resource directory created at {resource_directory_path}"
341
+ )
342
+
343
+ database_directory_path = self.database_directory_path
344
+
345
+ if not os.path.exists(database_directory_path):
346
+ os.makedirs(database_directory_path)
347
+ logger.info(
348
+ f"Database directory created at {database_directory_path}"
349
+ )
350
+
351
+ database_path = os.path.join(
352
+ database_directory_path, self.config[DATABASE_NAME]
353
+ )
354
+
355
+ if remove_database_if_exists:
356
+
357
+ if os.path.exists(database_path):
358
+ logger.info(
359
+ f"Removing existing database at {database_path}"
360
+ )
361
+ os.remove(database_path)
362
+
363
+ # Create the sqlalchemy database uri
364
+ path = f"sqlite:///{database_path}"
365
+ self.set_config(SQLALCHEMY_DATABASE_URI, path)
366
+
367
+ # Setup sql if needed
368
+ setup_sqlalchemy(self)
369
+ create_all_tables()
370
+
371
+ def initialize_data_sources(
372
+ self,
373
+ data_sources: List[DataSource],
374
+ ):
375
+ """
376
+ Function to initialize the data sources for the app. This method
377
+ should be called before running the algorithm. This method
378
+ initializes all data sources so that they are ready to be used.
379
+
380
+ Args:
381
+ data_sources (List[DataSource]): The data sources to initialize.
382
+ This should be a list of DataSource instances.
383
+
384
+ Returns:
385
+ None
386
+ """
387
+ logger.info("Initializing data sources")
388
+
389
+ if data_sources is None or len(data_sources) == 0:
390
+ return
391
+
392
+ data_provider_service = self.container.data_provider_service()
393
+ data_provider_service.reset()
394
+
395
+ for data_provider_tuple in self._data_providers:
396
+ data_provider_service.add_data_provider(
397
+ data_provider_tuple[0], priority=data_provider_tuple[1]
398
+ )
399
+
400
+ # Add the default data providers
401
+ data_provider_service.add_data_provider(CCXTOHLCVDataProvider())
402
+
403
+ # Initialize all data sources
404
+ data_provider_service.index_data_providers(data_sources)
405
+
406
+ def initialize_data_sources_backtest(
407
+ self,
408
+ data_sources: List[DataSource],
409
+ backtest_date_range: BacktestDateRange,
410
+ show_progress: bool = True
411
+ ):
412
+ """
413
+ Function to initialize the data sources for the app in backtest mode.
414
+ This method should be called before running the algorithm in backtest
415
+ mode. It initializes all data sources so that they are
416
+ ready to be used.
417
+
418
+ Args:
419
+ data_sources (List[DataSource]): The data sources to initialize.
420
+ backtest_date_range (BacktestDateRange): The date range for the
421
+ backtest. This should be an instance of BacktestDateRange.
422
+ show_progress (bool): Whether to show a progress bar when
423
+ preparing the backtest data for each data provider.
424
+
425
+ Returns:
426
+ None
427
+ """
428
+ logger.info("Initializing data sources for backtest")
429
+
430
+ if data_sources is None or len(data_sources) == 0:
431
+ return
432
+
433
+ data_provider_service = self.container.data_provider_service()
434
+ data_provider_service.reset()
435
+
436
+ for data_provider_tuple in self._data_providers:
437
+ data_provider_service.add_data_provider(
438
+ data_provider_tuple[0], priority=data_provider_tuple[1]
439
+ )
440
+
441
+ # Add the default data providers
442
+ data_provider_service.add_data_provider(CCXTOHLCVDataProvider())
443
+
444
+ # Initialize all data sources
445
+ data_provider_service.index_backtest_data_providers(
446
+ data_sources, backtest_date_range, show_progress=show_progress
447
+ )
448
+
449
+ description = "Preparing backtest data for all data sources"
450
+ data_providers = data_provider_service.data_provider_index.get_all()
451
+
452
+ # Prepare the backtest data for each data provider
453
+ if not show_progress:
454
+ for _, data_provider in data_providers:
455
+ data_provider.prepare_backtest_data(
456
+ backtest_start_date=backtest_date_range.start_date,
457
+ backtest_end_date=backtest_date_range.end_date
458
+ )
459
+ else:
460
+ for _, data_provider in \
461
+ tqdm(
462
+ data_providers, desc=description, colour="green"
463
+ ):
464
+
465
+ data_provider.prepare_backtest_data(
466
+ backtest_start_date=backtest_date_range.start_date,
467
+ backtest_end_date=backtest_date_range.end_date
468
+ )
469
+
470
+ def initialize_backtest_services(self):
471
+ """
472
+ Function to initialize the backtest services for the app. This method
473
+ should be called before running the algorithm in backtest mode.
474
+ It initializes the backtest services so that they are ready to be used.
475
+
476
+ Returns:
477
+ None
478
+ """
479
+ configuration_service = self.container.configuration_service()
480
+ self.initialize_order_executors()
481
+ self.initialize_portfolio_providers()
482
+ portfolio_conf_service = self.container \
483
+ .portfolio_configuration_service()
484
+ portfolio_snap_service = self.container \
485
+ .portfolio_snapshot_service()
486
+ market_cred_service = self.container.market_credential_service()
487
+ portfolio_provider_lookup = \
488
+ self.container.portfolio_provider_lookup()
489
+ # Override the portfolio service with the backtest portfolio service
490
+ self.container.portfolio_service.override(
491
+ BacktestPortfolioService(
492
+ configuration_service=configuration_service,
493
+ market_credential_service=market_cred_service,
494
+ position_service=self.container.position_service(),
495
+ order_service=self.container.order_service(),
496
+ portfolio_repository=self.container.portfolio_repository(),
497
+ portfolio_configuration_service=portfolio_conf_service,
498
+ portfolio_snapshot_service=portfolio_snap_service,
499
+ portfolio_provider_lookup=portfolio_provider_lookup
500
+ )
501
+ )
502
+
503
+ portfolio_conf_service = self.container. \
504
+ portfolio_configuration_service()
505
+ portfolio_snap_service = self.container. \
506
+ portfolio_snapshot_service()
507
+ configuration_service = self.container.configuration_service()
508
+ # Override the order service with the backtest order service
509
+ self.container.order_service.override(
510
+ OrderBacktestService(
511
+ trade_service=self.container.trade_service(),
512
+ order_repository=self.container.order_repository(),
513
+ position_service=self.container.position_service(),
514
+ portfolio_repository=self.container.portfolio_repository(),
515
+ portfolio_configuration_service=portfolio_conf_service,
516
+ portfolio_snapshot_service=portfolio_snap_service,
517
+ configuration_service=configuration_service,
518
+ )
519
+ )
520
+
521
+ def initialize_services(self):
522
+ """
523
+ Method to initialize the app. This method should be called before
524
+ running the algorithm. It initializes the services and the algorithm
525
+ and sets up the database if it does not exist.
526
+
527
+ Also, it initializes all required services for the algorithm.
528
+
529
+ Returns:
530
+ None
531
+ """
532
+ logger.info("Initializing app")
533
+ self.initialize_order_executors()
534
+ self.initialize_portfolio_providers()
535
+
536
+ # Initialize all market credentials
537
+ market_credential_service = self.container.market_credential_service()
538
+ market_credential_service.initialize()
539
+ portfolio_configuration_service = self.container \
540
+ .portfolio_configuration_service()
541
+
542
+ if portfolio_configuration_service.count() == 0:
543
+ raise OperationalException("No portfolios configured")
544
+
545
+ configuration_service = self.container.configuration_service()
546
+ config = configuration_service.get_config()
547
+
548
+ if AppMode.WEB.equals(config[APP_MODE]):
549
+ configuration_service.add_value(APP_MODE, AppMode.WEB.value)
550
+ self._initialize_web()
551
+
552
+ def run(self, number_of_iterations: int = None):
553
+ """
554
+ Entry point to run the application. This method should be called to
555
+ start the trading bot. This method can be called in three modes:
556
+
557
+ - Without any params: In this mode, the app runs until a keyboard
558
+ interrupt is received. This mode is useful when running the app in
559
+ a loop.
560
+ - With a payload: In this mode, the app runs only once with the
561
+ payload provided. This mode is useful when running the app in a
562
+ one-off mode, such as running the app from the command line or
563
+ on a schedule. Payload is a dictionary that contains the data to
564
+ handle for the algorithm. This data should look like this:
565
+ {
566
+ "action": "RUN_STRATEGY",
567
+ }
568
+ - With a number of iterations: In this mode, the app runs for the
569
+ number of iterations provided. This mode is useful when running the
570
+ app in a loop for a fixed number of iterations.
571
+
572
+ This function first checks if there is an algorithm registered.
573
+ If not, it raises an OperationalException. Then it
574
+ initializes the algorithm with the services and the configuration.
575
+
576
+ Args:
577
+ number_of_iterations (int): The number of iterations to run the
578
+ algorithm for
579
+
580
+ Returns:
581
+ None
582
+ """
583
+ self.initialize_config()
584
+ self.initialize_storage()
585
+ event_loop_service = None
586
+
587
+ try:
588
+
589
+ # Load the state if a state handler is provided
590
+ if self._state_handler is not None:
591
+ logger.info("Detected state handler, loading state")
592
+ self._state_handler.initialize()
593
+ config = self.container.configuration_service().get_config()
594
+ self._state_handler.load(config[RESOURCE_DIRECTORY])
595
+
596
+ logger.info("App initialization complete")
597
+
598
+ # Run all on_initialize hooks
599
+ for hook in self._on_initialize_hooks:
600
+ hook.on_run(self.context)
601
+
602
+ algorithm = self.get_algorithm()
603
+ self.initialize_data_sources(algorithm.data_sources)
604
+ self.initialize_services()
605
+ self.initialize_portfolios()
606
+
607
+ if AppMode.WEB.equals(self.config[APP_MODE]):
608
+ logger.info("Running web")
609
+ flask_thread = threading.Thread(
610
+ name='Web App',
611
+ target=self._flask_app.run,
612
+ kwargs={"port": 8080}
613
+ )
614
+ flask_thread.daemon = True
615
+ flask_thread.start()
616
+
617
+ trade_order_evaluator = DefaultTradeOrderEvaluator(
618
+ trade_service=self.container.trade_service(),
619
+ order_service=self.container.order_service()
620
+ )
621
+ event_loop_service = EventLoopService(
622
+ configuration_service=self.container.configuration_service(),
623
+ portfolio_snapshot_service=self.container
624
+ .portfolio_snapshot_service(),
625
+ context=self.context,
626
+ order_service=self.container.order_service(),
627
+ portfolio_service=self.container.portfolio_service(),
628
+ data_provider_service=self.container.data_provider_service(),
629
+ trade_service=self.container.trade_service(),
630
+ )
631
+ event_loop_service.initialize(
632
+ algorithm, trade_order_evaluator=trade_order_evaluator
633
+ )
634
+
635
+ try:
636
+ event_loop_service.start(
637
+ number_of_iterations=number_of_iterations
638
+ )
639
+ except KeyboardInterrupt:
640
+ exit(0)
641
+ except Exception as e:
642
+ logger.error(e)
643
+ raise e
644
+ finally:
645
+
646
+ if event_loop_service is not None:
647
+ self._run_history = event_loop_service.history
648
+
649
+ try:
650
+ # Upload state if state handler is provided
651
+ if self._state_handler is not None:
652
+ logger.info("Detected state handler, saving state")
653
+ config = \
654
+ self.container.configuration_service().get_config()
655
+ self._state_handler.save(config[RESOURCE_DIRECTORY])
656
+ except Exception as e:
657
+ logger.error(e)
658
+
659
+ def add_portfolio_configuration(self, portfolio_configuration):
660
+ """
661
+ Function to add a portfolio configuration to the app. The portfolio
662
+ configuration should be an instance of PortfolioConfiguration.
663
+
664
+ Args:
665
+ portfolio_configuration: Instance of PortfolioConfiguration
666
+
667
+ Returns:
668
+ None
669
+ """
670
+ portfolio_configuration_service = self.container \
671
+ .portfolio_configuration_service()
672
+ portfolio_configuration_service.add(portfolio_configuration)
673
+
674
+ def task(
675
+ self,
676
+ function=None,
677
+ time_unit: TimeUnit = TimeUnit.MINUTE,
678
+ interval=10,
679
+ ):
680
+ """
681
+ Function to add a task to the application.
682
+
683
+ Args:
684
+ function:
685
+ time_unit:
686
+ interval:
687
+
688
+ Returns:
689
+ Union(Task, Function): the task
690
+ """
691
+
692
+ if function:
693
+ task = Task(
694
+ decorated=function,
695
+ time_unit=time_unit,
696
+ interval=interval,
697
+ )
698
+ self._tasks.append(task)
699
+ return task
700
+ else:
701
+ def wrapper(f):
702
+ self._tasks.append(
703
+ Task(
704
+ decorated=f,
705
+ time_unit=time_unit,
706
+ interval=interval
707
+ )
708
+ )
709
+ return f
710
+
711
+ return wrapper
712
+
713
+ def add_task(self, task):
714
+ if inspect.isclass(task):
715
+ task = task()
716
+
717
+ assert isinstance(task, Task), \
718
+ OperationalException(
719
+ "Task object is not an instance of a Task"
720
+ )
721
+
722
+ self._tasks.append(task)
723
+
724
+ def add_tasks(self, tasks: List[Task]):
725
+ """
726
+ Function to add a list of tasks to the app. The tasks should be
727
+ instances of Task.
728
+
729
+ Args:
730
+ tasks: List of Task instances
731
+
732
+ Returns:
733
+ None
734
+ """
735
+ for task in tasks:
736
+ self.add_task(task)
737
+
738
+ def _initialize_web(self):
739
+ """
740
+ Initialize the app for web mode by setting the configuration
741
+ parameters for web mode and overriding the services with the
742
+ web services equivalents.
743
+
744
+ Web has the following implications:
745
+ - db
746
+ - sqlite
747
+ - services
748
+ - Flask app
749
+ - Investing Algorithm Framework App
750
+ - Algorithm
751
+ """
752
+ configuration_service = self.container.configuration_service()
753
+ self._flask_app = create_flask_app(configuration_service)
754
+
755
+ def get_portfolio_configurations(self):
756
+ portfolio_configuration_service = self.container \
757
+ .portfolio_configuration_service()
758
+ return portfolio_configuration_service.get_all()
759
+
760
+ def get_market_credential(self, market: str) -> MarketCredential:
761
+ """
762
+ Function to get a market credential from the app. This method
763
+ should be called when you want to get a market credential.
764
+
765
+ Args:
766
+ market (str): The market to get the credential for
767
+
768
+ Returns:
769
+ MarketCredential: Instance of MarketCredential
770
+ """
771
+
772
+ market_credential_service = self.container \
773
+ .market_credential_service()
774
+ market_credential = market_credential_service.get(market)
775
+ if market_credential is None:
776
+ raise OperationalException(
777
+ f"Market credential for {market} not found"
778
+ )
779
+ return market_credential
780
+
781
+ def get_market_credentials(self) -> List[MarketCredential]:
782
+ """
783
+ Function to get all market credentials from the app. This method
784
+ should be called when you want to get all market credentials.
785
+
786
+ Returns:
787
+ List of MarketCredential instances
788
+ """
789
+ market_credential_service = self.container \
790
+ .market_credential_service()
791
+ return market_credential_service.get_all()
792
+
793
+ def check_data_completeness(
794
+ self,
795
+ strategies: List[TradingStrategy],
796
+ backtest_date_range: BacktestDateRange,
797
+ show_progress: bool = True
798
+ ) -> Tuple[bool, Dict[str, Any]]:
799
+ """
800
+ Function to check the data completeness for a set of strategies
801
+ over a given backtest date range. This method checks if all data
802
+ sources required by the strategies have complete data for the
803
+ specified date range.
804
+
805
+ Args:
806
+ strategies (List[TradingStrategy]): List of strategy objects
807
+ to check data completeness for.
808
+ backtest_date_range (BacktestDateRange): The date range to
809
+ check data completeness for.
810
+ show_progress (bool): Whether to show a progress bar when
811
+ checking data completeness.
812
+ Returns:
813
+ Tuple[bool, Dict[str, Any]]: A tuple containing a boolean
814
+ indicating if the data is complete and a dictionary
815
+ with information about missing data for each data source.
816
+ """
817
+ data_sources = []
818
+ missing_data_info = {}
819
+
820
+ for strategy in strategies:
821
+ data_sources.extend(strategy.data_sources)
822
+
823
+ self.initialize_data_sources_backtest(
824
+ data_sources,
825
+ backtest_date_range,
826
+ show_progress=show_progress
827
+ )
828
+ data_provider_service = self.container.data_provider_service()
829
+ unique_data_sources = set(data_sources)
830
+
831
+ for data_source in unique_data_sources:
832
+
833
+ if DataType.OHLCV.equals(data_source.data_type):
834
+ required_start_date = backtest_date_range.start_date - \
835
+ timedelta(
836
+ minutes=TimeFrame.from_value(
837
+ data_source.time_frame
838
+ ).amount_of_minutes * data_source.window_size
839
+ )
840
+ number_of_required_data_points = \
841
+ data_source.get_number_of_required_data_points(
842
+ backtest_date_range.start_date,
843
+ backtest_date_range.end_date
844
+ )
845
+
846
+ try:
847
+ data_provider = data_provider_service.get(data_source)
848
+ number_of_available_data_points = \
849
+ data_provider.get_number_of_data_points(
850
+ backtest_date_range.start_date,
851
+ backtest_date_range.end_date
852
+ )
853
+
854
+ missing_dates = \
855
+ data_provider.get_missing_data_dates(
856
+ required_start_date,
857
+ backtest_date_range.end_date
858
+ )
859
+ if len(missing_dates) > 0:
860
+ missing_data_info[data_source.identifier] = {
861
+ "data_source_id": data_source.identifier,
862
+ "completeness_percentage": (
863
+ (
864
+ number_of_available_data_points /
865
+ number_of_required_data_points
866
+ ) * 100
867
+ ),
868
+ "missing_data_points": len(
869
+ missing_dates
870
+ ),
871
+ "missing_dates": missing_dates,
872
+ "data_source_file_path":
873
+ data_provider.get_data_source_file_path()
874
+ }
875
+
876
+ except Exception as e:
877
+ raise DataError(
878
+ f"Error getting data provider for data source "
879
+ f"{data_source.identifier} "
880
+ f"({data_source.symbol}): {str(e)}"
881
+ )
882
+
883
+ if len(missing_data_info.keys()) > 0:
884
+ return False, missing_data_info
885
+
886
+ return True, missing_data_info
887
+
888
+ def run_vector_backtests(
889
+ self,
890
+ initial_amount,
891
+ strategies: List[TradingStrategy],
892
+ backtest_date_range: BacktestDateRange = None,
893
+ backtest_date_ranges: List[BacktestDateRange] = None,
894
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
895
+ risk_free_rate: Optional[float] = None,
896
+ skip_data_sources_initialization: bool = False,
897
+ show_progress: bool = True,
898
+ market: Optional[str] = None,
899
+ trading_symbol: Optional[str] = None,
900
+ continue_on_error: bool = False,
901
+ ) -> List[Backtest]:
902
+ """
903
+ Run vectorized backtests for a set of strategies. The provided
904
+ set of strategies need to have their 'buy_signal_vectorized' and
905
+ 'sell_signal_vectorized' methods implemented to support vectorized
906
+ backtesting.
907
+
908
+ Args:
909
+ initial_amount: The initial amount to start the backtest with.
910
+ This will be the amount of trading currency that the backtest
911
+ portfolio will start with.
912
+ strategies (List[TradingStrategy]): List of strategy objects
913
+ that need to be backtested. Each strategy should implement
914
+ the 'buy_signal_vectorized' and 'sell_signal_vectorized'
915
+ methods to support vectorized backtesting.
916
+ backtest_date_range: The date range to run the backtest for
917
+ (instance of BacktestDateRange). This is used when
918
+ backtest_date_ranges is not provided.
919
+ backtest_date_ranges: List of date ranges to run the backtests for
920
+ (List of BacktestDateRange instances). If this is provided,
921
+ the backtests will be run for each date range in the list.
922
+ If this is not provided, the backtest_date_range will be used
923
+ snapshot_interval (SnapshotInterval): The snapshot
924
+ interval to use for the backtest. This is used to determine
925
+ how often the portfolio snapshot should be taken during the
926
+ backtest. The default is TRADE_CLOSE, which means that the
927
+ portfolio snapshot will be taken at the end of each trade.
928
+ risk_free_rate (Optional[float]): The risk-free rate to use for
929
+ the backtest. This is used to calculate the Sharpe ratio
930
+ and other performance metrics. If not provided, the default
931
+ risk-free rate will be tried to be fetched from the
932
+ US Treasury website.
933
+ skip_data_sources_initialization (bool): Whether to skip the
934
+ initialization of data sources. This is useful when the data
935
+ sources are already initialized, and you want to skip the
936
+ initialization step. This will speed up the backtesting
937
+ process, but make sure that the data sources are already
938
+ initialized before calling this method.
939
+ show_progress (bool): Whether to show progress bars during
940
+ data source initialization. This is useful for long-running
941
+ initialization processes.
942
+ market (str): The market to use for the backtest. This is used
943
+ to create a portfolio configuration if no portfolio
944
+ configuration is provided in the strategy.
945
+ trading_symbol (str): The trading symbol to use for the backtest.
946
+ This is used to create a portfolio configuration if no
947
+ portfolio configuration is provided in the strategy.
948
+ continue_on_error (bool): Whether to continue running other
949
+ backtests if an error occurs in one of the backtests. If set
950
+ to True, the backtest will return an empty Backtest instance
951
+ in case of an error. If set to False, the error will be raised.
952
+
953
+ Returns:
954
+ List[Backtest]: List of Backtest instances for each strategy
955
+ that was backtested.
956
+ """
957
+ backtests = []
958
+ backtests_ordered_by_strategy = {}
959
+ data_sources = []
960
+
961
+ if backtest_date_range is None and backtest_date_ranges is None:
962
+ raise OperationalException(
963
+ "Either backtest_date_range or backtest_date_ranges must be "
964
+ "provided"
965
+ )
966
+
967
+ for strategy in strategies:
968
+ data_sources.extend(strategy.data_sources)
969
+
970
+ if risk_free_rate is None:
971
+ logger.info("No risk free rate provided, retrieving it...")
972
+ risk_free_rate = get_risk_free_rate_us()
973
+
974
+ if risk_free_rate is None:
975
+ raise OperationalException(
976
+ "Could not retrieve risk free rate for backtest metrics."
977
+ "Please provide a risk free as an argument when running "
978
+ "your backtest or make sure you have an internet "
979
+ "connection"
980
+ )
981
+
982
+ if backtest_date_range is not None:
983
+ if not skip_data_sources_initialization:
984
+ self.initialize_data_sources_backtest(
985
+ data_sources,
986
+ backtest_date_range,
987
+ show_progress=show_progress
988
+ )
989
+
990
+ for strategy in tqdm(
991
+ strategies, colour="green", desc="Running backtests"
992
+ ):
993
+ backtest = self.run_vector_backtest(
994
+ backtest_date_range=backtest_date_range,
995
+ initial_amount=initial_amount,
996
+ strategy=strategy,
997
+ snapshot_interval=snapshot_interval,
998
+ risk_free_rate=risk_free_rate,
999
+ skip_data_sources_initialization=True,
1000
+ market=market,
1001
+ trading_symbol=trading_symbol,
1002
+ continue_on_error=continue_on_error
1003
+ )
1004
+ backtests.append(backtest)
1005
+ else:
1006
+ for backtest_date_range in tqdm(
1007
+ backtest_date_ranges,
1008
+ colour="green",
1009
+ desc="Running backtests for all date ranges"
1010
+ ):
1011
+ if not skip_data_sources_initialization:
1012
+ self.initialize_data_sources_backtest(
1013
+ data_sources,
1014
+ backtest_date_range,
1015
+ show_progress=show_progress
1016
+ )
1017
+ start_date = backtest_date_range.start_date.strftime(
1018
+ '%Y-%m-%d'
1019
+ )
1020
+ end_date = backtest_date_range.end_date.strftime('%Y-%m-%d')
1021
+
1022
+ for strategy in tqdm(
1023
+ strategies,
1024
+ colour="green",
1025
+ desc=f"Running backtests for "
1026
+ f"{start_date} to {end_date}"
1027
+ ):
1028
+
1029
+ if strategy not in backtests_ordered_by_strategy:
1030
+ backtests_ordered_by_strategy[strategy] = []
1031
+
1032
+ backtests_ordered_by_strategy[strategy].append(
1033
+ self.run_vector_backtest(
1034
+ backtest_date_range=backtest_date_range,
1035
+ initial_amount=initial_amount,
1036
+ strategy=strategy,
1037
+ snapshot_interval=snapshot_interval,
1038
+ risk_free_rate=risk_free_rate,
1039
+ skip_data_sources_initialization=True,
1040
+ market=market,
1041
+ trading_symbol=trading_symbol,
1042
+ )
1043
+ )
1044
+
1045
+ for strategy in backtests_ordered_by_strategy:
1046
+ backtests.append(
1047
+ combine_backtests(backtests_ordered_by_strategy[strategy])
1048
+ )
1049
+
1050
+ return backtests
1051
+
1052
+ def run_vector_backtest(
1053
+ self,
1054
+ backtest_date_range: BacktestDateRange,
1055
+ strategy: TradingStrategy,
1056
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
1057
+ metadata: Optional[Dict[str, str]] = None,
1058
+ risk_free_rate: Optional[float] = None,
1059
+ skip_data_sources_initialization: bool = False,
1060
+ show_data_initialization_progress: bool = True,
1061
+ initial_amount: float = None,
1062
+ market: str = None,
1063
+ trading_symbol: str = None,
1064
+ continue_on_error: bool = False,
1065
+ ) -> Backtest:
1066
+ """
1067
+ Run vectorized backtests for a strategy. The provided
1068
+ strategy needs to have its 'buy_signal_vectorized' and
1069
+ 'sell_signal_vectorized' methods implemented to support vectorized
1070
+ backtesting.
1071
+
1072
+ Args:
1073
+ backtest_date_range: The date range to run the backtest for
1074
+ (instance of BacktestDateRange)
1075
+ initial_amount: The initial amount to start the backtest with.
1076
+ This will be the amount of trading currency that the backtest
1077
+ portfolio will start with.
1078
+ strategy (TradingStrategy) (Optional): The strategy object
1079
+ that needs to be backtested.
1080
+ snapshot_interval (SnapshotInterval): The snapshot
1081
+ interval to use for the backtest. This is used to determine
1082
+ how often the portfolio snapshot should be taken during the
1083
+ backtest. The default is TRADE_CLOSE, which means that the
1084
+ portfolio snapshot will be taken at the end of each trade.
1085
+ risk_free_rate (Optional[float]): The risk-free rate to use for
1086
+ the backtest. This is used to calculate the Sharpe ratio
1087
+ and other performance metrics. If not provided, the default
1088
+ risk-free rate will be tried to be fetched from the
1089
+ US Treasury website.
1090
+ metadata (Optional[Dict[str, str]]): Metadata to attach to the
1091
+ backtest report. This can be used to store additional
1092
+ information about the backtest, such as the author, version,
1093
+ parameters or any other relevant information.
1094
+ skip_data_sources_initialization (bool): Whether to skip the
1095
+ initialization of data sources. This is useful when the data
1096
+ sources are already initialized, and you want to skip the
1097
+ initialization step. This will speed up the backtesting
1098
+ process, but make sure that the data sources are already
1099
+ initialized before calling this method.
1100
+ show_data_initialization_progress (bool): Whether to show the
1101
+ progress bar when initializing data sources.
1102
+ market (str): The market to use for the backtest. This is used
1103
+ to create a portfolio configuration if no portfolio
1104
+ configuration is provided in the strategy.
1105
+ trading_symbol (str): The trading symbol to use for the backtest.
1106
+ This is used to create a portfolio configuration if no
1107
+ portfolio configuration is provided in the strategy.
1108
+ initial_amount (float): The initial amount to start the
1109
+ backtest with. This will be the amount of trading currency
1110
+ that the portfolio will start with. If not provided,
1111
+ the initial amount from the portfolio configuration will
1112
+ be used.
1113
+ continue_on_error (bool): Whether to continue running other
1114
+ backtests if an error occurs in one of the backtests. If set
1115
+ to True, the backtest will return an empty Backtest instance
1116
+ in case of an error. If set to False, the error will be raised.
1117
+
1118
+ Returns:
1119
+ Backtest: Instance of Backtest
1120
+ """
1121
+ # Initialize configuration for vectorized backtesting
1122
+ self.initialize_backtest_config(
1123
+ backtest_date_range=backtest_date_range,
1124
+ snapshot_interval=snapshot_interval,
1125
+ initial_amount=initial_amount
1126
+ )
1127
+
1128
+ if not skip_data_sources_initialization:
1129
+ self.initialize_data_sources_backtest(
1130
+ strategy.data_sources,
1131
+ backtest_date_range,
1132
+ show_progress=show_data_initialization_progress
1133
+ )
1134
+
1135
+ if risk_free_rate is None:
1136
+ logger.info("No risk free rate provided, retrieving it...")
1137
+ risk_free_rate = get_risk_free_rate_us()
1138
+
1139
+ if risk_free_rate is None:
1140
+ raise OperationalException(
1141
+ "Could not retrieve risk free rate for backtest metrics."
1142
+ "Please provide a risk free as an argument when running "
1143
+ "your backtest or make sure you have an internet "
1144
+ "connection"
1145
+ )
1146
+
1147
+ backtest_service = self.container.backtest_service()
1148
+ backtest_service.validate_strategy_for_vector_backtest(strategy)
1149
+
1150
+ try:
1151
+ run = backtest_service.create_vector_backtest(
1152
+ strategy=strategy,
1153
+ backtest_date_range=backtest_date_range,
1154
+ risk_free_rate=risk_free_rate,
1155
+ market=market,
1156
+ trading_symbol=trading_symbol,
1157
+ initial_amount=initial_amount
1158
+ )
1159
+ backtest = Backtest(
1160
+ backtest_runs=[run],
1161
+ risk_free_rate=risk_free_rate,
1162
+ backtest_summary=generate_backtest_summary_metrics(
1163
+ [run.backtest_metrics]
1164
+ )
1165
+ )
1166
+ except Exception as e:
1167
+ logger.error(
1168
+ f"Error occurred during vector backtest for strategy "
1169
+ f"{strategy.strategy_id}: {str(e)}"
1170
+ )
1171
+ if continue_on_error:
1172
+ backtest = Backtest(
1173
+ backtest_runs=[],
1174
+ risk_free_rate=risk_free_rate,
1175
+ )
1176
+ else:
1177
+ raise e
1178
+
1179
+ # Add the metadata to the backtest
1180
+ if metadata is None:
1181
+
1182
+ if strategy.metadata is None:
1183
+ backtest.metadata = {}
1184
+ else:
1185
+ backtest.metadata = strategy.metadata
1186
+ else:
1187
+ backtest.metadata = metadata
1188
+
1189
+ return backtest
1190
+
1191
+ def run_backtests(
1192
+ self,
1193
+ backtest_date_ranges,
1194
+ initial_amount=None,
1195
+ strategy: Optional[TradingStrategy] = None,
1196
+ algorithm: Optional[Algorithm] = None,
1197
+ algorithms: Optional[List[Algorithm]] = None,
1198
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
1199
+ risk_free_rate: Optional[float] = None,
1200
+ ) -> List[Backtest]:
1201
+ """
1202
+ Function to run multiple backtests for a list of algorithms over
1203
+ a list of date ranges. This function will run each algorithm
1204
+ for each date range and return a list of backtest reports.
1205
+
1206
+ Args:
1207
+ algorithms: List of Algorithm instances to run backtests for.
1208
+ backtest_date_ranges (List[BacktestDateRange]): List of date ranges
1209
+ initial_amount (float): The initial amount to start the
1210
+ backtest with. This will be the amount of trading currency
1211
+ that the backtest portfolio will start with.
1212
+ snapshot_interval (SnapshotInterval): The snapshot interval to use
1213
+ for the backtest. This is used to determine how often the
1214
+ portfolio snapshot should be taken during the backtest.
1215
+ risk_free_rate (Optional[float]): The risk-free rate to use for
1216
+ the backtest. This is used to calculate the Sharpe ratio
1217
+ and other performance metrics. If not provided, the default
1218
+ risk-free rate will be tried to be fetched from the
1219
+ US Treasury website.
1220
+
1221
+ Returns:
1222
+ List[Backtest]: List of Backtest instances containing the results
1223
+ """
1224
+ backtests = []
1225
+
1226
+ if algorithms is not None:
1227
+ final_algorithms = algorithms
1228
+ elif strategy is not None:
1229
+ algorithm_factory = self.container.algorithm_factory()
1230
+ algorithm = algorithm_factory.create_algorithm(
1231
+ strategy=strategy
1232
+ )
1233
+ final_algorithms = [algorithm]
1234
+ elif algorithm is not None:
1235
+ final_algorithms = [algorithm]
1236
+ else:
1237
+ raise OperationalException(
1238
+ "No algorithms or strategy provided for backtesting"
1239
+ )
1240
+
1241
+ if risk_free_rate is None:
1242
+ logger.info("No risk free rate provided, retrieving it...")
1243
+ risk_free_rate = get_risk_free_rate_us()
1244
+
1245
+ if risk_free_rate is None:
1246
+ raise OperationalException(
1247
+ "Could not retrieve risk free rate for backtest metrics."
1248
+ "Please provide a risk free as an argument when running "
1249
+ "your backtest or make sure you have an internet "
1250
+ "connection"
1251
+ )
1252
+
1253
+ for date_range in backtest_date_ranges:
1254
+ for algorithm in final_algorithms:
1255
+ backtest = self.run_backtest(
1256
+ backtest_date_range=date_range,
1257
+ initial_amount=initial_amount,
1258
+ algorithm=algorithm,
1259
+ snapshot_interval=snapshot_interval,
1260
+ risk_free_rate=risk_free_rate
1261
+ )
1262
+ backtests.append(backtest)
1263
+
1264
+ return backtests
1265
+
1266
+ def run_backtest(
1267
+ self,
1268
+ backtest_date_range: BacktestDateRange,
1269
+ name: str = None,
1270
+ initial_amount=None,
1271
+ algorithm=None,
1272
+ strategy=None,
1273
+ strategies: List = None,
1274
+ snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
1275
+ risk_free_rate: Optional[float] = None,
1276
+ metadata: Optional[Dict[str, str]] = None,
1277
+ ) -> Backtest:
1278
+ """
1279
+ Run a backtest for an algorithm.
1280
+
1281
+ Args:
1282
+ backtest_date_range: The date range to run the backtest for
1283
+ (instance of BacktestDateRange)
1284
+ name: The name of the backtest. This is used to identify the
1285
+ backtest report in the output directory.
1286
+ initial_amount: The initial amount to start the backtest with.
1287
+ This will be the amount of trading currency that the backtest
1288
+ portfolio will start with.
1289
+ strategy (TradingStrategy) (Optional): The strategy object
1290
+ that needs to be backtested.
1291
+ strategies (List[TradingStrategy]) (Optional): List of strategy
1292
+ objects that need to be backtested
1293
+ algorithm (Algorithm) (Optional): The algorithm object that needs
1294
+ to be backtested. If this is provided, then the strategies
1295
+ and tasks of the algorithm will be used for the backtest.
1296
+ snapshot_interval (SnapshotInterval): The snapshot
1297
+ interval to use for the backtest. This is used to determine
1298
+ how often the portfolio snapshot should be taken during the
1299
+ backtest. The default is TRADE_CLOSE, which means that the
1300
+ portfolio snapshot will be taken at the end of each trade.
1301
+ risk_free_rate (Optional[float]): The risk-free rate to use for
1302
+ the backtest. This is used to calculate the Sharpe ratio
1303
+ and other performance metrics. If not provided, the default
1304
+ risk-free rate will be tried to be fetched from the
1305
+ US Treasury website.
1306
+ metadata (Optional[Dict[str, str]]): Metadata to attach to the
1307
+ backtest report. This can be used to store additional
1308
+ information about the backtest, such as the author, version,
1309
+ parameters or any other relevant information.
1310
+
1311
+ Returns:
1312
+ Backtest: Instance of Backtest
1313
+ """
1314
+ self.initialize_backtest_config(
1315
+ backtest_date_range=backtest_date_range,
1316
+ snapshot_interval=snapshot_interval,
1317
+ initial_amount=initial_amount
1318
+ )
1319
+ self.initialize_storage(remove_database_if_exists=True)
1320
+ self.initialize_backtest_services()
1321
+ self.initialize_backtest_portfolios()
1322
+
1323
+ if risk_free_rate is None:
1324
+ logger.info("No risk free rate provided, retrieving it...")
1325
+ risk_free_rate = get_risk_free_rate_us()
1326
+
1327
+ if risk_free_rate is None:
1328
+ raise OperationalException(
1329
+ "Could not retrieve risk free rate for backtest metrics."
1330
+ "Please provide a risk free as an argument when running "
1331
+ "your backtest or make sure you have an internet "
1332
+ "connection"
1333
+ )
1334
+
1335
+ algorithm = self.container.algorithm_factory().create_algorithm(
1336
+ name=name if name else self._name,
1337
+ strategies=(
1338
+ self._strategies if strategies is None else strategies
1339
+ ),
1340
+ algorithm=algorithm,
1341
+ strategy=strategy,
1342
+ tasks=self._tasks,
1343
+ on_strategy_run_hooks=self._on_strategy_run_hooks,
1344
+ )
1345
+ self.initialize_data_sources_backtest(
1346
+ algorithm.data_sources, backtest_date_range
1347
+ )
1348
+ backtest_service = self.container.backtest_service()
1349
+
1350
+ # Create backtest schedule
1351
+ schedule = backtest_service.generate_schedule(
1352
+ algorithm.strategies,
1353
+ algorithm.tasks,
1354
+ backtest_date_range.start_date,
1355
+ backtest_date_range.end_date
1356
+ )
1357
+
1358
+ # Initialize event loop
1359
+ event_loop_service = EventLoopService(
1360
+ configuration_service=self.container.configuration_service(),
1361
+ portfolio_snapshot_service=self.container
1362
+ .portfolio_snapshot_service(),
1363
+ context=self.context,
1364
+ order_service=self.container.order_service(),
1365
+ portfolio_service=self.container.portfolio_service(),
1366
+ data_provider_service=self.container.data_provider_service(),
1367
+ trade_service=self.container.trade_service(),
1368
+ )
1369
+ trade_order_evaluator = BacktestTradeOrderEvaluator(
1370
+ trade_service=self.container.trade_service(),
1371
+ order_service=self.container.order_service()
1372
+ )
1373
+ event_loop_service.initialize(
1374
+ algorithm=algorithm,
1375
+ trade_order_evaluator=trade_order_evaluator
1376
+ )
1377
+ event_loop_service.start(schedule=schedule, show_progress=True)
1378
+ self._run_history = event_loop_service.history
1379
+
1380
+ # Convert the current run to a backtest
1381
+ backtest = backtest_service.create_backtest(
1382
+ algorithm=algorithm,
1383
+ number_of_runs=event_loop_service.total_number_of_runs,
1384
+ backtest_date_range=backtest_date_range,
1385
+ risk_free_rate=risk_free_rate,
1386
+ )
1387
+
1388
+ # Add the metadata to the backtest
1389
+ if metadata is None:
1390
+
1391
+ if algorithm.metadata is not None:
1392
+ backtest.metadata = algorithm.metadata
1393
+ else:
1394
+ backtest.metadata = {}
1395
+ else:
1396
+ backtest.metadata = metadata
1397
+
1398
+ self.cleanup_backtest_resources()
1399
+ return backtest
1400
+
1401
+ def run_permutation_test(
1402
+ self,
1403
+ strategy: TradingStrategy,
1404
+ backtest_date_range: BacktestDateRange,
1405
+ number_of_permutations: int = 100,
1406
+ initial_amount: float = 1000.0,
1407
+ market: str = None,
1408
+ trading_symbol: str = None,
1409
+ risk_free_rate: Optional[float] = None
1410
+ ) -> BacktestPermutationTest:
1411
+ """
1412
+ Run a permutation test for a given strategy over a specified
1413
+ date range. This test is used to determine the statistical
1414
+ significance of the strategy's performance by comparing it
1415
+ against a set of random permutations of the market data.
1416
+
1417
+ The permutation test will run the main backtest and then
1418
+ generate a number of random permutations of the market data
1419
+ to create a distribution of returns. The p value will be
1420
+ calculated based on the performance of the main backtest
1421
+ compared to the distribution of returns from the permutations.
1422
+
1423
+ Args:
1424
+ strategy (TradingStrategy): The strategy to test.
1425
+ backtest_date_range (BacktestDateRange): The date range for the
1426
+ backtest.
1427
+ number_of_permutations (int): The number of permutations to run.
1428
+ Default is 100.
1429
+ initial_amount (float): The initial amount for the backtest.
1430
+ Default is 1000.0.
1431
+ risk_free_rate (Optional[float]): The risk-free rate to use for
1432
+ the backtest metrics. If not provided, it will try to fetch
1433
+ the risk-free rate from the US Treasury website.
1434
+ market (str): The market to use for the backtest. This is used
1435
+ to create a portfolio configuration if no portfolio
1436
+ configuration is provided in the strategy. If not provided,
1437
+ the first portfolio configuration found will be used.
1438
+ trading_symbol (str): The trading symbol to use for the backtest.
1439
+ This is used to create a portfolio configuration if no
1440
+ portfolio configuration is provided in the strategy. If not
1441
+ provided, the first trading symbol found in the portfolio
1442
+ configuration will be used.
1443
+
1444
+ Raises:
1445
+ OperationalException: If the risk-free rate cannot be retrieved.
1446
+
1447
+ Returns:
1448
+ Backtest: The backtest report containing the results of the
1449
+ main backtest and the p value from the permutation test.
1450
+ """
1451
+
1452
+ if risk_free_rate is None:
1453
+ logger.info("No risk free rate provided, retrieving it...")
1454
+ risk_free_rate = get_risk_free_rate_us()
1455
+
1456
+ if risk_free_rate is None:
1457
+ raise OperationalException(
1458
+ "Could not retrieve risk free rate for backtest metrics."
1459
+ "Please provide a risk free as an argument when running "
1460
+ "your backtest or make sure you have an internet "
1461
+ "connection"
1462
+ )
1463
+
1464
+ data_provider_service = self.container.data_provider_service()
1465
+ backtest = self.run_vector_backtest(
1466
+ backtest_date_range=backtest_date_range,
1467
+ initial_amount=initial_amount,
1468
+ strategy=strategy,
1469
+ snapshot_interval=SnapshotInterval.DAILY,
1470
+ risk_free_rate=risk_free_rate,
1471
+ market=market,
1472
+ trading_symbol=trading_symbol
1473
+ )
1474
+ backtest_metrics = backtest.get_backtest_metrics(backtest_date_range)
1475
+
1476
+ if backtest_metrics.number_of_trades == 0:
1477
+ raise OperationalException(
1478
+ "The strategy did not make any trades during the backtest. "
1479
+ "Cannot perform permutation test."
1480
+ )
1481
+
1482
+ # Select the ohlcv data from the strategy's data sources
1483
+ data_sources = strategy.data_sources
1484
+ original_data_combinations = []
1485
+ permuted_metrics = []
1486
+ permuted_datasets_ordered_by_symbol = {}
1487
+ original_datasets_ordered_by_symbol = {}
1488
+
1489
+ for data_source in data_sources:
1490
+ if DataType.OHLCV.equals(data_source.data_type):
1491
+ data_provider = data_provider_service.get(data_source)
1492
+ data = data_provider_service.get_data(
1493
+ data_source=data_source,
1494
+ start_date=data_provider._start_date_data_source,
1495
+ end_date=backtest_date_range.end_date
1496
+ )
1497
+ original_data_combinations.append((data_source, data))
1498
+ original_datasets_ordered_by_symbol[data_source.symbol] = \
1499
+ data_provider_service.get_data(
1500
+ data_source=data_source,
1501
+ start_date=data_provider._start_date_data_source,
1502
+ end_date=backtest_date_range.end_date
1503
+ )
1504
+
1505
+ for _ in tqdm(
1506
+ range(number_of_permutations),
1507
+ desc="Running Permutation Test",
1508
+ colour="green"
1509
+ ):
1510
+ permutated_datasets = []
1511
+ data_provider_service.reset()
1512
+
1513
+ for combi in original_data_combinations:
1514
+ # Permute the data for the data source
1515
+ permutated_data = create_ohlcv_permutation(data=combi[1])
1516
+ permutated_datasets.append((combi[0], permutated_data))
1517
+
1518
+ if combi[0].symbol not in permuted_datasets_ordered_by_symbol:
1519
+ permuted_datasets_ordered_by_symbol[combi[0].symbol] = \
1520
+ [permutated_data]
1521
+ else:
1522
+ permuted_datasets_ordered_by_symbol[combi[0].symbol]\
1523
+ .append(permutated_data)
1524
+
1525
+ self._data_providers = []
1526
+
1527
+ for combi in permutated_datasets:
1528
+ data_source = combi[0]
1529
+ data_provider = PandasOHLCVDataProvider(
1530
+ dataframe=combi[1],
1531
+ symbol=data_source.symbol,
1532
+ market=data_source.market,
1533
+ window_size=data_source.window_size,
1534
+ time_frame=data_source.time_frame,
1535
+ data_provider_identifier=data_source
1536
+ .data_provider_identifier,
1537
+ pandas=data_source.pandas,
1538
+ )
1539
+ # Add pandas ohlcv data provider to the data provider service
1540
+ data_provider_service.register_data_provider(
1541
+ data_source=data_source,
1542
+ data_provider=data_provider
1543
+ )
1544
+
1545
+ # Run the backtest with the permuted strategy
1546
+ permuted_backtest = self.run_vector_backtest(
1547
+ backtest_date_range=backtest_date_range,
1548
+ initial_amount=initial_amount,
1549
+ strategy=strategy,
1550
+ snapshot_interval=SnapshotInterval.DAILY,
1551
+ risk_free_rate=risk_free_rate,
1552
+ skip_data_sources_initialization=True,
1553
+ market=market,
1554
+ trading_symbol=trading_symbol
1555
+ )
1556
+
1557
+ # Add the results of the permuted backtest to the main backtest
1558
+ permuted_metrics.append(
1559
+ permuted_backtest.get_backtest_metrics(backtest_date_range)
1560
+ )
1561
+
1562
+ # Create a BacktestPermutationTestMetrics object
1563
+ permutation_test_metrics = BacktestPermutationTest(
1564
+ real_metrics=backtest_metrics,
1565
+ permutated_metrics=permuted_metrics,
1566
+ ohlcv_permutated_datasets=permuted_datasets_ordered_by_symbol,
1567
+ ohlcv_original_datasets=original_datasets_ordered_by_symbol,
1568
+ backtest_start_date=backtest_date_range.start_date,
1569
+ backtest_end_date=backtest_date_range.end_date,
1570
+ backtest_date_range_name=backtest_date_range.name
1571
+ )
1572
+ return permutation_test_metrics
1573
+
1574
+ def add_data_provider(self, data_provider, priority=3) -> None:
1575
+ """
1576
+ Function to add a data provider to the app. The data provider should
1577
+ be an instance of DataProvider or a DataProviderClass.
1578
+
1579
+ Args:
1580
+ data_provider: Instance or class of DataProvider
1581
+ priority: Optional priority for the data provider. If not
1582
+ provided, the data provider will be added with the default
1583
+ priority (3).
1584
+
1585
+ Returns:
1586
+ None
1587
+ """
1588
+ if inspect.isclass(data_provider):
1589
+ if not issubclass(data_provider, DataProvider):
1590
+ raise OperationalException(
1591
+ "Data provider should be an instance of DataProvider"
1592
+ )
1593
+
1594
+ data_provider = data_provider()
1595
+
1596
+ self._data_providers.append((data_provider, priority))
1597
+
1598
+ def add_market_credential(
1599
+ self, market_credential: MarketCredential
1600
+ ) -> None:
1601
+ """
1602
+ Function to add a market credential to the app. The market
1603
+ credential should be an instance of MarketCredential.
1604
+
1605
+ Args:
1606
+ market_credential:
1607
+
1608
+ Returns:
1609
+ None
1610
+ """
1611
+ market_credential.market = market_credential.market.upper()
1612
+ market_credential_service = self.container \
1613
+ .market_credential_service()
1614
+ market_credential_service.add(market_credential)
1615
+
1616
+ def on_initialize(self, app_hook):
1617
+ """
1618
+ Function to add a hook that runs when the app is initialized. The hook
1619
+ should be an instance of AppHook.
1620
+
1621
+ Args:
1622
+ app_hook: Instance of AppHook
1623
+
1624
+ Returns:
1625
+ None
1626
+ """
1627
+
1628
+ # Check if the app_hook inherits from AppHook
1629
+ if not issubclass(app_hook, AppHook):
1630
+ raise OperationalException(
1631
+ "App hook should be an instance of AppHook"
1632
+ )
1633
+
1634
+ if inspect.isclass(app_hook):
1635
+ app_hook = app_hook()
1636
+
1637
+ self._on_initialize_hooks.append(app_hook)
1638
+
1639
+ def on_strategy_run(self, app_hook):
1640
+ """
1641
+ Function to add a hook that runs when a strategy is run. The hook
1642
+ should be an instance of AppHook.
1643
+ """
1644
+
1645
+ # Check if the app_hook inherits from AppHook
1646
+ if inspect.isclass(app_hook) and not issubclass(app_hook, AppHook):
1647
+ raise OperationalException(
1648
+ "App hook should be an instance of AppHook"
1649
+ )
1650
+
1651
+ if inspect.isclass(app_hook):
1652
+ app_hook = app_hook()
1653
+
1654
+ self._on_strategy_run_hooks.append(app_hook)
1655
+
1656
+ def after_initialize(self, app_hook: AppHook):
1657
+ """
1658
+ Function to add a hook that runs after the app is initialized.
1659
+ The hook should be an instance of AppHook.
1660
+ """
1661
+
1662
+ if inspect.isclass(app_hook):
1663
+ app_hook = app_hook()
1664
+
1665
+ self._on_after_initialize_hooks.append(app_hook)
1666
+
1667
+ def strategy(
1668
+ self,
1669
+ function=None,
1670
+ time_unit=TimeUnit.MINUTE,
1671
+ interval=10,
1672
+ data_sources=None
1673
+ ):
1674
+ """
1675
+ Decorator for registering a strategy. This decorator can be used
1676
+ to define a trading strategy function and register it in your
1677
+ application.
1678
+
1679
+ Args:
1680
+ function: The wrapped function to should be converted to
1681
+ a TradingStrategy
1682
+ time_unit (TimeUnit): instance of TimeUnit Enum
1683
+ interval (int): interval of the schedule ( interval - TimeUnit )
1684
+ data_sources (List): List of data sources that the
1685
+ trading strategy function uses.
1686
+
1687
+ Returns:
1688
+ Function
1689
+ """
1690
+ from .strategy import TradingStrategy
1691
+
1692
+ if function:
1693
+ strategy_object = TradingStrategy(
1694
+ decorated=function,
1695
+ time_unit=time_unit,
1696
+ interval=interval,
1697
+ data_sources=data_sources
1698
+ )
1699
+ self.add_strategy(strategy_object)
1700
+ return strategy_object
1701
+ else:
1702
+
1703
+ def wrapper(f):
1704
+ self.add_strategy(
1705
+ TradingStrategy(
1706
+ decorated=f,
1707
+ time_unit=time_unit,
1708
+ interval=interval,
1709
+ data_sources=data_sources,
1710
+ worker_id=f.__name__
1711
+ )
1712
+ )
1713
+ return f
1714
+
1715
+ return wrapper
1716
+
1717
+ def add_strategies(self, strategies, throw_exception=True) -> None:
1718
+ """
1719
+ Function to add strategies to the app
1720
+ Args:
1721
+ strategies (List(TradingStrategy)): List of trading strategies that
1722
+ need to be registered.
1723
+ throw_exception (boolean): Flag to specify if an exception
1724
+ can be thrown if the strategies are not in the format or type
1725
+ that the application expects
1726
+
1727
+ Returns:
1728
+ None
1729
+ """
1730
+
1731
+ if strategies is not None:
1732
+ for strategy in strategies:
1733
+ self.add_strategy(strategy, throw_exception=throw_exception)
1734
+
1735
+ def add_strategy(self, strategy, throw_exception=True) -> None:
1736
+ """
1737
+ Function to add a strategy to the app. The strategy should be an
1738
+ instance of TradingStrategy or a subclass based on the TradingStrategy
1739
+ class.
1740
+
1741
+ Args:
1742
+ strategy: Instance of TradingStrategy
1743
+ throw_exception: Flag to allow for throwing an exception when
1744
+ the provided strategy is not inline with what the application
1745
+ expects.
1746
+
1747
+ Returns:
1748
+ None
1749
+ """
1750
+
1751
+ logger.info("Adding strategy")
1752
+
1753
+ if inspect.isclass(strategy):
1754
+
1755
+ if not issubclass(strategy, TradingStrategy):
1756
+ raise OperationalException(
1757
+ "The strategy must be a subclass of TradingStrategy"
1758
+ )
1759
+
1760
+ strategy = strategy()
1761
+
1762
+ if not isinstance(strategy, TradingStrategy):
1763
+
1764
+ if throw_exception:
1765
+ raise OperationalException(
1766
+ "Strategy should be an instance of TradingStrategy"
1767
+ )
1768
+ else:
1769
+ return
1770
+
1771
+ has_duplicates = False
1772
+
1773
+ for i in range(len(self._strategies)):
1774
+ for j in range(i + 1, len(self._strategies)):
1775
+ if self._strategies[i].worker_id == strategy.worker_id:
1776
+ has_duplicates = True
1777
+ break
1778
+
1779
+ if has_duplicates:
1780
+ raise OperationalException(
1781
+ "Can't add strategy, there already exists a strategy "
1782
+ "with the same id in the algorithm"
1783
+ )
1784
+
1785
+ self._strategies.append(strategy)
1786
+
1787
+ def add_state_handler(self, state_handler):
1788
+ """
1789
+ Function to add a state handler to the app. The state handler should
1790
+ be an instance of StateHandler.
1791
+
1792
+ Args:
1793
+ state_handler: Instance of StateHandler
1794
+
1795
+ Returns:
1796
+ None
1797
+ """
1798
+
1799
+ if inspect.isclass(state_handler):
1800
+ state_handler = state_handler()
1801
+
1802
+ if not isinstance(state_handler, StateHandler):
1803
+ raise OperationalException(
1804
+ "State handler should be an instance of StateHandler"
1805
+ )
1806
+
1807
+ self._state_handler = state_handler
1808
+
1809
+ def add_market(
1810
+ self,
1811
+ market,
1812
+ trading_symbol,
1813
+ api_key=None,
1814
+ secret_key=None,
1815
+ initial_balance=None
1816
+ ):
1817
+ """
1818
+ Function to add a market to the app. This function is a utility
1819
+ function to add a portfolio configuration and market credential
1820
+ to the app.
1821
+
1822
+ Args:
1823
+ market: String representing the market name
1824
+ trading_symbol: Trading symbol for the portfolio
1825
+ api_key: API key for the market
1826
+ secret_key: Secret key for the market
1827
+ initial_balance: Initial balance for the market
1828
+
1829
+ Returns:
1830
+ None
1831
+ """
1832
+
1833
+ portfolio_configuration = PortfolioConfiguration(
1834
+ market=market,
1835
+ trading_symbol=trading_symbol,
1836
+ initial_balance=initial_balance
1837
+ )
1838
+
1839
+ self.add_portfolio_configuration(portfolio_configuration)
1840
+ market_credential = MarketCredential(
1841
+ market=market,
1842
+ api_key=api_key,
1843
+ secret_key=secret_key
1844
+ )
1845
+ self.add_market_credential(market_credential)
1846
+
1847
+ def add_order_executor(self, order_executor):
1848
+ """
1849
+ Function to add an order executor to the app. The order executor
1850
+ should be an instance of OrderExecutor.
1851
+
1852
+ Args:
1853
+ order_executor: Instance of OrderExecutor
1854
+
1855
+ Returns:
1856
+ None
1857
+ """
1858
+
1859
+ if inspect.isclass(order_executor):
1860
+ order_executor = order_executor()
1861
+
1862
+ if not isinstance(order_executor, OrderExecutor):
1863
+ raise OperationalException(
1864
+ "Order executor should be an instance of OrderExecutor"
1865
+ )
1866
+
1867
+ order_executor_lookup = self.container.order_executor_lookup()
1868
+ order_executor_lookup.add_order_executor(
1869
+ order_executor=order_executor
1870
+ )
1871
+
1872
+ def get_order_executors(self):
1873
+ """
1874
+ Function to get all order executors from the app. This method
1875
+ should be called when you want to get all order executors.
1876
+
1877
+ Returns:
1878
+ List of OrderExecutor instances
1879
+ """
1880
+ order_executor_lookup = self.container.order_executor_lookup()
1881
+ return order_executor_lookup.get_all()
1882
+
1883
+ def add_portfolio_provider(self, portfolio_provider):
1884
+ """
1885
+ Function to add a portfolio provider to the app. The portfolio
1886
+ provider should be an instance of PortfolioProvider.
1887
+
1888
+ Args:
1889
+ portfolio_provider: Instance of PortfolioProvider
1890
+
1891
+ Returns:
1892
+ None
1893
+ """
1894
+
1895
+ if inspect.isclass(portfolio_provider):
1896
+ portfolio_provider = portfolio_provider()
1897
+
1898
+ if not isinstance(portfolio_provider, PortfolioProvider):
1899
+ raise OperationalException(
1900
+ "Portfolio provider should be an instance of "
1901
+ "PortfolioProvider"
1902
+ )
1903
+
1904
+ portfolio_provider_lookup = self.container.portfolio_provider_lookup()
1905
+ portfolio_provider_lookup.add_portfolio_provider(
1906
+ portfolio_provider=portfolio_provider
1907
+ )
1908
+
1909
+ def get_portfolio_providers(self):
1910
+ """
1911
+ Function to get all portfolio providers from the app. This method
1912
+ should be called when you want to get all portfolio providers.
1913
+
1914
+ Returns:
1915
+ List of PortfolioProvider instances
1916
+ """
1917
+ portfolio_provider_lookup = self.container.portfolio_provider_lookup()
1918
+ return portfolio_provider_lookup.get_all()
1919
+
1920
+ def initialize_order_executors(self):
1921
+ """
1922
+ Function to initialize the order executors. This function will
1923
+ first check if the app is running in backtest mode or not. If it is
1924
+ running in backtest mode, all order executors will be removed and
1925
+ a single BacktestOrderExecutor will be added to the order executors.
1926
+
1927
+ If it is not running in backtest mode, it will add the default
1928
+ CCXTOrderExecutor with a priority 3.
1929
+ """
1930
+ logger.info("Adding order executors")
1931
+ order_executor_lookup = self.container.order_executor_lookup()
1932
+ environment = self.config[ENVIRONMENT]
1933
+
1934
+ if Environment.BACKTEST.equals(environment):
1935
+ # If the app is running in backtest mode,
1936
+ # remove all order executors
1937
+ # and add a single BacktestOrderExecutor
1938
+ order_executor_lookup.reset()
1939
+ order_executor_lookup.add_order_executor(
1940
+ BacktestOrderExecutor(priority=1)
1941
+ )
1942
+ else:
1943
+ order_executor_lookup.add_order_executor(
1944
+ CCXTOrderExecutor(priority=3)
1945
+ )
1946
+
1947
+ for order_executor in order_executor_lookup.get_all():
1948
+ order_executor.config = self.config
1949
+
1950
+ def initialize_portfolios(self):
1951
+ """
1952
+ Function to initialize the portfolios. This function will
1953
+ first check if the app is running in backtest mode or not. If it is
1954
+ running in backtest mode, it will create the portfolios with the
1955
+ initial amount specified in the config. If it is not running in
1956
+ backtest mode, it will check if there are
1957
+
1958
+ """
1959
+ logger.info("Initializing portfolios")
1960
+ portfolio_configuration_service = self.container \
1961
+ .portfolio_configuration_service()
1962
+ portfolio_service = self.container.portfolio_service()
1963
+
1964
+ # Throw an error if no portfolios are configured
1965
+ if portfolio_configuration_service.count() == 0:
1966
+ raise OperationalException("No portfolios configured")
1967
+
1968
+ # Check if there are already existing portfolios
1969
+ portfolios = portfolio_service.get_all()
1970
+ portfolio_configurations = portfolio_configuration_service\
1971
+ .get_all()
1972
+ portfolio_provider_lookup = \
1973
+ self.container.portfolio_provider_lookup()
1974
+
1975
+ if len(portfolios) > 0:
1976
+
1977
+ # Check if there are matching portfolio configurations
1978
+ for portfolio in portfolios:
1979
+ logger.info(
1980
+ f"Checking if there is an matching portfolio "
1981
+ "configuration "
1982
+ f"for portfolio {portfolio.identifier}"
1983
+ )
1984
+ portfolio_configuration = \
1985
+ portfolio_configuration_service.get(
1986
+ portfolio.market
1987
+ )
1988
+
1989
+ if portfolio_configuration is None:
1990
+ raise ImproperlyConfigured(
1991
+ f"No matching portfolio configuration found for "
1992
+ f"existing portfolio {portfolio.market}, "
1993
+ f"please make sure that you have configured your "
1994
+ f"app with the right portfolio configurations "
1995
+ f"for the existing portfolios."
1996
+ f"If you want to create a new portfolio, please "
1997
+ f"remove the existing database (WARNING!!: this "
1998
+ f"will remove all existing history of your "
1999
+ f"trading bot.)"
2000
+ )
2001
+
2002
+ # Check if the portfolio configuration is still inline
2003
+ # with the initial balance
2004
+
2005
+ if portfolio_configuration.initial_balance != \
2006
+ portfolio.initial_balance:
2007
+ logger.warning(
2008
+ "The initial balance of the portfolio "
2009
+ "configuration is different from the existing "
2010
+ "portfolio. Checking if the existing portfolio "
2011
+ "can be updated..."
2012
+ )
2013
+
2014
+ # Register a portfolio provider for the portfolio
2015
+ portfolio_provider_lookup \
2016
+ .register_portfolio_provider_for_market(
2017
+ portfolio_configuration.market
2018
+ )
2019
+ initial_balance = portfolio_configuration\
2020
+ .initial_balance
2021
+
2022
+ if initial_balance != portfolio.initial_balance:
2023
+ raise ImproperlyConfigured(
2024
+ "The initial balance of the portfolio "
2025
+ "configuration is different then that of "
2026
+ "the existing portfolio. Please make sure "
2027
+ "that the initial balance of the portfolio "
2028
+ "configuration is the same as that of the "
2029
+ "existing portfolio. "
2030
+ f"Existing portfolio initial balance: "
2031
+ f"{portfolio.initial_balance}, "
2032
+ f"Portfolio configuration initial balance: "
2033
+ f"{portfolio_configuration.initial_balance}"
2034
+ "If this is intentional, please remove "
2035
+ "the database and re-run the app. "
2036
+ "WARNING!!: this will remove all existing "
2037
+ "history of your trading bot."
2038
+ )
2039
+
2040
+ order_executor_lookup = self.container.order_executor_lookup()
2041
+ market_credential_service = \
2042
+ self.container.market_credential_service()
2043
+ # Register portfolio providers and order executors
2044
+ for portfolio_configuration in portfolio_configurations:
2045
+
2046
+ # Register a portfolio provider for the portfolio
2047
+ portfolio_provider_lookup\
2048
+ .register_portfolio_provider_for_market(
2049
+ portfolio_configuration.market
2050
+ )
2051
+
2052
+ # Register an order executor for the portfolio
2053
+ order_executor_lookup.register_order_executor_for_market(
2054
+ portfolio_configuration.market
2055
+ )
2056
+
2057
+ market_credential = \
2058
+ market_credential_service.get(
2059
+ portfolio_configuration.market
2060
+ )
2061
+
2062
+ if market_credential is None:
2063
+ raise ImproperlyConfigured(
2064
+ f"No market credential found for existing "
2065
+ f"portfolio {portfolio_configuration.market} "
2066
+ "with market "
2067
+ "Cannot initialize portfolio configuration."
2068
+ )
2069
+
2070
+ if not portfolio_service.exists(
2071
+ {"identifier": portfolio_configuration.identifier}
2072
+ ):
2073
+ portfolio_service.create_portfolio_from_configuration(
2074
+ portfolio_configuration
2075
+ )
2076
+
2077
+ logger.info("Portfolio configurations complete")
2078
+ logger.info("Syncing portfolios")
2079
+ portfolio_service = self.container.portfolio_service()
2080
+ portfolio_sync_service = self.container.portfolio_sync_service()
2081
+
2082
+ for portfolio in portfolio_service.get_all():
2083
+ logger.info(f"Syncing portfolio {portfolio.identifier}")
2084
+ portfolio_sync_service.sync_unallocated(portfolio)
2085
+ portfolio_sync_service.sync_orders(portfolio)
2086
+
2087
+ def initialize_backtest_portfolios(self):
2088
+ """
2089
+ Function to initialize the backtest portfolios. This function will
2090
+ create a default portfolio provider for each market that is configured
2091
+ in the app. The default portfolio provider will be used to create
2092
+ portfolios for the app.
2093
+
2094
+ Returns:
2095
+ None
2096
+ """
2097
+ logger.info("Initializing backtest portfolios")
2098
+ config = self.config
2099
+ portfolio_configuration_service = self.container \
2100
+ .portfolio_configuration_service()
2101
+ portfolio_service = self.container.portfolio_service()
2102
+
2103
+ # Throw an error if no portfolios are configured
2104
+ if portfolio_configuration_service.count() == 0:
2105
+ raise OperationalException("No portfolios configured")
2106
+
2107
+ logger.info("Setting up backtest portfolios")
2108
+ initial_backtest_amount = config.get(
2109
+ BACKTESTING_INITIAL_AMOUNT, None
2110
+ )
2111
+
2112
+ for portfolio_configuration \
2113
+ in portfolio_configuration_service.get_all():
2114
+ if not portfolio_service.exists(
2115
+ {"identifier": portfolio_configuration.identifier}
2116
+ ):
2117
+ portfolio_service.create_portfolio_from_configuration(
2118
+ portfolio_configuration,
2119
+ initial_amount=initial_backtest_amount,
2120
+ )
2121
+
2122
+ def initialize_portfolio_providers(self):
2123
+ """
2124
+ Function to initialize the default portfolio providers.
2125
+ This function will create a default portfolio provider for
2126
+ each market that is configured in the app. The default portfolio
2127
+ provider will be used to create portfolios for the app.
2128
+
2129
+ Returns:
2130
+ None
2131
+ """
2132
+ logger.info("Adding portfolio providers")
2133
+ portfolio_provider_lookup = self.container\
2134
+ .portfolio_provider_lookup()
2135
+ environment = self.config[ENVIRONMENT]
2136
+
2137
+ if Environment.BACKTEST.equals(environment):
2138
+ # If the app is running in backtest mode,
2139
+ # remove all order executors
2140
+ # and add a single BacktestOrderExecutor
2141
+ portfolio_provider_lookup.reset()
2142
+ else:
2143
+ portfolio_provider_lookup.add_portfolio_provider(
2144
+ CCXTPortfolioProvider(priority=3)
2145
+ )
2146
+
2147
+ for portfolio_provider in portfolio_provider_lookup.get_all():
2148
+ portfolio_provider.config = self.config
2149
+
2150
+ def get_run_history(self):
2151
+ """
2152
+ Function to get the run history of the app. This function will
2153
+ return the history of the run schedule of all the strategies,
2154
+ and tasks that have been registered in the app.
2155
+
2156
+ Returns:
2157
+ dict: The run history of the app
2158
+ """
2159
+ return self._run_history
2160
+
2161
+ def has_run(self, worker_id) -> bool:
2162
+ """
2163
+ Function to check if a worker has run in the app. This function
2164
+ will check if the worker_id is present in the run history of the app.
2165
+
2166
+ Args:
2167
+ worker_id:
2168
+
2169
+ Returns:
2170
+ Boolean: True if the worker has run, False otherwise
2171
+ """
2172
+ if self._run_history is None:
2173
+ return False
2174
+
2175
+ return worker_id in self._run_history
2176
+
2177
+ def get_algorithm(self):
2178
+ """
2179
+ Function to get the algorithm that is currently running in the app.
2180
+ This function will return the algorithm that is currently running
2181
+ in the app.
2182
+
2183
+ Returns:
2184
+ Algorithm: The algorithm that is currently running in the app
2185
+ """
2186
+ algorithm_factory = self.container.algorithm_factory()
2187
+ return algorithm_factory.create_algorithm(
2188
+ name=self._name,
2189
+ strategies=self._strategies,
2190
+ tasks=self._tasks,
2191
+ on_strategy_run_hooks=self._on_strategy_run_hooks,
2192
+ )
2193
+
2194
+ def cleanup_backtest_resources(self):
2195
+ """
2196
+ Clean up the backtest database and remove SQLAlchemy models/tables.
2197
+ """
2198
+ logger.info("Cleaning up backtest resources")
2199
+ config = self.config
2200
+ environment = config[ENVIRONMENT]
2201
+
2202
+ if Environment.BACKTEST.equals(environment):
2203
+ db_uri = config.get(SQLALCHEMY_DATABASE_URI)
2204
+ clear_db(db_uri)