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