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