investing-algorithm-framework 3.7.0__py3-none-any.whl → 7.19.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of investing-algorithm-framework might be problematic. Click here for more details.
- investing_algorithm_framework/__init__.py +168 -45
- investing_algorithm_framework/app/__init__.py +32 -1
- investing_algorithm_framework/app/algorithm/__init__.py +7 -0
- investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +1933 -589
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1725 -0
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +27 -0
- investing_algorithm_framework/app/reporting/ascii.py +921 -0
- investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
- investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
- investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
- investing_algorithm_framework/app/reporting/generate.py +185 -0
- investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
- investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
- investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
- investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
- investing_algorithm_framework/app/stateless/action_handlers/__init__.py +4 -2
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +1 -1
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
- investing_algorithm_framework/app/strategy.py +664 -84
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/app/web/__init__.py +2 -1
- investing_algorithm_framework/app/web/create_app.py +4 -2
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +226 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
- investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
- investing_algorithm_framework/cli/initialize_app.py +603 -0
- investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
- investing_algorithm_framework/cli/templates/app.py.template +18 -0
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
- investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
- investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
- investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
- investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
- investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
- investing_algorithm_framework/cli/templates/env.example.template +2 -0
- investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
- investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
- investing_algorithm_framework/cli/templates/readme.md.template +135 -0
- investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
- investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
- investing_algorithm_framework/create_app.py +40 -6
- investing_algorithm_framework/dependency_container.py +72 -56
- investing_algorithm_framework/domain/__init__.py +71 -47
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +59 -91
- investing_algorithm_framework/domain/constants.py +13 -38
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +3 -2
- investing_algorithm_framework/domain/exceptions.py +51 -1
- investing_algorithm_framework/domain/models/__init__.py +17 -12
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/data/data_type.py +46 -0
- investing_algorithm_framework/domain/models/event.py +35 -0
- investing_algorithm_framework/domain/models/market/market_credential.py +55 -1
- investing_algorithm_framework/domain/models/order/order.py +77 -83
- investing_algorithm_framework/domain/models/order/order_status.py +2 -2
- investing_algorithm_framework/domain/models/order/order_type.py +1 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +81 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +26 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +12 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
- investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
- investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
- investing_algorithm_framework/domain/models/time_frame.py +37 -0
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +66 -2
- investing_algorithm_framework/domain/models/trade/__init__.py +8 -1
- investing_algorithm_framework/domain/models/trade/trade.py +295 -171
- investing_algorithm_framework/domain/models/trade/trade_status.py +9 -2
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -0
- investing_algorithm_framework/domain/services/__init__.py +2 -9
- investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +0 -6
- investing_algorithm_framework/domain/services/state_handler.py +38 -0
- investing_algorithm_framework/domain/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +12 -7
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/dates.py +57 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +53 -0
- investing_algorithm_framework/domain/utils/random.py +29 -0
- investing_algorithm_framework/download_data.py +108 -0
- investing_algorithm_framework/infrastructure/__init__.py +31 -18
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
- investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
- investing_algorithm_framework/infrastructure/models/__init__.py +6 -11
- investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -1
- investing_algorithm_framework/infrastructure/models/order/order.py +35 -49
- investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
- investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
- investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +1 -1
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -0
- investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -5
- investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
- investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
- investing_algorithm_framework/infrastructure/repositories/__init__.py +8 -0
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +5 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +1 -1
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +81 -27
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +4 -4
- investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
- investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
- investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
- investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
- investing_algorithm_framework/services/__init__.py +113 -16
- investing_algorithm_framework/services/backtesting/__init__.py +0 -7
- investing_algorithm_framework/services/backtesting/backtest_service.py +566 -359
- investing_algorithm_framework/services/configuration_service.py +77 -11
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/services/market_credential_service.py +16 -1
- investing_algorithm_framework/services/metrics/__init__.py +114 -0
- investing_algorithm_framework/services/metrics/alpha.py +0 -0
- investing_algorithm_framework/services/metrics/beta.py +0 -0
- investing_algorithm_framework/services/metrics/cagr.py +60 -0
- investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
- investing_algorithm_framework/services/metrics/drawdown.py +181 -0
- investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
- investing_algorithm_framework/services/metrics/exposure.py +210 -0
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
- investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
- investing_algorithm_framework/services/metrics/recovery.py +113 -0
- investing_algorithm_framework/services/metrics/returns.py +452 -0
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
- investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
- investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
- investing_algorithm_framework/services/metrics/ulcer.py +0 -0
- investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/services/metrics/win_rate.py +177 -0
- investing_algorithm_framework/services/order_service/__init__.py +3 -1
- investing_algorithm_framework/services/order_service/order_backtest_service.py +76 -89
- investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
- investing_algorithm_framework/services/order_service/order_service.py +407 -326
- investing_algorithm_framework/services/portfolios/__init__.py +3 -1
- investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +37 -3
- investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +22 -8
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
- investing_algorithm_framework/services/portfolios/portfolio_service.py +96 -28
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +97 -28
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +116 -313
- investing_algorithm_framework/services/positions/__init__.py +7 -0
- investing_algorithm_framework/services/positions/position_service.py +210 -0
- investing_algorithm_framework/services/repository_service.py +8 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
- investing_algorithm_framework/services/trade_service/__init__.py +7 -1
- investing_algorithm_framework/services/trade_service/trade_service.py +1013 -315
- investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
- investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
- investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
- investing_algorithm_framework-7.19.15.dist-info/RECORD +263 -0
- investing_algorithm_framework-7.19.15.dist-info/entry_points.txt +3 -0
- investing_algorithm_framework/app/algorithm.py +0 -1105
- investing_algorithm_framework/domain/graphs.py +0 -382
- investing_algorithm_framework/domain/metrics/__init__.py +0 -6
- investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -11
- investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -43
- investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
- investing_algorithm_framework/domain/models/backtesting/backtest_report.py +0 -580
- investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -243
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/services/market_data_sources.py +0 -344
- investing_algorithm_framework/domain/services/market_service.py +0 -153
- investing_algorithm_framework/domain/singleton.py +0 -9
- investing_algorithm_framework/domain/utils/backtesting.py +0 -472
- investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -12
- investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -559
- investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -254
- investing_algorithm_framework/infrastructure/models/market_data_sources/us_treasury_yield.py +0 -47
- investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
- investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -455
- investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
- investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
- investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -350
- investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py +0 -53
- investing_algorithm_framework/services/backtesting/graphs.py +0 -61
- investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -8
- investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -150
- investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -189
- investing_algorithm_framework/services/position_service.py +0 -31
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -264
- investing_algorithm_framework-3.7.0.dist-info/METADATA +0 -339
- investing_algorithm_framework-3.7.0.dist-info/RECORD +0 -147
- /investing_algorithm_framework/{domain → services}/metrics/price_efficiency.py +0 -0
- /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
- {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
- {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
from typing import Dict, List, Union
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
3
10
|
import pandas as pd
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
6
24
|
|
|
7
|
-
|
|
8
|
-
BACKTESTING_INDEX_DATETIME, TimeUnit, BacktestPosition, \
|
|
9
|
-
TradingDataType, OrderStatus, OperationalException, MarketDataSource, \
|
|
10
|
-
OrderSide, SYMBOLS, BacktestDateRange
|
|
11
|
-
from investing_algorithm_framework.services.market_data_source_service import \
|
|
12
|
-
MarketDataSourceService
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
13
26
|
|
|
14
27
|
|
|
15
28
|
class BacktestService:
|
|
@@ -18,192 +31,467 @@ class BacktestService:
|
|
|
18
31
|
"""
|
|
19
32
|
|
|
20
33
|
def __init__(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
self,
|
|
35
|
+
data_provider_service: DataProviderService,
|
|
36
|
+
order_service,
|
|
37
|
+
portfolio_service,
|
|
38
|
+
portfolio_snapshot_service,
|
|
39
|
+
position_repository,
|
|
40
|
+
trade_service,
|
|
41
|
+
configuration_service,
|
|
42
|
+
portfolio_configuration_service,
|
|
28
43
|
):
|
|
29
|
-
|
|
44
|
+
super().__init__()
|
|
30
45
|
self._order_service = order_service
|
|
31
|
-
self.
|
|
32
|
-
self.
|
|
33
|
-
|
|
34
|
-
TradingDataType.TICKER: {}
|
|
35
|
-
}
|
|
36
|
-
self._performance_service = performance_service
|
|
46
|
+
self._trade_service = trade_service
|
|
47
|
+
self._portfolio_service = portfolio_service
|
|
48
|
+
self._portfolio_snapshot_service = portfolio_snapshot_service
|
|
37
49
|
self._position_repository = position_repository
|
|
38
|
-
self._market_data_source_service: MarketDataSourceService \
|
|
39
|
-
= market_data_source_service
|
|
40
|
-
self._backtest_market_data_sources = []
|
|
41
50
|
self._configuration_service = configuration_service
|
|
51
|
+
self._portfolio_configuration_service: PortfolioConfigurationService \
|
|
52
|
+
= portfolio_configuration_service
|
|
53
|
+
self._data_provider_service = data_provider_service
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
def validate_strategy_for_vector_backtest(self, strategy):
|
|
56
|
+
"""
|
|
57
|
+
Validate if the strategy is suitable for backtesting.
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
self._resource_directory = resource_directory
|
|
59
|
+
Args:
|
|
60
|
+
strategy: The strategy to validate.
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
Raises:
|
|
63
|
+
OperationalException: If the strategy does not have the required
|
|
64
|
+
buy/sell signal functions.
|
|
54
65
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
)
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
For a polars DataFrame, the 'DateTime' column will be
|
|
86
|
+
used as the index if it exists.
|
|
63
87
|
|
|
64
|
-
|
|
88
|
+
If no index is found an exception will be raised.
|
|
65
89
|
|
|
66
90
|
Args:
|
|
67
|
-
|
|
68
|
-
|
|
91
|
+
data: The data frame to process.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
OperationalException: If no valid index is found.
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
Returns:
|
|
97
|
+
The index of the data frame.
|
|
98
|
+
"""
|
|
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:
|
|
72
123
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
initial_unallocated = 0
|
|
124
|
+
Vectorized backtest for multiple assets using strategy
|
|
125
|
+
buy/sell signals.
|
|
76
126
|
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
79
140
|
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
)
|
|
82
162
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
portfolio_configuration = portfolio_configurations[0]
|
|
176
|
+
|
|
177
|
+
trading_symbol = portfolio_configurations[0].trading_symbol
|
|
178
|
+
portfolio = Portfolio.from_portfolio_configuration(
|
|
179
|
+
portfolio_configuration
|
|
180
|
+
)
|
|
85
181
|
|
|
86
|
-
|
|
87
|
-
|
|
182
|
+
# Load vectorized backtest data
|
|
183
|
+
data = self._data_provider_service.get_vectorized_backtest_data(
|
|
184
|
+
data_sources=strategy.data_sources,
|
|
88
185
|
start_date=backtest_date_range.start_date,
|
|
89
186
|
end_date=backtest_date_range.end_date
|
|
90
187
|
)
|
|
91
188
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
189
|
+
# Compute signals from strategy
|
|
190
|
+
buy_signals = strategy.generate_buy_signals(data)
|
|
191
|
+
sell_signals = strategy.generate_sell_signals(data)
|
|
192
|
+
|
|
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
|
|
100
199
|
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
106
205
|
)
|
|
107
|
-
return self.create_backtest_report(
|
|
108
|
-
algorithm, len(schedule), backtest_date_range, initial_unallocated
|
|
109
|
-
)
|
|
110
206
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
229
|
+
)
|
|
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
|
+
)
|
|
118
265
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
)
|
|
124
277
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
130
410
|
)
|
|
131
411
|
)
|
|
132
412
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
algorithm.config[BACKTESTING_INDEX_DATETIME] = index_date
|
|
137
|
-
market_data = {}
|
|
138
|
-
|
|
139
|
-
if strategy.strategy_profile.market_data_sources is not None:
|
|
140
|
-
|
|
141
|
-
for data_id in strategy.strategy_profile.market_data_sources:
|
|
413
|
+
unique_symbols = set()
|
|
414
|
+
for trade in trades:
|
|
415
|
+
unique_symbols.add(trade.target_symbol)
|
|
142
416
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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())
|
|
445
|
+
)
|
|
151
446
|
|
|
152
|
-
|
|
153
|
-
|
|
447
|
+
# Create backtest metrics
|
|
448
|
+
run.backtest_metrics = create_backtest_metrics(
|
|
449
|
+
run, risk_free_rate=risk_free_rate
|
|
450
|
+
)
|
|
451
|
+
return run
|
|
154
452
|
|
|
155
453
|
def generate_schedule(
|
|
156
|
-
self,
|
|
157
|
-
|
|
454
|
+
self,
|
|
455
|
+
strategies,
|
|
456
|
+
tasks,
|
|
457
|
+
start_date,
|
|
458
|
+
end_date
|
|
459
|
+
) -> Dict[datetime, Dict[str, List[str]]]:
|
|
158
460
|
"""
|
|
159
|
-
|
|
160
|
-
calculate when the strategies should run based on the given start
|
|
161
|
-
and end date. The schedule will be stored in a pandas DataFrame.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
strategies: The strategies to generate the schedule for
|
|
165
|
-
start_date: The start date of the schedule
|
|
166
|
-
end_date: The end date of the schedule
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
pd.DataFrame: The schedule DataFrame
|
|
461
|
+
Generates a dict-based schedule: datetime => {strategy_ids, task_ids}
|
|
170
462
|
"""
|
|
171
|
-
|
|
463
|
+
schedule = defaultdict(
|
|
464
|
+
lambda: {"strategy_ids": set(), "task_ids": set(tasks)}
|
|
465
|
+
)
|
|
172
466
|
|
|
173
467
|
for strategy in strategies:
|
|
174
|
-
|
|
175
|
-
time_unit = strategy.strategy_profile.time_unit
|
|
468
|
+
strategy_id = strategy.strategy_profile.strategy_id
|
|
176
469
|
interval = strategy.strategy_profile.interval
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
while current_time <= end_date:
|
|
180
|
-
data.append({
|
|
181
|
-
"id": id,
|
|
182
|
-
'run_time': current_time,
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
if TimeUnit.SECOND.equals(time_unit):
|
|
186
|
-
current_time += timedelta(seconds=interval)
|
|
187
|
-
elif TimeUnit.MINUTE.equals(time_unit):
|
|
188
|
-
current_time += timedelta(minutes=interval)
|
|
189
|
-
elif TimeUnit.HOUR.equals(time_unit):
|
|
190
|
-
current_time += timedelta(hours=interval)
|
|
191
|
-
elif TimeUnit.DAY.equals(time_unit):
|
|
192
|
-
current_time += timedelta(days=interval)
|
|
193
|
-
else:
|
|
194
|
-
raise ValueError(f"Unsupported time unit: {time_unit}")
|
|
195
|
-
|
|
196
|
-
schedule_df = pd.DataFrame(data)
|
|
197
|
-
if schedule_df.empty:
|
|
198
|
-
raise OperationalException(
|
|
199
|
-
"Could not generate schedule "
|
|
200
|
-
"for backtest, do you have a strategy "
|
|
201
|
-
"registered for your algorithm?"
|
|
202
|
-
)
|
|
470
|
+
time_unit = strategy.strategy_profile.time_unit
|
|
203
471
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
|
207
495
|
|
|
208
496
|
def get_strategy_from_strategy_profiles(self, strategy_profiles, id):
|
|
209
497
|
|
|
@@ -214,231 +502,150 @@ class BacktestService:
|
|
|
214
502
|
|
|
215
503
|
raise ValueError(f"Strategy profile with id {id} not found.")
|
|
216
504
|
|
|
217
|
-
def
|
|
505
|
+
def _get_initial_unallocated(self) -> float:
|
|
506
|
+
"""
|
|
507
|
+
Get the initial unallocated amount for the backtest.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
float: The initial unallocated amount.
|
|
511
|
+
"""
|
|
512
|
+
portfolios = self._portfolio_service.get_all()
|
|
513
|
+
initial_unallocated = 0.0
|
|
514
|
+
|
|
515
|
+
for portfolio in portfolios:
|
|
516
|
+
initial_unallocated += portfolio.initial_balance
|
|
517
|
+
|
|
518
|
+
return initial_unallocated
|
|
519
|
+
|
|
520
|
+
def create_backtest(
|
|
218
521
|
self,
|
|
219
522
|
algorithm,
|
|
220
523
|
number_of_runs,
|
|
221
524
|
backtest_date_range: BacktestDateRange,
|
|
222
|
-
|
|
223
|
-
|
|
525
|
+
risk_free_rate,
|
|
526
|
+
strategy_directory_path=None
|
|
527
|
+
) -> Backtest:
|
|
224
528
|
"""
|
|
225
|
-
Create a backtest
|
|
226
|
-
will create a backtest report for the given algorithm and return
|
|
227
|
-
the backtest report instance.
|
|
529
|
+
Create a backtest for the given algorithm.
|
|
228
530
|
|
|
229
|
-
It will
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
232
535
|
|
|
233
536
|
Args:
|
|
234
537
|
algorithm: The algorithm to create the backtest report for
|
|
235
538
|
number_of_runs: The number of runs
|
|
236
539
|
backtest_date_range: The backtest date range of the backtest
|
|
237
|
-
|
|
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
|
|
238
543
|
|
|
239
544
|
Returns:
|
|
240
|
-
|
|
545
|
+
Backtest: The backtest containing the results and metrics.
|
|
241
546
|
"""
|
|
242
547
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
# Check if strategy_id is None
|
|
247
|
-
if None in ids:
|
|
248
|
-
# Remove None from ids
|
|
249
|
-
ids = [x for x in ids if x is not None]
|
|
250
|
-
|
|
251
|
-
backtest_report = BacktestReport(
|
|
252
|
-
name=algorithm.name,
|
|
253
|
-
strategy_identifiers=ids,
|
|
254
|
-
backtest_date_range=backtest_date_range,
|
|
255
|
-
initial_unallocated=initial_unallocated,
|
|
256
|
-
trading_symbol=portfolio.trading_symbol,
|
|
257
|
-
created_at=datetime.utcnow(),
|
|
258
|
-
)
|
|
259
|
-
backtest_report.number_of_runs = number_of_runs
|
|
260
|
-
backtest_report.number_of_orders = self._order_service.count({
|
|
261
|
-
"portfolio": portfolio.id
|
|
262
|
-
})
|
|
263
|
-
backtest_report.number_of_positions = \
|
|
264
|
-
self._position_repository.count({
|
|
265
|
-
"portfolio": portfolio.id,
|
|
266
|
-
"amount_gt": 0
|
|
267
|
-
})
|
|
268
|
-
backtest_report.percentage_negative_trades = \
|
|
269
|
-
self._performance_service \
|
|
270
|
-
.get_percentage_negative_trades(portfolio.id)
|
|
271
|
-
backtest_report.percentage_positive_trades = \
|
|
272
|
-
self._performance_service \
|
|
273
|
-
.get_percentage_positive_trades(portfolio.id)
|
|
274
|
-
backtest_report.number_of_trades_closed = \
|
|
275
|
-
self._performance_service \
|
|
276
|
-
.get_number_of_trades_closed(portfolio.id)
|
|
277
|
-
backtest_report.number_of_trades_open = \
|
|
278
|
-
self._performance_service \
|
|
279
|
-
.get_number_of_trades_open(portfolio.id)
|
|
280
|
-
backtest_report.total_cost = portfolio.total_cost
|
|
281
|
-
backtest_report.total_net_gain = portfolio.total_net_gain
|
|
282
|
-
backtest_report.total_net_gain_percentage = \
|
|
283
|
-
self._performance_service \
|
|
284
|
-
.get_total_net_gain_percentage_of_backtest(
|
|
285
|
-
portfolio.id, backtest_report
|
|
286
|
-
)
|
|
287
|
-
positions = self._position_repository.get_all({
|
|
288
|
-
"portfolio": portfolio.id
|
|
289
|
-
})
|
|
290
|
-
orders = self._order_service.get_all({
|
|
291
|
-
"portfolio": portfolio.id
|
|
292
|
-
})
|
|
293
|
-
tickers = {}
|
|
294
|
-
|
|
295
|
-
for position in positions:
|
|
296
|
-
|
|
297
|
-
if position.symbol != portfolio.trading_symbol:
|
|
298
|
-
ticker_symbol = \
|
|
299
|
-
f"{position.symbol}/{portfolio.trading_symbol}"
|
|
300
|
-
|
|
301
|
-
if not self._market_data_source_service\
|
|
302
|
-
.has_ticker_market_data_source(
|
|
303
|
-
symbol=ticker_symbol, market=portfolio.market
|
|
304
|
-
):
|
|
305
|
-
raise OperationalException(
|
|
306
|
-
f"Ticker market data source for "
|
|
307
|
-
f"symbol {ticker_symbol} and market "
|
|
308
|
-
f"{portfolio.market} not found, please make "
|
|
309
|
-
f"sure you register a ticker market data "
|
|
310
|
-
f"source for this symbol and market in "
|
|
311
|
-
f"backtest mode. Otherwise, the backtest "
|
|
312
|
-
f"report cannot be generated."
|
|
313
|
-
)
|
|
314
|
-
tickers[ticker_symbol] = \
|
|
315
|
-
self._market_data_source_service.get_ticker(
|
|
316
|
-
f"{position.symbol}/{portfolio.trading_symbol}",
|
|
317
|
-
market=portfolio.market
|
|
318
|
-
)
|
|
548
|
+
# Get the first portfolio
|
|
549
|
+
portfolio = self._portfolio_service.get_all()[0]
|
|
319
550
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
551
|
+
# List all strategy related files in the strategy directory
|
|
552
|
+
strategy_related_paths = []
|
|
553
|
+
|
|
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"
|
|
327
559
|
)
|
|
328
|
-
backtest_report.total_value = self._performance_service \
|
|
329
|
-
.get_total_value(portfolio.id, tickers, backtest_report)
|
|
330
|
-
backtest_report.average_trade_duration = \
|
|
331
|
-
self._performance_service \
|
|
332
|
-
.get_average_trade_duration(portfolio.id)
|
|
333
|
-
backtest_report.average_trade_size = \
|
|
334
|
-
self._performance_service.get_average_trade_size(portfolio.id)
|
|
335
|
-
positions = self._position_repository.get_all({
|
|
336
|
-
"portfolio": portfolio.id
|
|
337
|
-
})
|
|
338
|
-
backtest_positions = []
|
|
339
|
-
|
|
340
|
-
for position in positions:
|
|
341
|
-
|
|
342
|
-
if position.symbol == portfolio.trading_symbol:
|
|
343
|
-
backtest_position = BacktestPosition(
|
|
344
|
-
position,
|
|
345
|
-
trading_symbol=True,
|
|
346
|
-
total_value_portfolio=backtest_report.total_value
|
|
347
|
-
)
|
|
348
|
-
backtest_position.price = 1
|
|
349
|
-
else:
|
|
350
|
-
pending_buy_orders = self._order_service.get_all({
|
|
351
|
-
"portfolio": portfolio.id,
|
|
352
|
-
"target_symbol": position.symbol,
|
|
353
|
-
"status": OrderStatus.OPEN.value,
|
|
354
|
-
"order_side": OrderSide.BUY.value
|
|
355
|
-
})
|
|
356
|
-
amount_in_pending_buy_orders = 0
|
|
357
|
-
|
|
358
|
-
for order in pending_buy_orders:
|
|
359
|
-
amount_in_pending_buy_orders += order.amount
|
|
360
|
-
|
|
361
|
-
pending_sell_orders = self._order_service.get_all({
|
|
362
|
-
"portfolio": portfolio.id,
|
|
363
|
-
"target_symbol": position.symbol,
|
|
364
|
-
"status": OrderStatus.OPEN.value,
|
|
365
|
-
"order_side": OrderSide.SELL.value
|
|
366
|
-
})
|
|
367
|
-
amount_in_pending_sell_orders = 0
|
|
368
|
-
|
|
369
|
-
for order in pending_sell_orders:
|
|
370
|
-
amount_in_pending_sell_orders += order.amount
|
|
371
|
-
|
|
372
|
-
backtest_position = BacktestPosition(
|
|
373
|
-
position,
|
|
374
|
-
amount_pending_buy=amount_in_pending_buy_orders,
|
|
375
|
-
amount_pending_sell=amount_in_pending_sell_orders,
|
|
376
|
-
total_value_portfolio=backtest_report.total_value
|
|
377
|
-
)
|
|
378
560
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
385
575
|
)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
f"symbol {symbol} and market {market} not found"
|
|
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]
|
|
608
|
+
)
|
|
420
609
|
)
|
|
421
610
|
|
|
422
|
-
|
|
611
|
+
@staticmethod
|
|
612
|
+
def get_most_granular_ohlcv_data_source(data_sources):
|
|
423
613
|
"""
|
|
424
|
-
|
|
614
|
+
Get the most granular data source from a list of data sources.
|
|
425
615
|
|
|
426
|
-
|
|
427
|
-
|
|
616
|
+
Args:
|
|
617
|
+
data_sources: List of data sources.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
The most granular data source.
|
|
428
621
|
"""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
+
}
|
|
634
|
+
|
|
635
|
+
most_granular = None
|
|
636
|
+
highest_granularity = float('inf')
|
|
637
|
+
|
|
638
|
+
ohlcv_data_sources = [
|
|
639
|
+
ds for ds in data_sources if DataType.OHLCV.equals(ds.data_type)
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
if len(ohlcv_data_sources) == 0:
|
|
643
|
+
raise OperationalException("No OHLCV data sources found")
|
|
644
|
+
|
|
645
|
+
for source in ohlcv_data_sources:
|
|
646
|
+
|
|
647
|
+
if granularity_order[source.time_frame] < highest_granularity:
|
|
648
|
+
highest_granularity = granularity_order[source.time_frame]
|
|
649
|
+
most_granular = source
|
|
650
|
+
|
|
651
|
+
return most_granular
|