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