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
|
@@ -1,278 +1,497 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import logging
|
|
3
2
|
import os
|
|
4
|
-
import
|
|
3
|
+
import sys
|
|
4
|
+
from collections import defaultdict
|
|
5
5
|
from datetime import datetime, timedelta, timezone
|
|
6
|
+
from typing import Dict, List, Union
|
|
7
|
+
from uuid import uuid4
|
|
6
8
|
|
|
9
|
+
import numpy as np
|
|
7
10
|
import pandas as pd
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from investing_algorithm_framework.services.
|
|
16
|
-
|
|
11
|
+
import polars as pl
|
|
12
|
+
|
|
13
|
+
from investing_algorithm_framework.domain import BacktestRun, OrderType, \
|
|
14
|
+
TimeUnit, Trade, OperationalException, BacktestDateRange, TimeFrame, \
|
|
15
|
+
Backtest, TradeStatus, PortfolioSnapshot, Order, OrderStatus, OrderSide, \
|
|
16
|
+
Portfolio, DataType, generate_backtest_summary_metrics, \
|
|
17
|
+
PortfolioConfiguration
|
|
18
|
+
from investing_algorithm_framework.services.data_providers import \
|
|
19
|
+
DataProviderService
|
|
20
|
+
from investing_algorithm_framework.services.portfolios import \
|
|
21
|
+
PortfolioConfigurationService
|
|
22
|
+
from investing_algorithm_framework.services.metrics import \
|
|
23
|
+
create_backtest_metrics
|
|
17
24
|
|
|
18
25
|
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}\.json$"
|
|
23
|
-
)
|
|
24
|
-
BACKTEST_REPORT_DIRECTORY_PATTERN = (
|
|
25
|
-
r"^report_\w+_backtest-start-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
|
|
26
|
-
r"backtest-end-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
|
|
27
|
-
r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}$"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class BacktestService(Observable):
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BacktestService:
|
|
32
29
|
"""
|
|
33
30
|
Service that facilitates backtests for algorithm objects.
|
|
34
31
|
"""
|
|
35
32
|
|
|
36
33
|
def __init__(
|
|
37
34
|
self,
|
|
38
|
-
|
|
35
|
+
data_provider_service: DataProviderService,
|
|
39
36
|
order_service,
|
|
40
37
|
portfolio_service,
|
|
41
38
|
portfolio_snapshot_service,
|
|
42
39
|
position_repository,
|
|
43
40
|
trade_service,
|
|
44
|
-
performance_service,
|
|
45
41
|
configuration_service,
|
|
46
42
|
portfolio_configuration_service,
|
|
47
|
-
strategy_orchestrator_service
|
|
48
43
|
):
|
|
49
44
|
super().__init__()
|
|
50
|
-
self._resource_directory = None
|
|
51
45
|
self._order_service = order_service
|
|
52
46
|
self._trade_service = trade_service
|
|
53
47
|
self._portfolio_service = portfolio_service
|
|
54
|
-
self._data_index = {
|
|
55
|
-
TradingDataType.OHLCV: {},
|
|
56
|
-
TradingDataType.TICKER: {}
|
|
57
|
-
}
|
|
58
|
-
self._performance_service = performance_service
|
|
59
48
|
self._portfolio_snapshot_service = portfolio_snapshot_service
|
|
60
49
|
self._position_repository = position_repository
|
|
61
|
-
self._market_data_source_service: MarketDataSourceService \
|
|
62
|
-
= market_data_source_service
|
|
63
|
-
self._backtest_market_data_sources = []
|
|
64
50
|
self._configuration_service = configuration_service
|
|
65
|
-
self._portfolio_configuration_service
|
|
66
|
-
|
|
51
|
+
self._portfolio_configuration_service: PortfolioConfigurationService \
|
|
52
|
+
= portfolio_configuration_service
|
|
53
|
+
self._data_provider_service = data_provider_service
|
|
67
54
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
55
|
+
def validate_strategy_for_vector_backtest(self, strategy):
|
|
56
|
+
"""
|
|
57
|
+
Validate if the strategy is suitable for backtesting.
|
|
71
58
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
self._resource_directory = resource_directory
|
|
59
|
+
Args:
|
|
60
|
+
strategy: The strategy to validate.
|
|
75
61
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
context,
|
|
80
|
-
strategy_orchestrator_service,
|
|
81
|
-
backtest_date_range: BacktestDateRange,
|
|
82
|
-
initial_amount=None
|
|
83
|
-
) -> BacktestResult:
|
|
62
|
+
Raises:
|
|
63
|
+
OperationalException: If the strategy does not have the required
|
|
64
|
+
buy/sell signal functions.
|
|
84
65
|
"""
|
|
85
|
-
|
|
86
|
-
|
|
66
|
+
if not hasattr(strategy, 'generate_buy_signals'):
|
|
67
|
+
raise OperationalException(
|
|
68
|
+
"Strategy must define a vectorized buy signal function "
|
|
69
|
+
"(buy_signal_vectorized)."
|
|
70
|
+
)
|
|
71
|
+
if not hasattr(strategy, 'generate_sell_signals'):
|
|
72
|
+
raise OperationalException(
|
|
73
|
+
"Strategy must define a vectorized sell signal function "
|
|
74
|
+
"(sell_signal_vectorized)."
|
|
75
|
+
)
|
|
87
76
|
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
def _get_data_frame_index(self, data: Union[pl.DataFrame, pd.DataFrame]):
|
|
78
|
+
"""
|
|
79
|
+
Function to return the index for a given df. If the provided
|
|
80
|
+
data is of type pandas Dataframe, first will be checked if
|
|
81
|
+
it has a index. If this is not the case the function will
|
|
82
|
+
check if there is a 'DateTime' column and add this
|
|
83
|
+
as the index.
|
|
84
|
+
|
|
85
|
+
For a polars DataFrame, the 'DateTime' column will be
|
|
86
|
+
used as the index if it exists.
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
the backtest is run for each date in the schedule.
|
|
88
|
+
If no index is found an exception will be raised.
|
|
93
89
|
|
|
94
90
|
Args:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
context (Context): The context of the object of the application
|
|
91
|
+
data: The data frame to process.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
OperationalException: If no valid index is found.
|
|
100
95
|
|
|
101
96
|
Returns:
|
|
102
|
-
|
|
97
|
+
The index of the data frame.
|
|
103
98
|
"""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
if isinstance(data, pl.DataFrame):
|
|
100
|
+
if "Datetime" in data.columns:
|
|
101
|
+
return data["Datetime"]
|
|
102
|
+
else:
|
|
103
|
+
raise OperationalException("No valid index found.")
|
|
104
|
+
elif isinstance(data, pd.DataFrame):
|
|
105
|
+
if data.index is not None:
|
|
106
|
+
return data.index
|
|
107
|
+
elif "Datetime" in data.columns:
|
|
108
|
+
return data["Datetime"]
|
|
109
|
+
else:
|
|
110
|
+
raise OperationalException("No valid index found.")
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError("Unsupported data frame type.")
|
|
113
|
+
|
|
114
|
+
def create_vector_backtest(
|
|
115
|
+
self,
|
|
116
|
+
strategy,
|
|
117
|
+
backtest_date_range: BacktestDateRange,
|
|
118
|
+
risk_free_rate: float = 0.027,
|
|
119
|
+
initial_amount: float = None,
|
|
120
|
+
trading_symbol: str = None,
|
|
121
|
+
market: str = None,
|
|
122
|
+
) -> BacktestRun:
|
|
123
|
+
"""
|
|
124
|
+
Vectorized backtest for multiple assets using strategy
|
|
125
|
+
buy/sell signals.
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
127
|
+
Args:
|
|
128
|
+
strategy: The strategy to backtest.
|
|
129
|
+
backtest_date_range: The date range for the backtest.
|
|
130
|
+
risk_free_rate: The risk-free rate to use for the backtest
|
|
131
|
+
metrics. Default is 0.027 (2.7%).
|
|
132
|
+
initial_amount: The initial amount to use for the backtest.
|
|
133
|
+
If None, the initial amount will be taken from the first
|
|
134
|
+
portfolio configuration.
|
|
135
|
+
trading_symbol: The trading symbol to use for the backtest.
|
|
136
|
+
If None, the trading symbol will be taken from the first
|
|
137
|
+
portfolio configuration.
|
|
138
|
+
market: The market to use for the backtest. If None, the market
|
|
139
|
+
will be taken from the first portfolio configuration.
|
|
111
140
|
|
|
112
|
-
|
|
141
|
+
Returns:
|
|
142
|
+
BacktestRun: The backtest run containing the results and metrics.
|
|
143
|
+
"""
|
|
144
|
+
portfolio_configurations = self._portfolio_configuration_service\
|
|
145
|
+
.get_all()
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
portfolio_configurations is None
|
|
149
|
+
or len(portfolio_configurations) == 0
|
|
150
|
+
) and (
|
|
151
|
+
initial_amount is None
|
|
152
|
+
or trading_symbol is None
|
|
153
|
+
or market is None
|
|
154
|
+
):
|
|
155
|
+
raise OperationalException(
|
|
156
|
+
"No initial amount, trading symbol or market provided "
|
|
157
|
+
"for the backtest and no portfolio configurations found. "
|
|
158
|
+
"please register a portfolio configuration "
|
|
159
|
+
"or specify the initial amount, trading symbol and "
|
|
160
|
+
"market parameters before running a backtest."
|
|
161
|
+
)
|
|
113
162
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
163
|
+
if portfolio_configurations is None \
|
|
164
|
+
or len(portfolio_configurations) == 0:
|
|
165
|
+
portfolio_configurations = []
|
|
166
|
+
portfolio_configurations.append(
|
|
167
|
+
PortfolioConfiguration(
|
|
168
|
+
identifier="vector_backtest",
|
|
169
|
+
market=market,
|
|
170
|
+
trading_symbol=trading_symbol,
|
|
171
|
+
initial_balance=initial_amount
|
|
120
172
|
)
|
|
121
|
-
self._portfolio_service.delete(portfolio.id)
|
|
122
|
-
|
|
123
|
-
# Check if the portfolio configuration has an initial balance
|
|
124
|
-
self._portfolio_service.create_portfolio_from_configuration(
|
|
125
|
-
portfolio_configuration,
|
|
126
|
-
initial_amount=initial_amount,
|
|
127
|
-
created_at=backtest_date_range.start_date,
|
|
128
173
|
)
|
|
129
174
|
|
|
130
|
-
|
|
131
|
-
portfolios = self._portfolio_service.get_all()
|
|
132
|
-
initial_unallocated = 0
|
|
133
|
-
|
|
134
|
-
for portfolio in portfolios:
|
|
135
|
-
initial_unallocated += portfolio.unallocated
|
|
136
|
-
|
|
137
|
-
for strategy in algorithm.strategies:
|
|
138
|
-
strategy_profiles.append(strategy.strategy_profile)
|
|
175
|
+
portfolio_configuration = portfolio_configurations[0]
|
|
139
176
|
|
|
140
|
-
|
|
141
|
-
|
|
177
|
+
trading_symbol = portfolio_configurations[0].trading_symbol
|
|
178
|
+
portfolio = Portfolio.from_portfolio_configuration(
|
|
179
|
+
portfolio_configuration
|
|
180
|
+
)
|
|
142
181
|
|
|
143
|
-
|
|
144
|
-
|
|
182
|
+
# Load vectorized backtest data
|
|
183
|
+
data = self._data_provider_service.get_vectorized_backtest_data(
|
|
184
|
+
data_sources=strategy.data_sources,
|
|
145
185
|
start_date=backtest_date_range.start_date,
|
|
146
186
|
end_date=backtest_date_range.end_date
|
|
147
187
|
)
|
|
148
188
|
|
|
149
|
-
|
|
189
|
+
# Compute signals from strategy
|
|
190
|
+
buy_signals = strategy.generate_buy_signals(data)
|
|
191
|
+
sell_signals = strategy.generate_sell_signals(data)
|
|
150
192
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
strategy_profile = self.get_strategy_from_strategy_profiles(
|
|
158
|
-
strategy_profiles, row['id']
|
|
193
|
+
# Build master index (union of all indices in signal dict)
|
|
194
|
+
index = pd.Index([])
|
|
195
|
+
|
|
196
|
+
most_granular_ohlcv_data_source = \
|
|
197
|
+
BacktestService.get_most_granular_ohlcv_data_source(
|
|
198
|
+
strategy.data_sources
|
|
159
199
|
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
200
|
+
most_granular_ohlcv_data = self._data_provider_service.get_ohlcv_data(
|
|
201
|
+
symbol=most_granular_ohlcv_data_source.symbol,
|
|
202
|
+
start_date=backtest_date_range.start_date,
|
|
203
|
+
end_date=backtest_date_range.end_date,
|
|
204
|
+
pandas=True
|
|
163
205
|
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
206
|
+
|
|
207
|
+
# Make sure to filter out the buy and sell signals that are before
|
|
208
|
+
# the backtest start date
|
|
209
|
+
buy_signals = {k: v[v.index >= backtest_date_range.start_date]
|
|
210
|
+
for k, v in buy_signals.items()}
|
|
211
|
+
sell_signals = {k: v[v.index >= backtest_date_range.start_date]
|
|
212
|
+
for k, v in sell_signals.items()}
|
|
213
|
+
|
|
214
|
+
index = index.union(most_granular_ohlcv_data.index)
|
|
215
|
+
index = index.sort_values()
|
|
216
|
+
|
|
217
|
+
# Initialize trades and portfolio values
|
|
218
|
+
trades = []
|
|
219
|
+
orders = []
|
|
220
|
+
granular_ohlcv_data_order_by_symbol = {}
|
|
221
|
+
snapshots = [
|
|
222
|
+
PortfolioSnapshot(
|
|
223
|
+
trading_symbol=trading_symbol,
|
|
224
|
+
portfolio_id=portfolio.identifier,
|
|
225
|
+
created_at=backtest_date_range.start_date,
|
|
226
|
+
unallocated=initial_amount,
|
|
227
|
+
total_value=initial_amount,
|
|
228
|
+
total_net_gain=0.0
|
|
168
229
|
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
for symbol in buy_signals.keys():
|
|
233
|
+
full_symbol = f"{symbol}/{trading_symbol}"
|
|
234
|
+
# find PositionSize object
|
|
235
|
+
pos_size_obj = next(
|
|
236
|
+
(p for p in strategy.position_sizes if
|
|
237
|
+
p.symbol == symbol), None
|
|
238
|
+
)
|
|
239
|
+
# Load most granular OHLCV data for the symbol
|
|
240
|
+
df = self._data_provider_service.get_ohlcv_data(
|
|
241
|
+
symbol=full_symbol,
|
|
242
|
+
start_date=backtest_date_range.start_date,
|
|
243
|
+
end_date=backtest_date_range.end_date,
|
|
244
|
+
pandas=True
|
|
245
|
+
)
|
|
246
|
+
granular_ohlcv_data_order_by_symbol[full_symbol] = df
|
|
247
|
+
|
|
248
|
+
# Align signals with most granular OHLCV data
|
|
249
|
+
close = df["Close"]
|
|
250
|
+
buy_signal = buy_signals[symbol].reindex(index, fill_value=False)
|
|
251
|
+
sell_signal = sell_signals[symbol].reindex(index, fill_value=False)
|
|
252
|
+
|
|
253
|
+
signal = pd.Series(0, index=index)
|
|
254
|
+
signal[buy_signal] = 1
|
|
255
|
+
signal[sell_signal] = -1
|
|
256
|
+
signal = signal.replace(0, np.nan).ffill().shift(1).fillna(0)
|
|
257
|
+
signal = signal.astype(float)
|
|
258
|
+
|
|
259
|
+
if pos_size_obj is None:
|
|
260
|
+
raise OperationalException(
|
|
261
|
+
f"No position size object defined "
|
|
262
|
+
f"for symbol {symbol}, please make sure to "
|
|
263
|
+
f"register a PositionSize object in the strategy."
|
|
264
|
+
)
|
|
180
265
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
266
|
+
capital_for_trade = pos_size_obj.get_size(
|
|
267
|
+
Portfolio(
|
|
268
|
+
unallocated=initial_amount,
|
|
269
|
+
initial_balance=initial_amount,
|
|
270
|
+
trading_symbol=trading_symbol,
|
|
271
|
+
net_size=0,
|
|
272
|
+
market="BACKTEST",
|
|
273
|
+
identifier="vector_backtest"
|
|
274
|
+
) if pos_size_obj else (initial_amount / len(buy_signals)),
|
|
275
|
+
asset_price=close.iloc[0]
|
|
276
|
+
)
|
|
184
277
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
278
|
+
# Trade generation
|
|
279
|
+
last_trade = None
|
|
280
|
+
|
|
281
|
+
# Align signals with most granular OHLCV data
|
|
282
|
+
close = df["Close"].reindex(index, method='ffill')
|
|
283
|
+
buy_signal = buy_signals[symbol].reindex(index, fill_value=False)
|
|
284
|
+
sell_signal = sell_signals[symbol].reindex(index, fill_value=False)
|
|
285
|
+
|
|
286
|
+
# Loop over all timestamps in the backtest
|
|
287
|
+
for i in range(len(index)):
|
|
288
|
+
|
|
289
|
+
# 1 = buy, -1 = sell, 0 = hold
|
|
290
|
+
current_signal = signal.iloc[i]
|
|
291
|
+
current_price = float(close.iloc[i])
|
|
292
|
+
current_date = index[i]
|
|
293
|
+
|
|
294
|
+
# Convert the pd.Timestamp to an utc datetime object
|
|
295
|
+
if isinstance(current_date, pd.Timestamp):
|
|
296
|
+
current_date = current_date.to_pydatetime()
|
|
297
|
+
|
|
298
|
+
if current_date.tzinfo is None:
|
|
299
|
+
current_date = current_date.replace(tzinfo=timezone.utc)
|
|
300
|
+
|
|
301
|
+
# If we are not in a position, and we get a buy signal
|
|
302
|
+
if current_signal == 1 and last_trade is None:
|
|
303
|
+
amount = float(capital_for_trade / current_price)
|
|
304
|
+
buy_order = Order(
|
|
305
|
+
id=uuid4(),
|
|
306
|
+
target_symbol=symbol,
|
|
307
|
+
trading_symbol=trading_symbol,
|
|
308
|
+
order_type=OrderType.LIMIT,
|
|
309
|
+
price=current_price,
|
|
310
|
+
amount=amount,
|
|
311
|
+
status=OrderStatus.CLOSED,
|
|
312
|
+
created_at=current_date,
|
|
313
|
+
updated_at=current_date,
|
|
314
|
+
order_side=OrderSide.BUY
|
|
315
|
+
)
|
|
316
|
+
orders.append(buy_order)
|
|
317
|
+
trade = Trade(
|
|
318
|
+
id=uuid4(),
|
|
319
|
+
orders=[buy_order],
|
|
320
|
+
target_symbol=symbol,
|
|
321
|
+
trading_symbol=trading_symbol,
|
|
322
|
+
available_amount=amount,
|
|
323
|
+
remaining=0,
|
|
324
|
+
filled_amount=amount,
|
|
325
|
+
open_price=current_price,
|
|
326
|
+
opened_at=current_date,
|
|
327
|
+
closed_at=None,
|
|
328
|
+
amount=amount,
|
|
329
|
+
status=TradeStatus.OPEN.value,
|
|
330
|
+
cost=capital_for_trade
|
|
331
|
+
)
|
|
332
|
+
last_trade = trade
|
|
333
|
+
trades.append(trade)
|
|
334
|
+
|
|
335
|
+
# If we are in a position, and we get a sell signal
|
|
336
|
+
if current_signal == -1 and last_trade is not None:
|
|
337
|
+
net_gain_val = (
|
|
338
|
+
current_price - last_trade.open_price
|
|
339
|
+
) * last_trade.available_amount
|
|
340
|
+
sell_order = Order(
|
|
341
|
+
id=uuid4(),
|
|
342
|
+
target_symbol=symbol,
|
|
343
|
+
trading_symbol=trading_symbol,
|
|
344
|
+
order_type=OrderType.LIMIT,
|
|
345
|
+
price=current_price,
|
|
346
|
+
amount=last_trade.available_amount,
|
|
347
|
+
status=OrderStatus.CLOSED,
|
|
348
|
+
created_at=current_date,
|
|
349
|
+
updated_at=current_date,
|
|
350
|
+
order_side=OrderSide.SELL
|
|
351
|
+
)
|
|
352
|
+
orders.append(sell_order)
|
|
353
|
+
trade_orders = last_trade.orders
|
|
354
|
+
trade_orders.append(sell_order)
|
|
355
|
+
last_trade.update(
|
|
356
|
+
{
|
|
357
|
+
"orders": trade_orders,
|
|
358
|
+
"closed_at": current_date,
|
|
359
|
+
"status": TradeStatus.CLOSED,
|
|
360
|
+
"updated_at": current_date,
|
|
361
|
+
"net_gain": net_gain_val
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
last_trade = None
|
|
365
|
+
|
|
366
|
+
unallocated = initial_amount
|
|
367
|
+
total_net_gain = 0.0
|
|
368
|
+
open_trades = []
|
|
369
|
+
|
|
370
|
+
# Create portfolio snapshots
|
|
371
|
+
for ts in index:
|
|
372
|
+
allocated = 0
|
|
373
|
+
interval_datetime = pd.Timestamp(ts).to_pydatetime()
|
|
374
|
+
interval_datetime = interval_datetime.replace(tzinfo=timezone.utc)
|
|
375
|
+
|
|
376
|
+
for trade in trades:
|
|
377
|
+
|
|
378
|
+
if trade.opened_at == interval_datetime:
|
|
379
|
+
# Snapshot taken at the moment a trade is opened
|
|
380
|
+
unallocated -= trade.cost
|
|
381
|
+
open_trades.append(trade)
|
|
382
|
+
|
|
383
|
+
if trade.closed_at == interval_datetime:
|
|
384
|
+
# Snapshot taken at the moment a trade is closed
|
|
385
|
+
unallocated += trade.cost + trade.net_gain
|
|
386
|
+
total_net_gain += trade.net_gain
|
|
387
|
+
open_trades.remove(trade)
|
|
388
|
+
|
|
389
|
+
for open_trade in open_trades:
|
|
390
|
+
ohlcv = granular_ohlcv_data_order_by_symbol[
|
|
391
|
+
f"{open_trade.target_symbol}/{trading_symbol}"
|
|
392
|
+
]
|
|
393
|
+
try:
|
|
394
|
+
price = ohlcv.loc[:ts, "Close"].iloc[-1]
|
|
395
|
+
open_trade.last_reported_price = price
|
|
396
|
+
except IndexError:
|
|
397
|
+
continue # skip if no price yet
|
|
398
|
+
|
|
399
|
+
allocated += open_trade.filled_amount * price
|
|
400
|
+
|
|
401
|
+
# total_value = invested_value + unallocated
|
|
402
|
+
# total_net_gain = total_value - initial_amount
|
|
403
|
+
snapshots.append(
|
|
404
|
+
PortfolioSnapshot(
|
|
405
|
+
portfolio_id=portfolio.identifier,
|
|
406
|
+
created_at=interval_datetime,
|
|
407
|
+
unallocated=unallocated,
|
|
408
|
+
total_value=unallocated + allocated,
|
|
409
|
+
total_net_gain=total_net_gain
|
|
410
|
+
)
|
|
188
411
|
)
|
|
189
|
-
self._portfolio_service.delete(portfolio.id)
|
|
190
412
|
|
|
191
|
-
|
|
413
|
+
unique_symbols = set()
|
|
414
|
+
for trade in trades:
|
|
415
|
+
unique_symbols.add(trade.target_symbol)
|
|
192
416
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
417
|
+
number_of_trades_closed = len(
|
|
418
|
+
[t for t in trades if TradeStatus.CLOSED.equals(t.status)]
|
|
419
|
+
)
|
|
420
|
+
number_of_trades_open = len(
|
|
421
|
+
[t for t in trades if TradeStatus.OPEN.equals(t.status)]
|
|
422
|
+
)
|
|
423
|
+
# Create a backtest run object
|
|
424
|
+
run = BacktestRun(
|
|
425
|
+
trading_symbol=trading_symbol,
|
|
426
|
+
initial_unallocated=initial_amount,
|
|
427
|
+
number_of_runs=1,
|
|
428
|
+
portfolio_snapshots=snapshots,
|
|
429
|
+
trades=trades,
|
|
430
|
+
orders=orders,
|
|
431
|
+
positions=[],
|
|
432
|
+
created_at=datetime.now(timezone.utc),
|
|
433
|
+
backtest_start_date=backtest_date_range.start_date,
|
|
434
|
+
backtest_end_date=backtest_date_range.end_date,
|
|
435
|
+
backtest_date_range_name=backtest_date_range.name,
|
|
436
|
+
number_of_days=(
|
|
437
|
+
backtest_date_range.end_date - backtest_date_range.end_date
|
|
438
|
+
).days,
|
|
439
|
+
number_of_trades=len(trades),
|
|
440
|
+
number_of_orders=len(orders),
|
|
441
|
+
number_of_trades_closed=number_of_trades_closed,
|
|
442
|
+
number_of_trades_open=number_of_trades_open,
|
|
443
|
+
number_of_positions=len(unique_symbols),
|
|
444
|
+
symbols=list(buy_signals.keys())
|
|
198
445
|
)
|
|
199
|
-
algorithm.config[BACKTESTING_INDEX_DATETIME] = index_date
|
|
200
|
-
market_data = {}
|
|
201
|
-
|
|
202
|
-
if strategy.strategy_profile.market_data_sources is not None:
|
|
203
|
-
|
|
204
|
-
for data_id in strategy.strategy_profile.market_data_sources:
|
|
205
|
-
|
|
206
|
-
if isinstance(data_id, MarketDataSource):
|
|
207
|
-
market_data[data_id.get_identifier()] = \
|
|
208
|
-
self._market_data_source_service.get_data(
|
|
209
|
-
data_id.get_identifier()
|
|
210
|
-
)
|
|
211
|
-
else:
|
|
212
|
-
market_data[data_id] = \
|
|
213
|
-
self._market_data_source_service.get_data(data_id)
|
|
214
|
-
|
|
215
|
-
strategy.run_strategy(context=context, market_data=market_data)
|
|
216
446
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
context=context, strategy=strategy, config=config
|
|
447
|
+
# Create backtest metrics
|
|
448
|
+
run.backtest_metrics = create_backtest_metrics(
|
|
449
|
+
run, risk_free_rate=risk_free_rate
|
|
221
450
|
)
|
|
451
|
+
return run
|
|
222
452
|
|
|
223
453
|
def generate_schedule(
|
|
224
|
-
self,
|
|
225
|
-
|
|
454
|
+
self,
|
|
455
|
+
strategies,
|
|
456
|
+
tasks,
|
|
457
|
+
start_date,
|
|
458
|
+
end_date
|
|
459
|
+
) -> Dict[datetime, Dict[str, List[str]]]:
|
|
226
460
|
"""
|
|
227
|
-
|
|
228
|
-
calculate when the strategies should run based on the given start
|
|
229
|
-
and end date. The schedule will be stored in a pandas DataFrame.
|
|
230
|
-
|
|
231
|
-
Args:
|
|
232
|
-
strategies: The strategies to generate the schedule for
|
|
233
|
-
start_date: The start date of the schedule
|
|
234
|
-
end_date: The end date of the schedule
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
pd.DataFrame: The schedule DataFrame
|
|
461
|
+
Generates a dict-based schedule: datetime => {strategy_ids, task_ids}
|
|
238
462
|
"""
|
|
239
|
-
|
|
463
|
+
schedule = defaultdict(
|
|
464
|
+
lambda: {"strategy_ids": set(), "task_ids": set(tasks)}
|
|
465
|
+
)
|
|
240
466
|
|
|
241
467
|
for strategy in strategies:
|
|
242
|
-
|
|
243
|
-
time_unit = strategy.strategy_profile.time_unit
|
|
468
|
+
strategy_id = strategy.strategy_profile.strategy_id
|
|
244
469
|
interval = strategy.strategy_profile.interval
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
while current_time <= end_date:
|
|
248
|
-
data.append({
|
|
249
|
-
"id": id,
|
|
250
|
-
'run_time': current_time,
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
if TimeUnit.SECOND.equals(time_unit):
|
|
254
|
-
current_time += timedelta(seconds=interval)
|
|
255
|
-
elif TimeUnit.MINUTE.equals(time_unit):
|
|
256
|
-
current_time += timedelta(minutes=interval)
|
|
257
|
-
elif TimeUnit.HOUR.equals(time_unit):
|
|
258
|
-
current_time += timedelta(hours=interval)
|
|
259
|
-
elif TimeUnit.DAY.equals(time_unit):
|
|
260
|
-
current_time += timedelta(days=interval)
|
|
261
|
-
else:
|
|
262
|
-
raise ValueError(f"Unsupported time unit: {time_unit}")
|
|
263
|
-
|
|
264
|
-
schedule_df = pd.DataFrame(data)
|
|
265
|
-
|
|
266
|
-
if schedule_df.empty:
|
|
267
|
-
raise OperationalException(
|
|
268
|
-
"Could not generate schedule "
|
|
269
|
-
"for backtest, do you have a strategy "
|
|
270
|
-
"registered for your algorithm?"
|
|
271
|
-
)
|
|
470
|
+
time_unit = strategy.strategy_profile.time_unit
|
|
272
471
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
472
|
+
if time_unit == TimeUnit.SECOND:
|
|
473
|
+
step = timedelta(seconds=interval)
|
|
474
|
+
elif time_unit == TimeUnit.MINUTE:
|
|
475
|
+
step = timedelta(minutes=interval)
|
|
476
|
+
elif time_unit == TimeUnit.HOUR:
|
|
477
|
+
step = timedelta(hours=interval)
|
|
478
|
+
elif time_unit == TimeUnit.DAY:
|
|
479
|
+
step = timedelta(days=interval)
|
|
480
|
+
else:
|
|
481
|
+
raise ValueError(f"Unsupported time unit: {time_unit}")
|
|
482
|
+
|
|
483
|
+
t = start_date
|
|
484
|
+
while t <= end_date:
|
|
485
|
+
schedule[t]["strategy_ids"].add(strategy_id)
|
|
486
|
+
t += step
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
ts: {
|
|
490
|
+
"strategy_ids": sorted(data["strategy_ids"]),
|
|
491
|
+
"task_ids": sorted(data["task_ids"])
|
|
492
|
+
}
|
|
493
|
+
for ts, data in schedule.items()
|
|
494
|
+
}
|
|
276
495
|
|
|
277
496
|
def get_strategy_from_strategy_profiles(self, strategy_profiles, id):
|
|
278
497
|
|
|
@@ -283,309 +502,150 @@ class BacktestService(Observable):
|
|
|
283
502
|
|
|
284
503
|
raise ValueError(f"Strategy profile with id {id} not found.")
|
|
285
504
|
|
|
286
|
-
def
|
|
287
|
-
self,
|
|
288
|
-
algorithm,
|
|
289
|
-
context,
|
|
290
|
-
number_of_runs,
|
|
291
|
-
backtest_date_range: BacktestDateRange,
|
|
292
|
-
initial_unallocated=0
|
|
293
|
-
) -> BacktestResult:
|
|
505
|
+
def _get_initial_unallocated(self) -> float:
|
|
294
506
|
"""
|
|
295
|
-
|
|
296
|
-
will create a backtest report for the given algorithm and return
|
|
297
|
-
the backtest report instance.
|
|
298
|
-
|
|
299
|
-
It will calculate various performance metrics for the backtest.
|
|
300
|
-
Also, it will add all traces to the backtest report. The traces
|
|
301
|
-
are collected from each strategy that was run during the backtest.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
algorithm: The algorithm to create the backtest report for
|
|
305
|
-
number_of_runs: The number of runs
|
|
306
|
-
backtest_date_range: The backtest date range of the backtest
|
|
307
|
-
initial_unallocated: The initial unallocated amount
|
|
507
|
+
Get the initial unallocated amount for the backtest.
|
|
308
508
|
|
|
309
509
|
Returns:
|
|
310
|
-
|
|
510
|
+
float: The initial unallocated amount.
|
|
311
511
|
"""
|
|
512
|
+
portfolios = self._portfolio_service.get_all()
|
|
513
|
+
initial_unallocated = 0.0
|
|
312
514
|
|
|
313
|
-
for portfolio in
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# Check if strategy_id is None
|
|
317
|
-
if None in ids:
|
|
318
|
-
# Remove None from ids
|
|
319
|
-
ids = [x for x in ids if x is not None]
|
|
320
|
-
|
|
321
|
-
positions = self._position_repository.get_all({
|
|
322
|
-
"portfolio": portfolio.id
|
|
323
|
-
})
|
|
324
|
-
tickers = {}
|
|
325
|
-
|
|
326
|
-
for position in positions:
|
|
327
|
-
|
|
328
|
-
if position.symbol != portfolio.trading_symbol:
|
|
329
|
-
ticker_symbol = \
|
|
330
|
-
f"{position.symbol}/{portfolio.trading_symbol}"
|
|
331
|
-
|
|
332
|
-
if not self._market_data_source_service\
|
|
333
|
-
.has_ticker_market_data_source(
|
|
334
|
-
symbol=ticker_symbol, market=portfolio.market
|
|
335
|
-
):
|
|
336
|
-
raise OperationalException(
|
|
337
|
-
f"Ticker market data source for "
|
|
338
|
-
f"symbol {ticker_symbol} and market "
|
|
339
|
-
f"{portfolio.market} not found, please make "
|
|
340
|
-
f"sure you register a ticker market data "
|
|
341
|
-
f"source for this symbol and market in "
|
|
342
|
-
f"backtest mode. Otherwise, the backtest "
|
|
343
|
-
f"report cannot be generated."
|
|
344
|
-
)
|
|
345
|
-
tickers[ticker_symbol] = \
|
|
346
|
-
self._market_data_source_service.get_ticker(
|
|
347
|
-
f"{position.symbol}/{portfolio.trading_symbol}",
|
|
348
|
-
market=portfolio.market
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
positions = self._position_repository.get_all({
|
|
352
|
-
"portfolio": portfolio.id
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
# Create the last snapshot of the portfolio
|
|
356
|
-
self._portfolio_snapshot_service.create_snapshot(
|
|
357
|
-
portfolio=portfolio,
|
|
358
|
-
created_at=backtest_date_range.end_date
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
backtest_report = BacktestResult(
|
|
362
|
-
name=algorithm.name,
|
|
363
|
-
backtest_date_range=backtest_date_range,
|
|
364
|
-
initial_unallocated=initial_unallocated,
|
|
365
|
-
trading_symbol=portfolio.trading_symbol,
|
|
366
|
-
created_at=datetime.now(tz=timezone.utc),
|
|
367
|
-
portfolio_snapshots=self._portfolio_snapshot_service.get_all(
|
|
368
|
-
{"portfolio_id": portfolio.id}
|
|
369
|
-
),
|
|
370
|
-
number_of_runs=number_of_runs,
|
|
371
|
-
trades=self._trade_service.get_all(
|
|
372
|
-
{"portfolio": portfolio.id}
|
|
373
|
-
),
|
|
374
|
-
orders=self._order_service.get_all(
|
|
375
|
-
{"portfolio": portfolio.id}
|
|
376
|
-
),
|
|
377
|
-
positions=self._position_repository.get_all(
|
|
378
|
-
{"portfolio": portfolio.id}
|
|
379
|
-
),
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
# Calculate metrics for the backtest report
|
|
383
|
-
return backtest_report
|
|
384
|
-
|
|
385
|
-
def set_backtest_market_data_sources(self, market_data_sources):
|
|
386
|
-
self._backtest_market_data_sources = market_data_sources
|
|
387
|
-
|
|
388
|
-
def get_backtest_market_data_sources(self):
|
|
389
|
-
return self._backtest_market_data_sources
|
|
390
|
-
|
|
391
|
-
def get_backtest_market_data_source(self, symbol, market):
|
|
392
|
-
|
|
393
|
-
for market_data_source in self._backtest_market_data_sources:
|
|
394
|
-
if market_data_source.symbol == symbol \
|
|
395
|
-
and market_data_source.market == market:
|
|
396
|
-
return market_data_source
|
|
397
|
-
raise OperationalException(
|
|
398
|
-
f"Market data source for "
|
|
399
|
-
f"symbol {symbol} and market {market} not found"
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
def _check_if_required_market_data_sources_are_registered(self):
|
|
403
|
-
"""
|
|
404
|
-
Check if the required market data sources are registered.
|
|
515
|
+
for portfolio in portfolios:
|
|
516
|
+
initial_unallocated += portfolio.initial_balance
|
|
405
517
|
|
|
406
|
-
|
|
407
|
-
if a ticker market data source is registered for the symbol and market.
|
|
408
|
-
"""
|
|
409
|
-
symbols = self._configuration_service.config[SYMBOLS]
|
|
410
|
-
|
|
411
|
-
if symbols is not None:
|
|
412
|
-
|
|
413
|
-
for symbol in symbols:
|
|
414
|
-
if not self._market_data_source_service\
|
|
415
|
-
.has_ticker_market_data_source(
|
|
416
|
-
symbol=symbol
|
|
417
|
-
):
|
|
418
|
-
raise OperationalException(
|
|
419
|
-
f"Ticker market data source for symbol {symbol} not "
|
|
420
|
-
f"found, please make sure you register a ticker "
|
|
421
|
-
f"market data source for this symbol in backtest "
|
|
422
|
-
f"mode. Otherwise, the backtest report "
|
|
423
|
-
f"cannot be generated."
|
|
424
|
-
)
|
|
518
|
+
return initial_unallocated
|
|
425
519
|
|
|
426
|
-
def
|
|
520
|
+
def create_backtest(
|
|
427
521
|
self,
|
|
428
|
-
|
|
522
|
+
algorithm,
|
|
523
|
+
number_of_runs,
|
|
429
524
|
backtest_date_range: BacktestDateRange,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
Function to get a report based on the algorithm name and
|
|
434
|
-
backtest date range if it exists.
|
|
435
|
-
|
|
436
|
-
Args:
|
|
437
|
-
algorithm_name: str - The name of the algorithm
|
|
438
|
-
backtest_date_range: BacktestDateRange - The backtest date range
|
|
439
|
-
directory: str - The output directory
|
|
440
|
-
|
|
441
|
-
Returns:
|
|
442
|
-
BacktestResult - The backtest report if it exists, otherwise None
|
|
525
|
+
risk_free_rate,
|
|
526
|
+
strategy_directory_path=None
|
|
527
|
+
) -> Backtest:
|
|
443
528
|
"""
|
|
529
|
+
Create a backtest for the given algorithm.
|
|
444
530
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
# and backtest date range
|
|
450
|
-
if self._is_backtest_report(os.path.join(root, file)):
|
|
451
|
-
# Read the file
|
|
452
|
-
with open(os.path.join(root, file), "r") as json_file:
|
|
453
|
-
|
|
454
|
-
name = \
|
|
455
|
-
self._get_algorithm_name_from_backtest_report_file(
|
|
456
|
-
os.path.join(root, file)
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
if name == algorithm_name:
|
|
460
|
-
backtest_start_date = \
|
|
461
|
-
self._get_start_date_from_backtest_report_file(
|
|
462
|
-
os.path.join(root, file)
|
|
463
|
-
)
|
|
464
|
-
backtest_end_date = \
|
|
465
|
-
self._get_end_date_from_backtest_report_file(
|
|
466
|
-
os.path.join(root, file)
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
if backtest_start_date == \
|
|
470
|
-
backtest_date_range.start_date \
|
|
471
|
-
and backtest_end_date == \
|
|
472
|
-
backtest_date_range.end_date:
|
|
473
|
-
# Parse the JSON file
|
|
474
|
-
report = json.load(json_file)
|
|
475
|
-
# Convert the JSON file to a
|
|
476
|
-
# BacktestResult object
|
|
477
|
-
return BacktestResult.from_dict(report)
|
|
478
|
-
|
|
479
|
-
return None
|
|
480
|
-
|
|
481
|
-
def _get_start_date_from_backtest_report_file(self, path: str) -> datetime:
|
|
482
|
-
"""
|
|
483
|
-
Function to get the backtest start date from a backtest report file.
|
|
531
|
+
It will store all results and metrics in a Backtest object through
|
|
532
|
+
the BacktestResults and BacktestMetrics objects. Optionally,
|
|
533
|
+
it will also store the strategy related paths and backtest
|
|
534
|
+
data file paths.
|
|
484
535
|
|
|
485
536
|
Args:
|
|
486
|
-
|
|
537
|
+
algorithm: The algorithm to create the backtest report for
|
|
538
|
+
number_of_runs: The number of runs
|
|
539
|
+
backtest_date_range: The backtest date range of the backtest
|
|
540
|
+
risk_free_rate: The risk-free rate to use for the backtest metrics
|
|
541
|
+
strategy_directory_path (optional, str): The path to the
|
|
542
|
+
strategy directory
|
|
487
543
|
|
|
488
544
|
Returns:
|
|
489
|
-
|
|
545
|
+
Backtest: The backtest containing the results and metrics.
|
|
490
546
|
"""
|
|
491
547
|
|
|
492
|
-
# Get the
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
try:
|
|
496
|
-
# Parse the backtest start date
|
|
497
|
-
return datetime.strptime(
|
|
498
|
-
backtest_start_date, DATETIME_FORMAT_BACKTESTING
|
|
499
|
-
)
|
|
500
|
-
except ValueError:
|
|
501
|
-
# Try to parse the backtest start date with a different format
|
|
502
|
-
return parser.parse(backtest_start_date)
|
|
548
|
+
# Get the first portfolio
|
|
549
|
+
portfolio = self._portfolio_service.get_all()[0]
|
|
503
550
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
Function to get the backtest end date from a backtest report file.
|
|
551
|
+
# List all strategy related files in the strategy directory
|
|
552
|
+
strategy_related_paths = []
|
|
507
553
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
# Get the backtest end date from the file name
|
|
516
|
-
backtest_end_date = os.path.basename(path).split("_")[5]
|
|
554
|
+
if strategy_directory_path is not None:
|
|
555
|
+
if not os.path.exists(strategy_directory_path) or \
|
|
556
|
+
not os.path.isdir(strategy_directory_path):
|
|
557
|
+
raise OperationalException(
|
|
558
|
+
"Strategy directory does not exist"
|
|
559
|
+
)
|
|
517
560
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
561
|
+
strategy_files = os.listdir(strategy_directory_path)
|
|
562
|
+
for file in strategy_files:
|
|
563
|
+
source_file = os.path.join(strategy_directory_path, file)
|
|
564
|
+
if os.path.isfile(source_file):
|
|
565
|
+
strategy_related_paths.append(source_file)
|
|
566
|
+
else:
|
|
567
|
+
if algorithm is not None and hasattr(algorithm, 'strategies'):
|
|
568
|
+
for strategy in algorithm.strategies:
|
|
569
|
+
mod = sys.modules[strategy.__module__]
|
|
570
|
+
strategy_directory_path = os.path.dirname(mod.__file__)
|
|
571
|
+
strategy_files = os.listdir(strategy_directory_path)
|
|
572
|
+
for file in strategy_files:
|
|
573
|
+
source_file = os.path.join(
|
|
574
|
+
strategy_directory_path, file
|
|
575
|
+
)
|
|
576
|
+
if os.path.isfile(source_file):
|
|
577
|
+
strategy_related_paths.append(source_file)
|
|
578
|
+
|
|
579
|
+
run = BacktestRun(
|
|
580
|
+
backtest_start_date=backtest_date_range.start_date,
|
|
581
|
+
backtest_end_date=backtest_date_range.end_date,
|
|
582
|
+
backtest_date_range_name=backtest_date_range.name,
|
|
583
|
+
initial_unallocated=self._get_initial_unallocated(),
|
|
584
|
+
trading_symbol=portfolio.trading_symbol,
|
|
585
|
+
created_at=datetime.now(tz=timezone.utc),
|
|
586
|
+
portfolio_snapshots=self._portfolio_snapshot_service.get_all(
|
|
587
|
+
{"portfolio_id": portfolio.id}
|
|
588
|
+
),
|
|
589
|
+
number_of_runs=number_of_runs,
|
|
590
|
+
trades=self._trade_service.get_all(
|
|
591
|
+
{"portfolio": portfolio.id}
|
|
592
|
+
),
|
|
593
|
+
orders=self._order_service.get_all(
|
|
594
|
+
{"portfolio": portfolio.id}
|
|
595
|
+
),
|
|
596
|
+
positions=self._position_repository.get_all(
|
|
597
|
+
{"portfolio": portfolio.id}
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
backtest_metrics = create_backtest_metrics(
|
|
601
|
+
run, risk_free_rate=risk_free_rate
|
|
602
|
+
)
|
|
603
|
+
run.backtest_metrics = backtest_metrics
|
|
604
|
+
return Backtest(
|
|
605
|
+
backtest_runs=[run],
|
|
606
|
+
backtest_summary=generate_backtest_summary_metrics(
|
|
607
|
+
[backtest_metrics]
|
|
522
608
|
)
|
|
523
|
-
|
|
524
|
-
# Try to parse the backtest end date with a different format
|
|
525
|
-
return parser.parse(backtest_end_date)
|
|
526
|
-
|
|
527
|
-
def _get_algorithm_name_from_backtest_report_file(self, path: str) -> str:
|
|
528
|
-
"""
|
|
529
|
-
Function to get the algorithm name from a backtest report file.
|
|
530
|
-
|
|
531
|
-
Args:
|
|
532
|
-
path: str - The path to the backtest report file
|
|
533
|
-
|
|
534
|
-
Returns:
|
|
535
|
-
str - The algorithm name
|
|
536
|
-
"""
|
|
537
|
-
# Get the word between "report_" and "_backtest_start_date"
|
|
538
|
-
# it can contain _
|
|
539
|
-
# Get the algorithm name from the file name
|
|
540
|
-
algorithm_name = os.path.basename(path).split("_")[1]
|
|
541
|
-
return algorithm_name
|
|
609
|
+
)
|
|
542
610
|
|
|
543
|
-
|
|
611
|
+
@staticmethod
|
|
612
|
+
def get_most_granular_ohlcv_data_source(data_sources):
|
|
544
613
|
"""
|
|
545
|
-
|
|
614
|
+
Get the most granular data source from a list of data sources.
|
|
546
615
|
|
|
547
616
|
Args:
|
|
548
|
-
|
|
617
|
+
data_sources: List of data sources.
|
|
549
618
|
|
|
550
619
|
Returns:
|
|
551
|
-
|
|
620
|
+
The most granular data source.
|
|
552
621
|
"""
|
|
622
|
+
granularity_order = {
|
|
623
|
+
TimeFrame.ONE_MINUTE: 1,
|
|
624
|
+
TimeFrame.FIVE_MINUTE: 5,
|
|
625
|
+
TimeFrame.FIFTEEN_MINUTE: 15,
|
|
626
|
+
TimeFrame.ONE_HOUR: 60,
|
|
627
|
+
TimeFrame.TWO_HOUR: 120,
|
|
628
|
+
TimeFrame.FOUR_HOUR: 240,
|
|
629
|
+
TimeFrame.TWELVE_HOUR: 720,
|
|
630
|
+
TimeFrame.ONE_DAY: 1440,
|
|
631
|
+
TimeFrame.ONE_WEEK: 10080,
|
|
632
|
+
TimeFrame.ONE_MONTH: 43200
|
|
633
|
+
}
|
|
553
634
|
|
|
554
|
-
|
|
555
|
-
|
|
635
|
+
most_granular = None
|
|
636
|
+
highest_granularity = float('inf')
|
|
556
637
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
BACKTEST_REPORT_FILE_NAME_PATTERN, os.path.basename(path)
|
|
561
|
-
):
|
|
562
|
-
return True
|
|
638
|
+
ohlcv_data_sources = [
|
|
639
|
+
ds for ds in data_sources if DataType.OHLCV.equals(ds.data_type)
|
|
640
|
+
]
|
|
563
641
|
|
|
564
|
-
|
|
642
|
+
if len(ohlcv_data_sources) == 0:
|
|
643
|
+
raise OperationalException("No OHLCV data sources found")
|
|
565
644
|
|
|
566
|
-
|
|
567
|
-
def create_report_directory_name(report) -> str:
|
|
568
|
-
"""
|
|
569
|
-
Function to create a directory name for a backtest report.
|
|
570
|
-
The directory name will be automatically generated based on the
|
|
571
|
-
algorithm name and creation date.
|
|
645
|
+
for source in ohlcv_data_sources:
|
|
572
646
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
647
|
+
if granularity_order[source.time_frame] < highest_granularity:
|
|
648
|
+
highest_granularity = granularity_order[source.time_frame]
|
|
649
|
+
most_granular = source
|
|
576
650
|
|
|
577
|
-
|
|
578
|
-
directory_name: str The directory name for the
|
|
579
|
-
backtest report file.
|
|
580
|
-
"""
|
|
581
|
-
created_at = report.results\
|
|
582
|
-
.created_at.strftime(DATETIME_FORMAT_BACKTESTING)
|
|
583
|
-
backtest_start_date = report.results.backtest_date_range.start_date
|
|
584
|
-
backtest_end_date = report.results.backtest_date_range.end_date
|
|
585
|
-
name = report.results.name
|
|
586
|
-
|
|
587
|
-
start_date = backtest_start_date.strftime(DATETIME_FORMAT_BACKTESTING)
|
|
588
|
-
end_date = backtest_end_date.strftime(DATETIME_FORMAT_BACKTESTING)
|
|
589
|
-
directory_name = f"report_{name}_backtest-start-date_" \
|
|
590
|
-
f"{start_date}_backtest-end-date_{end_date}_{created_at}"
|
|
591
|
-
return directory_name
|
|
651
|
+
return most_granular
|