investing-algorithm-framework 6.9.1__py3-none-any.whl → 7.19.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of investing-algorithm-framework might be problematic. Click here for more details.
- investing_algorithm_framework/__init__.py +147 -44
- investing_algorithm_framework/app/__init__.py +23 -6
- investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +1322 -707
- investing_algorithm_framework/app/context.py +196 -88
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +16 -5
- investing_algorithm_framework/app/reporting/ascii.py +57 -202
- investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
- investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
- investing_algorithm_framework/app/reporting/generate.py +100 -114
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
- investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
- investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
- investing_algorithm_framework/app/strategy.py +315 -175
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/cli/cli.py +30 -12
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
- investing_algorithm_framework/cli/initialize_app.py +20 -1
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
- investing_algorithm_framework/create_app.py +3 -5
- investing_algorithm_framework/dependency_container.py +25 -39
- investing_algorithm_framework/domain/__init__.py +45 -38
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +27 -0
- investing_algorithm_framework/domain/constants.py +6 -34
- investing_algorithm_framework/domain/data_provider.py +200 -56
- investing_algorithm_framework/domain/exceptions.py +34 -1
- investing_algorithm_framework/domain/models/__init__.py +10 -19
- investing_algorithm_framework/domain/models/base_model.py +0 -6
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
- investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +34 -13
- investing_algorithm_framework/domain/models/order/order_status.py +1 -1
- investing_algorithm_framework/domain/models/order/order_type.py +1 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +9 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
- investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
- investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
- investing_algorithm_framework/domain/models/time_frame.py +7 -0
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +63 -1
- investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
- investing_algorithm_framework/domain/models/trade/trade.py +56 -32
- investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
- investing_algorithm_framework/domain/order_executor.py +19 -0
- investing_algorithm_framework/domain/portfolio_provider.py +20 -1
- investing_algorithm_framework/domain/services/__init__.py +0 -13
- investing_algorithm_framework/domain/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +5 -1
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +17 -14
- investing_algorithm_framework/download_data.py +40 -10
- investing_algorithm_framework/infrastructure/__init__.py +13 -25
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
- investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
- investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
- investing_algorithm_framework/services/__init__.py +105 -8
- investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
- investing_algorithm_framework/services/configuration_service.py +14 -4
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
- investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
- investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
- investing_algorithm_framework/services/order_service/order_service.py +9 -71
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
- investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
- investing_algorithm_framework/services/repository_service.py +5 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
- investing_algorithm_framework/services/trade_service/__init__.py +7 -1
- investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
- investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
- investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
- investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
- investing_algorithm_framework/app/reporting/evaluation.py +0 -243
- investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
- investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
- investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
- investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
- investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
- investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
- investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
- investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
- investing_algorithm_framework/domain/models/data_source.py +0 -21
- investing_algorithm_framework/domain/models/date_range.py +0 -64
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
- investing_algorithm_framework/domain/services/market_service.py +0 -153
- investing_algorithm_framework/domain/services/observable.py +0 -51
- investing_algorithm_framework/domain/services/observer.py +0 -19
- investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
- investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
- investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
- investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
- investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
- investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
- investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
- investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
- investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
- investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
- investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
- investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
- investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
- investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
- /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/entry_points.txt +0 -0
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
|
|
4
|
-
import polars
|
|
5
|
-
from dateutil.parser import parse
|
|
6
|
-
|
|
7
|
-
from investing_algorithm_framework.domain import OHLCVMarketDataSource, \
|
|
8
|
-
BacktestMarketDataSource, OperationalException, TickerMarketDataSource, \
|
|
9
|
-
DATETIME_FORMAT
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class CSVOHLCVMarketDataSource(OHLCVMarketDataSource):
|
|
15
|
-
"""
|
|
16
|
-
Implementation of a OHLCV data source that reads OHLCV data
|
|
17
|
-
from a csv file. Market data source that reads OHLCV data from a csv file.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
def empty(self, end_date):
|
|
21
|
-
data = self.get_data(end_date=end_date, config={})
|
|
22
|
-
return len(data) == 0
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
csv_file_path,
|
|
27
|
-
market=None,
|
|
28
|
-
symbol=None,
|
|
29
|
-
identifier=None,
|
|
30
|
-
window_size=None,
|
|
31
|
-
):
|
|
32
|
-
super().__init__(
|
|
33
|
-
identifier=identifier,
|
|
34
|
-
market=market,
|
|
35
|
-
symbol=symbol,
|
|
36
|
-
time_frame=None,
|
|
37
|
-
window_size=window_size,
|
|
38
|
-
)
|
|
39
|
-
self._csv_file_path = csv_file_path
|
|
40
|
-
self._columns = [
|
|
41
|
-
"Datetime", "Open", "High", "Low", "Close", "Volume"
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
df = polars.read_csv(self._csv_file_path)
|
|
45
|
-
|
|
46
|
-
# Check if all column names are in the csv file
|
|
47
|
-
if not all(column in df.columns for column in self._columns):
|
|
48
|
-
# Identify missing columns
|
|
49
|
-
missing_columns = [column for column in self._columns if
|
|
50
|
-
column not in df.columns]
|
|
51
|
-
raise OperationalException(
|
|
52
|
-
f"Csv file {self._csv_file_path} does not contain "
|
|
53
|
-
f"all required ohlcv columns. "
|
|
54
|
-
f"Missing columns: {missing_columns}"
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
self.data = self._load_data(self.csv_file_path)
|
|
58
|
-
first_row = self.data.head(1)
|
|
59
|
-
last_row = self.data.tail(1)
|
|
60
|
-
self._start_date_data_source = first_row["Datetime"][0]
|
|
61
|
-
self._end_date_data_source = last_row["Datetime"][0]
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def csv_file_path(self):
|
|
65
|
-
return self._csv_file_path
|
|
66
|
-
|
|
67
|
-
def _load_data(self, file_path):
|
|
68
|
-
return polars.read_csv(
|
|
69
|
-
file_path,
|
|
70
|
-
schema_overrides={"Datetime": polars.Datetime},
|
|
71
|
-
low_memory=True
|
|
72
|
-
).with_columns(
|
|
73
|
-
polars.col("Datetime").cast(
|
|
74
|
-
polars.Datetime(time_unit="ms", time_zone="UTC")
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
def get_data(
|
|
79
|
-
self,
|
|
80
|
-
start_date: datetime = None,
|
|
81
|
-
end_date: datetime = None,
|
|
82
|
-
config=None,
|
|
83
|
-
):
|
|
84
|
-
"""
|
|
85
|
-
Get the data from the csv file. The data can be filtered by
|
|
86
|
-
the start_date and end_date in the kwargs. backtest_index_date
|
|
87
|
-
can also be provided to filter the data, where this will be used
|
|
88
|
-
as start_date.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
**kwargs: Keyword arguments that can contain the following:
|
|
92
|
-
start_date (datetime): The start date to filter the data.
|
|
93
|
-
end_date (datetime): The end date to filter the data.
|
|
94
|
-
backtest_index_date (datetime): The backtest index date to
|
|
95
|
-
filter the data. This will be used as start_date.
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
df (polars.DataFrame): The data from the csv file.
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
if start_date is None and end_date is None:
|
|
102
|
-
return self.data
|
|
103
|
-
|
|
104
|
-
if end_date is not None and start_date is not None:
|
|
105
|
-
|
|
106
|
-
if end_date < start_date:
|
|
107
|
-
raise OperationalException(
|
|
108
|
-
f"End date {end_date} is before the start date "
|
|
109
|
-
f"{start_date}"
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
if start_date > self._end_date_data_source:
|
|
113
|
-
return polars.DataFrame()
|
|
114
|
-
|
|
115
|
-
df = self.data
|
|
116
|
-
df = df.filter(
|
|
117
|
-
(df['Datetime'] >= start_date)
|
|
118
|
-
& (df['Datetime'] <= end_date)
|
|
119
|
-
)
|
|
120
|
-
return df
|
|
121
|
-
|
|
122
|
-
if start_date is not None:
|
|
123
|
-
|
|
124
|
-
if start_date < self._start_date_data_source:
|
|
125
|
-
return polars.DataFrame()
|
|
126
|
-
|
|
127
|
-
if start_date > self._end_date_data_source:
|
|
128
|
-
return polars.DataFrame()
|
|
129
|
-
|
|
130
|
-
df = self.data
|
|
131
|
-
df = df.filter(
|
|
132
|
-
(df['Datetime'] >= start_date)
|
|
133
|
-
)
|
|
134
|
-
df = df.head(self.window_size)
|
|
135
|
-
return df
|
|
136
|
-
|
|
137
|
-
if end_date is not None:
|
|
138
|
-
|
|
139
|
-
if end_date < self._start_date_data_source:
|
|
140
|
-
return polars.DataFrame()
|
|
141
|
-
|
|
142
|
-
if end_date > self._end_date_data_source:
|
|
143
|
-
return polars.DataFrame()
|
|
144
|
-
|
|
145
|
-
df = self.data
|
|
146
|
-
df = df.filter(
|
|
147
|
-
(df['Datetime'] <= end_date)
|
|
148
|
-
)
|
|
149
|
-
df = df.tail(self.window_size)
|
|
150
|
-
return df
|
|
151
|
-
|
|
152
|
-
return polars.read_csv(
|
|
153
|
-
self.csv_file_path, columns=self._columns, separator=","
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
def dataframe_to_list_of_lists(self, dataframe, columns):
|
|
157
|
-
# Extract selected columns from DataFrame and convert
|
|
158
|
-
# to a list of lists
|
|
159
|
-
data_list_of_lists = dataframe[columns].values.tolist()
|
|
160
|
-
return data_list_of_lists
|
|
161
|
-
|
|
162
|
-
def to_backtest_market_data_source(self) -> BacktestMarketDataSource:
|
|
163
|
-
pass
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
class CSVTickerMarketDataSource(TickerMarketDataSource):
|
|
167
|
-
|
|
168
|
-
def __init__(
|
|
169
|
-
self,
|
|
170
|
-
identifier,
|
|
171
|
-
market,
|
|
172
|
-
symbol,
|
|
173
|
-
csv_file_path,
|
|
174
|
-
):
|
|
175
|
-
super().__init__(
|
|
176
|
-
identifier=identifier,
|
|
177
|
-
market=market,
|
|
178
|
-
symbol=symbol,
|
|
179
|
-
)
|
|
180
|
-
self._csv_file_path = csv_file_path
|
|
181
|
-
self._columns = [
|
|
182
|
-
"Datetime", "Open", "High", "Low", "Close", "Volume"
|
|
183
|
-
]
|
|
184
|
-
df = polars.read_csv(self._csv_file_path)
|
|
185
|
-
|
|
186
|
-
if not all(column in df.columns for column in self._columns):
|
|
187
|
-
# Identify missing columns
|
|
188
|
-
missing_columns = [column for column in self._columns if
|
|
189
|
-
column not in df.columns]
|
|
190
|
-
raise OperationalException(
|
|
191
|
-
f"Csv file {self._csv_file_path} does not contain "
|
|
192
|
-
f"all required ohlcv columns. "
|
|
193
|
-
f"Missing columns: {missing_columns}"
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
first_row = df.head(1)
|
|
197
|
-
last_row = df.tail(1)
|
|
198
|
-
self._start_date_data_source = parse(first_row["Datetime"][0])
|
|
199
|
-
self._end_date_data_source = parse(last_row["Datetime"][0])
|
|
200
|
-
|
|
201
|
-
@property
|
|
202
|
-
def csv_file_path(self):
|
|
203
|
-
return self._csv_file_path
|
|
204
|
-
|
|
205
|
-
def get_data(
|
|
206
|
-
self,
|
|
207
|
-
start_date: datetime = None,
|
|
208
|
-
end_date: datetime = None,
|
|
209
|
-
config=None,
|
|
210
|
-
):
|
|
211
|
-
|
|
212
|
-
if end_date is None:
|
|
213
|
-
raise OperationalException("Date is required to get ticker data")
|
|
214
|
-
|
|
215
|
-
date = end_date
|
|
216
|
-
|
|
217
|
-
if not isinstance(date, datetime):
|
|
218
|
-
|
|
219
|
-
if isinstance(date, str):
|
|
220
|
-
date = parse(date)
|
|
221
|
-
else:
|
|
222
|
-
raise OperationalException(
|
|
223
|
-
"Date value should be either a string or datetime object"
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
if date < self._start_date_data_source:
|
|
227
|
-
raise OperationalException(
|
|
228
|
-
f"Date {date} is before the start date "
|
|
229
|
-
f"of the data source {self._start_date_data_source}"
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
if date > self._end_date_data_source:
|
|
233
|
-
raise OperationalException(
|
|
234
|
-
f"Date {date} is after the end date "
|
|
235
|
-
f"of the data source {self._end_date_data_source}"
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
# Filter the data based on the backtest index date and the end date
|
|
239
|
-
df = polars.read_csv(self._csv_file_path)
|
|
240
|
-
df = df.filter(
|
|
241
|
-
(df['Datetime'] >= date.strftime(DATETIME_FORMAT))
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
# Check if the dataframe is empty
|
|
245
|
-
if df.shape[0] == 0:
|
|
246
|
-
raise OperationalException(
|
|
247
|
-
f"No ticker data found for {self.symbol} "
|
|
248
|
-
f"at {date.strftime(DATETIME_FORMAT)}"
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
first_row = df.head(1)[0]
|
|
252
|
-
|
|
253
|
-
# Calculate the bid and ask price based on the high and low price
|
|
254
|
-
return {
|
|
255
|
-
"symbol": self.symbol,
|
|
256
|
-
"bid": float((first_row["Low"][0])
|
|
257
|
-
+ float(first_row["High"][0])) / 2,
|
|
258
|
-
"ask": float((first_row["Low"][0])
|
|
259
|
-
+ float(first_row["High"][0])) / 2,
|
|
260
|
-
"datetime": first_row["Datetime"][0],
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
def dataframe_to_list_of_lists(self, dataframe, columns):
|
|
264
|
-
# Extract selected columns from DataFrame and convert
|
|
265
|
-
# to a list of lists
|
|
266
|
-
data_list_of_lists = dataframe[columns].values.tolist()
|
|
267
|
-
return data_list_of_lists
|
|
268
|
-
|
|
269
|
-
def to_backtest_market_data_source(self) -> BacktestMarketDataSource:
|
|
270
|
-
pass
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timedelta, timezone
|
|
2
|
-
|
|
3
|
-
from dateutil import parser
|
|
4
|
-
from pandas import DataFrame
|
|
5
|
-
import polars as pl
|
|
6
|
-
|
|
7
|
-
from investing_algorithm_framework.domain import OHLCVMarketDataSource, \
|
|
8
|
-
BacktestMarketDataSource, OperationalException, TimeFrame, sync_timezones
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class PandasOHLCVBacktestMarketDataSource(
|
|
12
|
-
OHLCVMarketDataSource, BacktestMarketDataSource
|
|
13
|
-
):
|
|
14
|
-
"""
|
|
15
|
-
PandasOHLCVBacktestMarketDataSource implementation
|
|
16
|
-
of a OHLCVMarketDataSource. This implementation uses a pandas
|
|
17
|
-
dataframe to provide data to the strategy.
|
|
18
|
-
"""
|
|
19
|
-
backtest_data_directory = None
|
|
20
|
-
backtest_data_end_date = None
|
|
21
|
-
total_minutes_time_frame = None
|
|
22
|
-
column_names = ["Datetime", "Open", "High", "Low", "Close", "Volume"]
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
identifier,
|
|
27
|
-
market,
|
|
28
|
-
symbol,
|
|
29
|
-
time_frame,
|
|
30
|
-
dataframe=None,
|
|
31
|
-
window_size=None,
|
|
32
|
-
):
|
|
33
|
-
super().__init__(
|
|
34
|
-
identifier=identifier,
|
|
35
|
-
market=market,
|
|
36
|
-
symbol=symbol,
|
|
37
|
-
time_frame=time_frame,
|
|
38
|
-
window_size=window_size,
|
|
39
|
-
)
|
|
40
|
-
self.dataframe = dataframe
|
|
41
|
-
self._start_date_data_source = None
|
|
42
|
-
self._end_date_data_source = None
|
|
43
|
-
self.backtest_end_index = self.window_size
|
|
44
|
-
self.backtest_start_index = 0
|
|
45
|
-
self.window_cache = {}
|
|
46
|
-
|
|
47
|
-
def prepare_data(
|
|
48
|
-
self,
|
|
49
|
-
config,
|
|
50
|
-
backtest_start_date,
|
|
51
|
-
backtest_end_date,
|
|
52
|
-
):
|
|
53
|
-
"""
|
|
54
|
-
Prepare data implementation of ccxt based ohlcv backtest market
|
|
55
|
-
data source
|
|
56
|
-
|
|
57
|
-
This implementation will check if the data source already exists before
|
|
58
|
-
pulling all the data. This optimization will prevent downloading
|
|
59
|
-
of unnecessary resources.
|
|
60
|
-
|
|
61
|
-
When downloading the data it will use the ccxt library.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
config (dict): the configuration of the data source
|
|
65
|
-
backtest_start_date (datetime): the start date of the backtest
|
|
66
|
-
backtest_end_date (datetime): the end date of the backtest
|
|
67
|
-
time_frame (string): the time frame of the data
|
|
68
|
-
window_size (int): the total amount of candle sticks that need to
|
|
69
|
-
be returned
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
None
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
if config is None:
|
|
76
|
-
config = self.config
|
|
77
|
-
|
|
78
|
-
# Calculating the backtest data start date
|
|
79
|
-
backtest_data_start_date = \
|
|
80
|
-
backtest_start_date - timedelta(
|
|
81
|
-
minutes=(
|
|
82
|
-
(self.window_size + 1) *
|
|
83
|
-
TimeFrame.from_value(self.time_frame).amount_of_minutes
|
|
84
|
-
)
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
self.backtest_data_start_date = backtest_data_start_date \
|
|
88
|
-
.replace(microsecond=0)
|
|
89
|
-
self.backtest_data_end_date = backtest_end_date.replace(microsecond=0)
|
|
90
|
-
|
|
91
|
-
if not isinstance(self.dataframe, DataFrame):
|
|
92
|
-
raise OperationalException(
|
|
93
|
-
"Provided dataframe is not a pandas dataframe"
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
if not set(self.column_names).issubset(self.dataframe.columns):
|
|
97
|
-
raise OperationalException(
|
|
98
|
-
"Provided dataframe does not have all required columns. "
|
|
99
|
-
"Your pandas dataframe should have the following columns: "
|
|
100
|
-
"Datetime, Open, High, Low, Close, Volume"
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
# Get first and last row and check if backtest start and end dates
|
|
104
|
-
# are within the dataframe
|
|
105
|
-
first_row = self.dataframe.head(1)
|
|
106
|
-
last_row = self.dataframe.tail(1)
|
|
107
|
-
|
|
108
|
-
if backtest_end_date > last_row["Datetime"].iloc[0]:
|
|
109
|
-
raise OperationalException(
|
|
110
|
-
f"Backtest end date {backtest_end_date} is "
|
|
111
|
-
f"after the end date of the data source "
|
|
112
|
-
f"{last_row['Datetime'].iloc[0]}"
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
if backtest_data_start_date < first_row["Datetime"].iloc[0]:
|
|
116
|
-
raise OperationalException(
|
|
117
|
-
f"Backtest start date {backtest_data_start_date} is "
|
|
118
|
-
f"before the start date of the data source "
|
|
119
|
-
f"{first_row['Datetime'][0]}"
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
self._precompute_sliding_windows() # Precompute sliding windows!
|
|
123
|
-
|
|
124
|
-
def _precompute_sliding_windows(self):
|
|
125
|
-
"""
|
|
126
|
-
Precompute all sliding windows for fast retrieval.
|
|
127
|
-
|
|
128
|
-
A sliding window is calculated as a subset of the data. It will
|
|
129
|
-
take for each timestamp in the data a window of size `window_size`
|
|
130
|
-
and stores it in a cache with the last timestamp of the window.
|
|
131
|
-
"""
|
|
132
|
-
self.window_cache = {}
|
|
133
|
-
timestamps = self.dataframe["Datetime"].to_list()
|
|
134
|
-
|
|
135
|
-
for i in range(len(timestamps) - self.window_size + 1):
|
|
136
|
-
# Use last timestamp as key
|
|
137
|
-
end_time = timestamps[i + self.window_size - 1]
|
|
138
|
-
|
|
139
|
-
# Convert end_time datetime object to UTC
|
|
140
|
-
if isinstance(end_time, str):
|
|
141
|
-
end_time = parser.parse(end_time)
|
|
142
|
-
elif isinstance(end_time, datetime):
|
|
143
|
-
end_time = end_time
|
|
144
|
-
|
|
145
|
-
self.window_cache[end_time] = \
|
|
146
|
-
self.dataframe.iloc[i:i + self.window_size]
|
|
147
|
-
|
|
148
|
-
def get_data(
|
|
149
|
-
self,
|
|
150
|
-
date,
|
|
151
|
-
config=None,
|
|
152
|
-
):
|
|
153
|
-
"""
|
|
154
|
-
Get data implementation of ccxt based ohlcv backtest market data
|
|
155
|
-
source. This implementation will use polars to load and filter the
|
|
156
|
-
data.
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
data = self.window_cache.get(date)
|
|
160
|
-
|
|
161
|
-
if data is not None:
|
|
162
|
-
data = pl.from_pandas(data)
|
|
163
|
-
return data
|
|
164
|
-
|
|
165
|
-
# Find closest previous timestamp
|
|
166
|
-
sorted_timestamps = sorted(self.window_cache.keys())
|
|
167
|
-
|
|
168
|
-
closest_date = None
|
|
169
|
-
for ts in reversed(sorted_timestamps):
|
|
170
|
-
date = sync_timezones(ts, date)
|
|
171
|
-
|
|
172
|
-
if ts <= date:
|
|
173
|
-
closest_date = ts
|
|
174
|
-
break
|
|
175
|
-
|
|
176
|
-
data = self.window_cache.get(closest_date) if closest_date else None
|
|
177
|
-
|
|
178
|
-
if data is not None:
|
|
179
|
-
data = pl.from_pandas(data)
|
|
180
|
-
|
|
181
|
-
return data
|
|
182
|
-
|
|
183
|
-
def to_backtest_market_data_source(self) -> BacktestMarketDataSource:
|
|
184
|
-
# Ignore this method for now
|
|
185
|
-
pass
|
|
186
|
-
|
|
187
|
-
def empty(self):
|
|
188
|
-
return False
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
class PandasOHLCVMarketDataSource(OHLCVMarketDataSource):
|
|
192
|
-
"""
|
|
193
|
-
PandasOHLCVMarketDataSource implementation of a OHLCVMarketDataSource
|
|
194
|
-
using a pandas dataframe to provide data to the strategy.
|
|
195
|
-
|
|
196
|
-
"""
|
|
197
|
-
|
|
198
|
-
"""
|
|
199
|
-
PandasOHLCVBacktestMarketDataSource implementation
|
|
200
|
-
of a OHLCVMarketDataSource. This implementation uses a pandas
|
|
201
|
-
dataframe to provide data to the strategy.
|
|
202
|
-
"""
|
|
203
|
-
backtest_data_directory = None
|
|
204
|
-
backtest_data_end_date = None
|
|
205
|
-
total_minutes_time_frame = None
|
|
206
|
-
column_names = ["Datetime", "Open", "High", "Low", "Close", "Volume"]
|
|
207
|
-
|
|
208
|
-
def __init__(
|
|
209
|
-
self,
|
|
210
|
-
identifier,
|
|
211
|
-
market,
|
|
212
|
-
symbol,
|
|
213
|
-
time_frame,
|
|
214
|
-
dataframe=None,
|
|
215
|
-
window_size=None,
|
|
216
|
-
):
|
|
217
|
-
super().__init__(
|
|
218
|
-
identifier=identifier,
|
|
219
|
-
market=market,
|
|
220
|
-
symbol=symbol,
|
|
221
|
-
time_frame=time_frame,
|
|
222
|
-
window_size=window_size,
|
|
223
|
-
)
|
|
224
|
-
self.dataframe = dataframe
|
|
225
|
-
self._start_date_data_source = None
|
|
226
|
-
self._end_date_data_source = None
|
|
227
|
-
self.backtest_end_index = self.window_size
|
|
228
|
-
self.backtest_start_index = 0
|
|
229
|
-
|
|
230
|
-
def get_data(
|
|
231
|
-
self,
|
|
232
|
-
start_date: datetime = None,
|
|
233
|
-
end_date: datetime = None,
|
|
234
|
-
config=None,
|
|
235
|
-
):
|
|
236
|
-
"""
|
|
237
|
-
Implementation of get_data for CCXTOHLCVMarketDataSource.
|
|
238
|
-
This implementation uses the CCXTMarketService to get the OHLCV data.
|
|
239
|
-
|
|
240
|
-
Args:
|
|
241
|
-
start_date: datetime (optional) - the start date of the data. The
|
|
242
|
-
first candle stick should close to this date.
|
|
243
|
-
end_date: datetime (optional) - the end date of the data. The last
|
|
244
|
-
candle stick should close to this date.
|
|
245
|
-
storage_path: string (optional) - the storage path specifies the
|
|
246
|
-
directory where the data is written to or read from.
|
|
247
|
-
If set the data provider will write all its downloaded data
|
|
248
|
-
to this location. Also, it will check if the
|
|
249
|
-
data already exists at the storage location. If this is the
|
|
250
|
-
case it will return this.
|
|
251
|
-
|
|
252
|
-
Returns
|
|
253
|
-
polars.DataFrame with the OHLCV data
|
|
254
|
-
"""
|
|
255
|
-
|
|
256
|
-
# Calculate the start and end dates
|
|
257
|
-
if start_date is None or end_date is None:
|
|
258
|
-
|
|
259
|
-
if start_date is None:
|
|
260
|
-
|
|
261
|
-
if end_date is None:
|
|
262
|
-
end_date = datetime.now(tz=timezone.utc)
|
|
263
|
-
|
|
264
|
-
if self.window_size is None:
|
|
265
|
-
raise OperationalException(
|
|
266
|
-
"Window_size should be defined before the " +
|
|
267
|
-
"get_data method can be called. Make sure to set " +
|
|
268
|
-
"the window_size attribute on your " +
|
|
269
|
-
"CCXTOHLCVMarketDataSource or provide a start_date " +
|
|
270
|
-
"and end_date to the get_data method."
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
start_date = self.create_start_date(
|
|
274
|
-
end_date=end_date,
|
|
275
|
-
time_frame=self.time_frame,
|
|
276
|
-
window_size=self.window_size
|
|
277
|
-
)
|
|
278
|
-
else:
|
|
279
|
-
end_date = self.create_end_date(
|
|
280
|
-
start_date=start_date,
|
|
281
|
-
time_frame=self.time_frame,
|
|
282
|
-
window_size=self.window_size
|
|
283
|
-
)
|
|
284
|
-
# Return the dataframe sliced by the start and end date
|
|
285
|
-
start_date = sync_timezones(
|
|
286
|
-
self.dataframe["Datetime"].dt.tz, start_date
|
|
287
|
-
)
|
|
288
|
-
end_date = sync_timezones(
|
|
289
|
-
self.dataframe["Datetime"].dt.tz, end_date
|
|
290
|
-
)
|
|
291
|
-
data = self.dataframe[
|
|
292
|
-
(self.dataframe["Datetime"] >= start_date) &
|
|
293
|
-
(self.dataframe["Datetime"] <= end_date)
|
|
294
|
-
]
|
|
295
|
-
|
|
296
|
-
if data is not None:
|
|
297
|
-
data = pl.from_pandas(data)
|
|
298
|
-
|
|
299
|
-
return data
|
|
300
|
-
|
|
301
|
-
def to_backtest_market_data_source(self) -> BacktestMarketDataSource:
|
|
302
|
-
return PandasOHLCVBacktestMarketDataSource(
|
|
303
|
-
identifier=self.identifier,
|
|
304
|
-
market=self.market,
|
|
305
|
-
symbol=self.symbol,
|
|
306
|
-
time_frame=self.time_frame,
|
|
307
|
-
dataframe=self.dataframe,
|
|
308
|
-
window_size=self.window_size
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
def empty(self):
|
|
312
|
-
return False
|