investing-algorithm-framework 7.19.14__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 +197 -0
- investing_algorithm_framework/app/__init__.py +47 -0
- 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 +2204 -0
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1667 -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/__init__.py +35 -0
- investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
- investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
- investing_algorithm_framework/app/strategy.py +675 -0
- investing_algorithm_framework/app/task.py +41 -0
- investing_algorithm_framework/app/web/__init__.py +5 -0
- investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
- investing_algorithm_framework/app/web/controllers/orders.py +20 -0
- investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
- investing_algorithm_framework/app/web/controllers/positions.py +18 -0
- investing_algorithm_framework/app/web/create_app.py +20 -0
- investing_algorithm_framework/app/web/error_handler.py +59 -0
- investing_algorithm_framework/app/web/responses.py +20 -0
- investing_algorithm_framework/app/web/run_strategies.py +4 -0
- investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
- investing_algorithm_framework/app/web/schemas/order.py +12 -0
- investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
- investing_algorithm_framework/app/web/schemas/position.py +15 -0
- investing_algorithm_framework/app/web/setup_cors.py +6 -0
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +207 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -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 +54 -0
- investing_algorithm_framework/dependency_container.py +155 -0
- investing_algorithm_framework/domain/__init__.py +148 -0
- 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 +435 -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 +111 -0
- investing_algorithm_framework/domain/constants.py +83 -0
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +42 -0
- investing_algorithm_framework/domain/decimal_parsing.py +40 -0
- investing_algorithm_framework/domain/exceptions.py +112 -0
- investing_algorithm_framework/domain/models/__init__.py +43 -0
- investing_algorithm_framework/domain/models/app_mode.py +34 -0
- investing_algorithm_framework/domain/models/base_model.py +25 -0
- 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/__init__.py +5 -0
- investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
- investing_algorithm_framework/domain/models/order/__init__.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +384 -0
- investing_algorithm_framework/domain/models/order/order_side.py +36 -0
- investing_algorithm_framework/domain/models/order/order_status.py +37 -0
- investing_algorithm_framework/domain/models/order/order_type.py +30 -0
- investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
- investing_algorithm_framework/domain/models/position/__init__.py +4 -0
- investing_algorithm_framework/domain/models/position/position.py +68 -0
- investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
- investing_algorithm_framework/domain/models/time_frame.py +153 -0
- investing_algorithm_framework/domain/models/time_interval.py +124 -0
- investing_algorithm_framework/domain/models/time_unit.py +149 -0
- investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
- investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
- investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
- investing_algorithm_framework/domain/models/trade/trade.py +388 -0
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
- investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -0
- investing_algorithm_framework/domain/positions/__init__.py +4 -0
- investing_algorithm_framework/domain/positions/position_size.py +41 -0
- investing_algorithm_framework/domain/services/__init__.py +11 -0
- investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
- investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
- investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
- investing_algorithm_framework/domain/services/rounding_service.py +27 -0
- investing_algorithm_framework/domain/services/state_handler.py +38 -0
- investing_algorithm_framework/domain/stateless_actions.py +7 -0
- investing_algorithm_framework/domain/strategy.py +44 -0
- investing_algorithm_framework/domain/utils/__init__.py +27 -0
- investing_algorithm_framework/domain/utils/csv.py +104 -0
- 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 +41 -0
- investing_algorithm_framework/domain/utils/signatures.py +17 -0
- investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
- investing_algorithm_framework/domain/utils/synchronized.py +12 -0
- investing_algorithm_framework/download_data.py +108 -0
- investing_algorithm_framework/infrastructure/__init__.py +50 -0
- 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 +10 -0
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
- investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
- investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
- investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
- 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 +4 -0
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
- investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
- investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
- investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
- 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 +40 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -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 +21 -0
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
- investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
- 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 +132 -0
- investing_algorithm_framework/services/backtesting/__init__.py +5 -0
- investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
- investing_algorithm_framework/services/configuration_service.py +96 -0
- 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 +40 -0
- 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/price_efficiency.py +57 -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 +9 -0
- investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
- investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
- investing_algorithm_framework/services/order_service/order_service.py +826 -0
- investing_algorithm_framework/services/portfolios/__init__.py +16 -0
- investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
- investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
- investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
- investing_algorithm_framework/services/positions/__init__.py +7 -0
- investing_algorithm_framework/services/positions/position_service.py +210 -0
- investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
- investing_algorithm_framework/services/repository_service.py +40 -0
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
- investing_algorithm_framework/services/trade_service/__init__.py +3 -0
- investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
- investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
- investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
- investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
- investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
- investing_algorithm_framework-7.19.14.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,651 @@
|
|
|
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
|
|
10
|
+
import pandas as pd
|
|
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
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BacktestService:
|
|
29
|
+
"""
|
|
30
|
+
Service that facilitates backtests for algorithm objects.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
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,
|
|
43
|
+
):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self._order_service = order_service
|
|
46
|
+
self._trade_service = trade_service
|
|
47
|
+
self._portfolio_service = portfolio_service
|
|
48
|
+
self._portfolio_snapshot_service = portfolio_snapshot_service
|
|
49
|
+
self._position_repository = position_repository
|
|
50
|
+
self._configuration_service = configuration_service
|
|
51
|
+
self._portfolio_configuration_service: PortfolioConfigurationService \
|
|
52
|
+
= portfolio_configuration_service
|
|
53
|
+
self._data_provider_service = data_provider_service
|
|
54
|
+
|
|
55
|
+
def validate_strategy_for_vector_backtest(self, strategy):
|
|
56
|
+
"""
|
|
57
|
+
Validate if the strategy is suitable for backtesting.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
strategy: The strategy to validate.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
OperationalException: If the strategy does not have the required
|
|
64
|
+
buy/sell signal functions.
|
|
65
|
+
"""
|
|
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
|
+
)
|
|
76
|
+
|
|
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.
|
|
87
|
+
|
|
88
|
+
If no index is found an exception will be raised.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
data: The data frame to process.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
OperationalException: If no valid index is found.
|
|
95
|
+
|
|
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:
|
|
123
|
+
"""
|
|
124
|
+
Vectorized backtest for multiple assets using strategy
|
|
125
|
+
buy/sell signals.
|
|
126
|
+
|
|
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.
|
|
140
|
+
|
|
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
|
+
)
|
|
162
|
+
|
|
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
|
+
)
|
|
181
|
+
|
|
182
|
+
# Load vectorized backtest data
|
|
183
|
+
data = self._data_provider_service.get_vectorized_backtest_data(
|
|
184
|
+
data_sources=strategy.data_sources,
|
|
185
|
+
start_date=backtest_date_range.start_date,
|
|
186
|
+
end_date=backtest_date_range.end_date
|
|
187
|
+
)
|
|
188
|
+
|
|
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
|
|
199
|
+
)
|
|
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
|
|
205
|
+
)
|
|
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
|
|
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
|
+
)
|
|
265
|
+
|
|
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
|
+
)
|
|
277
|
+
|
|
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
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
unique_symbols = set()
|
|
414
|
+
for trade in trades:
|
|
415
|
+
unique_symbols.add(trade.target_symbol)
|
|
416
|
+
|
|
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
|
+
)
|
|
446
|
+
|
|
447
|
+
# Create backtest metrics
|
|
448
|
+
run.backtest_metrics = create_backtest_metrics(
|
|
449
|
+
run, risk_free_rate=risk_free_rate
|
|
450
|
+
)
|
|
451
|
+
return run
|
|
452
|
+
|
|
453
|
+
def generate_schedule(
|
|
454
|
+
self,
|
|
455
|
+
strategies,
|
|
456
|
+
tasks,
|
|
457
|
+
start_date,
|
|
458
|
+
end_date
|
|
459
|
+
) -> Dict[datetime, Dict[str, List[str]]]:
|
|
460
|
+
"""
|
|
461
|
+
Generates a dict-based schedule: datetime => {strategy_ids, task_ids}
|
|
462
|
+
"""
|
|
463
|
+
schedule = defaultdict(
|
|
464
|
+
lambda: {"strategy_ids": set(), "task_ids": set(tasks)}
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
for strategy in strategies:
|
|
468
|
+
strategy_id = strategy.strategy_profile.strategy_id
|
|
469
|
+
interval = strategy.strategy_profile.interval
|
|
470
|
+
time_unit = strategy.strategy_profile.time_unit
|
|
471
|
+
|
|
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
|
+
}
|
|
495
|
+
|
|
496
|
+
def get_strategy_from_strategy_profiles(self, strategy_profiles, id):
|
|
497
|
+
|
|
498
|
+
for strategy_profile in strategy_profiles:
|
|
499
|
+
|
|
500
|
+
if strategy_profile.strategy_id == id:
|
|
501
|
+
return strategy_profile
|
|
502
|
+
|
|
503
|
+
raise ValueError(f"Strategy profile with id {id} not found.")
|
|
504
|
+
|
|
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(
|
|
521
|
+
self,
|
|
522
|
+
algorithm,
|
|
523
|
+
number_of_runs,
|
|
524
|
+
backtest_date_range: BacktestDateRange,
|
|
525
|
+
risk_free_rate,
|
|
526
|
+
strategy_directory_path=None
|
|
527
|
+
) -> Backtest:
|
|
528
|
+
"""
|
|
529
|
+
Create a backtest for the given algorithm.
|
|
530
|
+
|
|
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.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
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
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
Backtest: The backtest containing the results and metrics.
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
# Get the first portfolio
|
|
549
|
+
portfolio = self._portfolio_service.get_all()[0]
|
|
550
|
+
|
|
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"
|
|
559
|
+
)
|
|
560
|
+
|
|
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]
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
@staticmethod
|
|
612
|
+
def get_most_granular_ohlcv_data_source(data_sources):
|
|
613
|
+
"""
|
|
614
|
+
Get the most granular data source from a list of data sources.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
data_sources: List of data sources.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
The most granular data source.
|
|
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
|
+
}
|
|
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
|