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,1143 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os.path
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from time import sleep
|
|
5
|
+
from typing import Union, List
|
|
6
|
+
|
|
7
|
+
import ccxt
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import polars as pl
|
|
10
|
+
from dateutil import parser
|
|
11
|
+
|
|
12
|
+
from investing_algorithm_framework.domain import OperationalException, \
|
|
13
|
+
DATETIME_FORMAT, DataProvider, convert_polars_to_pandas, \
|
|
14
|
+
NetworkError, TimeFrame, MarketCredential, DataType, DataSource, \
|
|
15
|
+
RESOURCE_DIRECTORY, CCXT_DATETIME_FORMAT, DATA_DIRECTORY, \
|
|
16
|
+
DATETIME_FORMAT_FILE_NAME
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("investing_algorithm_framework")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CCXTOHLCVDataProvider(DataProvider):
|
|
22
|
+
"""
|
|
23
|
+
Implementation of Data Provider for OHLCV data. OHLCV data
|
|
24
|
+
will be downloaded with the CCXT library.
|
|
25
|
+
|
|
26
|
+
If in backtest mode, and the data is already
|
|
27
|
+
available in the storage path, it will be loaded from there. If the
|
|
28
|
+
data is not available in the storage path, it will be fetched from the
|
|
29
|
+
CCXT library and saved to the storage path in csv format.
|
|
30
|
+
|
|
31
|
+
If the get_data method is called with a start and end date, the
|
|
32
|
+
data provider will look if the data is already available in the
|
|
33
|
+
storage directory. If this is the case, it will read the data
|
|
34
|
+
from the csv file and return it.
|
|
35
|
+
|
|
36
|
+
The CSV file should contain the following
|
|
37
|
+
columns: Datetime, Open, High, Low, Close, Volume.
|
|
38
|
+
The Datetime column should be in UTC timezone and in milliseconds.
|
|
39
|
+
The data will be loaded into a Polars DataFrame and will be kept in memory.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
data_type (DataType): The type of data provided by this provider,
|
|
43
|
+
which is OHLCV.
|
|
44
|
+
data_provider_identifier (str): Identifier for the CSV OHLCV data
|
|
45
|
+
provider.
|
|
46
|
+
_start_date_data_source (datetime): The start date of the data
|
|
47
|
+
source, determined from the first row of the data.
|
|
48
|
+
_end_date_data_source (datetime): The end date of the data
|
|
49
|
+
source, determined from the last row of the data.
|
|
50
|
+
data (polars.DataFrame): The OHLCV data loaded from the CSV file when
|
|
51
|
+
in backtest mode.
|
|
52
|
+
"""
|
|
53
|
+
data_type = DataType.OHLCV
|
|
54
|
+
data_provider_identifier = "ccxt_ohlcv_data_provider"
|
|
55
|
+
storage_directory = None
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
symbol: str = None,
|
|
60
|
+
time_frame: str = None,
|
|
61
|
+
market: str = None,
|
|
62
|
+
window_size=None,
|
|
63
|
+
data_provider_identifier: str = None,
|
|
64
|
+
storage_directory=None,
|
|
65
|
+
pandas: bool = False,
|
|
66
|
+
config=None
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Initialize the CCXT OHLCV Data Provider.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
symbol (str): The symbol for which the data is provided.
|
|
73
|
+
time_frame (str): The time frame for the data.
|
|
74
|
+
market (str, optional): The market for the data. Defaults to None.
|
|
75
|
+
window_size (int, optional): The window size for the data.
|
|
76
|
+
Defaults to None.
|
|
77
|
+
data_provider_identifier (str, optional): The identifier for the
|
|
78
|
+
data provider.
|
|
79
|
+
pandas (bool, optional): If True, the data will be returned
|
|
80
|
+
as a pandas DataFrame instead of a Polars DataFrame.
|
|
81
|
+
storage_directory: (str, optional): the storage directory where
|
|
82
|
+
the OHLCV data need to be stored.
|
|
83
|
+
"""
|
|
84
|
+
if data_provider_identifier is None:
|
|
85
|
+
data_provider_identifier = self.data_provider_identifier
|
|
86
|
+
|
|
87
|
+
super().__init__(
|
|
88
|
+
symbol=symbol,
|
|
89
|
+
market=market,
|
|
90
|
+
time_frame=time_frame,
|
|
91
|
+
window_size=window_size,
|
|
92
|
+
storage_directory=storage_directory,
|
|
93
|
+
data_provider_identifier=data_provider_identifier,
|
|
94
|
+
config=config
|
|
95
|
+
)
|
|
96
|
+
self._start_date_data_source = None
|
|
97
|
+
self._end_date_data_source = None
|
|
98
|
+
self._columns = ["Datetime", "Open", "High", "Low", "Close", "Volume"]
|
|
99
|
+
self.pandas = pandas
|
|
100
|
+
self.window_cache = {}
|
|
101
|
+
self.data = None
|
|
102
|
+
self.total_number_of_data_points = 0
|
|
103
|
+
self.missing_data_point_dates = []
|
|
104
|
+
self.data_file_path = None
|
|
105
|
+
|
|
106
|
+
def has_data(
|
|
107
|
+
self,
|
|
108
|
+
data_source: DataSource,
|
|
109
|
+
start_date: datetime = None,
|
|
110
|
+
end_date: datetime = None
|
|
111
|
+
) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Implementation of the has_data method to check if
|
|
114
|
+
the data provider has data for the given data source.
|
|
115
|
+
|
|
116
|
+
If start_date and/or end_date are provided, first the
|
|
117
|
+
storage_directory_will be checked for existence of the data.
|
|
118
|
+
|
|
119
|
+
If nothing is found or start_date and/or end_date are not provided
|
|
120
|
+
the ccxt library will be directly queried.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
data_source (DataSource): The data source to check.
|
|
124
|
+
start_date (datetime, optional): The start date for the data.
|
|
125
|
+
Defaults to None.
|
|
126
|
+
end_date (datetime, optional): The end date for the data.
|
|
127
|
+
Defaults to None.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
bool: True if the data provider has data for the given data source,
|
|
131
|
+
False otherwise.
|
|
132
|
+
"""
|
|
133
|
+
market = data_source.market
|
|
134
|
+
symbol = data_source.symbol
|
|
135
|
+
data_type = data_source.data_type
|
|
136
|
+
start_date = start_date or data_source.start_date
|
|
137
|
+
end_date = end_date or data_source.end_date
|
|
138
|
+
|
|
139
|
+
if not DataType.OHLCV.equals(data_type):
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
if start_date is not None and end_date is not None:
|
|
143
|
+
# Check if the data is available in the storage path
|
|
144
|
+
data = self._get_data_from_storage(
|
|
145
|
+
symbol=symbol,
|
|
146
|
+
market=market,
|
|
147
|
+
time_frame=data_source.time_frame,
|
|
148
|
+
storage_path=data_source.storage_path,
|
|
149
|
+
start_date=start_date,
|
|
150
|
+
end_date=end_date
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if data is not None:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
if market is None:
|
|
157
|
+
market = "binance"
|
|
158
|
+
|
|
159
|
+
# Check if ccxt has an exchange for the given market
|
|
160
|
+
try:
|
|
161
|
+
market = market.lower()
|
|
162
|
+
exchange_class = getattr(ccxt, market)
|
|
163
|
+
exchange = exchange_class()
|
|
164
|
+
symbols = exchange.load_markets()
|
|
165
|
+
symbols = list(symbols.keys())
|
|
166
|
+
return symbol in symbols
|
|
167
|
+
|
|
168
|
+
except ccxt.NetworkError:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(e)
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def prepare_backtest_data(
|
|
176
|
+
self,
|
|
177
|
+
backtest_start_date,
|
|
178
|
+
backtest_end_date,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Prepares backtest data for a given symbol and date range.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
backtest_start_date (datetime): The start date for the
|
|
185
|
+
backtest data.
|
|
186
|
+
backtest_end_date (datetime): The end date for the
|
|
187
|
+
backtest data.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
OperationalException: If the backtest start date is before the
|
|
191
|
+
start date of the data source or if the backtest end date is
|
|
192
|
+
after the end date of the data source.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
None
|
|
196
|
+
"""
|
|
197
|
+
# There must be at least backtest_start_date - window_size * time_frame
|
|
198
|
+
# data available to create a sliding window.
|
|
199
|
+
if self.window_size is not None:
|
|
200
|
+
required_start_date = backtest_start_date - \
|
|
201
|
+
timedelta(
|
|
202
|
+
minutes=TimeFrame.from_value(
|
|
203
|
+
self.time_frame
|
|
204
|
+
).amount_of_minutes * self.window_size
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
required_start_date = backtest_start_date
|
|
208
|
+
|
|
209
|
+
storage_directory_path = self.get_storage_directory()
|
|
210
|
+
|
|
211
|
+
# Check if the data source is already available in the storage path
|
|
212
|
+
data = self._get_data_from_storage(
|
|
213
|
+
symbol=self.symbol,
|
|
214
|
+
market=self.market,
|
|
215
|
+
time_frame=self.time_frame,
|
|
216
|
+
storage_path=storage_directory_path,
|
|
217
|
+
start_date=required_start_date,
|
|
218
|
+
end_date=backtest_end_date
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if data is None:
|
|
222
|
+
# Disable pandas if it is set to True, because logic
|
|
223
|
+
# depends on polars DataFrame
|
|
224
|
+
has_pandas_flag = self.pandas
|
|
225
|
+
self.pandas = False
|
|
226
|
+
|
|
227
|
+
# If the data is not available in the storage path,
|
|
228
|
+
# retrieve it from the CCXT data provider
|
|
229
|
+
data = self.get_data(
|
|
230
|
+
start_date=required_start_date,
|
|
231
|
+
end_date=backtest_end_date,
|
|
232
|
+
save=True,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
self.pandas = has_pandas_flag
|
|
236
|
+
|
|
237
|
+
self.data = data
|
|
238
|
+
self._start_date_data_source = self.data["Datetime"].min()
|
|
239
|
+
self._end_date_data_source = self.data["Datetime"].max()
|
|
240
|
+
self.total_number_of_data_points = len(self.data)
|
|
241
|
+
|
|
242
|
+
if required_start_date < self._start_date_data_source:
|
|
243
|
+
self.number_of_missing_data_points = (
|
|
244
|
+
self._start_date_data_source - required_start_date
|
|
245
|
+
).total_seconds() / (
|
|
246
|
+
TimeFrame.from_value(self.time_frame).amount_of_minutes * 60
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if self.window_size is not None:
|
|
250
|
+
# Create cache with sliding windows
|
|
251
|
+
self._precompute_sliding_windows(
|
|
252
|
+
data=data,
|
|
253
|
+
window_size=self.window_size,
|
|
254
|
+
time_frame=self.time_frame,
|
|
255
|
+
start_date=backtest_start_date,
|
|
256
|
+
end_date=backtest_end_date
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
n_min = TimeFrame.from_value(self.time_frame).amount_of_minutes
|
|
260
|
+
# Assume self.data is a Polars DataFrame with a "Datetime" column
|
|
261
|
+
expected_dates = pl.datetime_range(
|
|
262
|
+
start=required_start_date,
|
|
263
|
+
end=backtest_end_date,
|
|
264
|
+
interval=f"{n_min}m",
|
|
265
|
+
eager=True
|
|
266
|
+
).to_list()
|
|
267
|
+
|
|
268
|
+
actual_dates = self.data["Datetime"].to_list()
|
|
269
|
+
|
|
270
|
+
# Find missing dates
|
|
271
|
+
self.missing_data_point_dates = sorted(
|
|
272
|
+
set(expected_dates) - set(actual_dates)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def get_data(
|
|
276
|
+
self,
|
|
277
|
+
date: datetime = None,
|
|
278
|
+
start_date: datetime = None,
|
|
279
|
+
end_date: datetime = None,
|
|
280
|
+
save: bool = False,
|
|
281
|
+
) -> Union[pl.DataFrame, pd.DataFrame]:
|
|
282
|
+
"""
|
|
283
|
+
Function to retrieve data from the CCXT data provider.
|
|
284
|
+
This function retrieves OHLCV data for a given symbol, time frame,
|
|
285
|
+
and market. It uses the CCXT library to fetch the data and returns
|
|
286
|
+
it in a polars DataFrame format. If pandas is set to True, it
|
|
287
|
+
converts the polars DataFrame to a pandas DataFrame.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
date (datetime, optional): The date for which to retrieve the data.
|
|
291
|
+
start_date (datetime): The start date for the data.
|
|
292
|
+
end_date (datetime): The end date for the data.
|
|
293
|
+
save (bool): If True, the data will be saved to the storage path
|
|
294
|
+
if it is not already available. Defaults to False.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
DataFrame: The data for the given symbol and market.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
if self.market is None:
|
|
301
|
+
raise OperationalException(
|
|
302
|
+
"Market is not set. Please set the market "
|
|
303
|
+
"before calling get_data."
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if self.symbol is None:
|
|
307
|
+
raise OperationalException(
|
|
308
|
+
"Symbol is not set. Please set the symbol "
|
|
309
|
+
"before calling get_data."
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if self.time_frame is None:
|
|
313
|
+
raise OperationalException(
|
|
314
|
+
"Time frame is not set. Please set the time frame "
|
|
315
|
+
"before requesting ohlcv data."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if date is not None and self.window_size is not None \
|
|
319
|
+
and self.time_frame is not None:
|
|
320
|
+
start_date = self.create_start_date(
|
|
321
|
+
end_date=date,
|
|
322
|
+
time_frame=self.time_frame,
|
|
323
|
+
window_size=self.window_size
|
|
324
|
+
)
|
|
325
|
+
end_date = date
|
|
326
|
+
else:
|
|
327
|
+
if (end_date is None and start_date is None
|
|
328
|
+
and self.window_size is None):
|
|
329
|
+
raise OperationalException(
|
|
330
|
+
"A start date or end date or window size is required "
|
|
331
|
+
"to retrieve ohlcv data."
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if (start_date is not None and end_date is None
|
|
335
|
+
and self.window_size is None):
|
|
336
|
+
end_date = datetime.now(tz=timezone.utc)
|
|
337
|
+
|
|
338
|
+
if (end_date is not None and start_date is None
|
|
339
|
+
and self.window_size is None):
|
|
340
|
+
raise OperationalException(
|
|
341
|
+
"A window size is required when using an end date "
|
|
342
|
+
"to retrieve ohlcv data."
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if start_date is not None and end_date is None:
|
|
346
|
+
end_date = self.create_end_date(
|
|
347
|
+
start_date=start_date,
|
|
348
|
+
time_frame=self.time_frame,
|
|
349
|
+
window_size=self.window_size
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if end_date is not None and start_date is None \
|
|
353
|
+
and self.window_size is not None:
|
|
354
|
+
start_date = self.create_start_date(
|
|
355
|
+
end_date=end_date,
|
|
356
|
+
time_frame=self.time_frame,
|
|
357
|
+
window_size=self.window_size
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if start_date is None and end_date is None:
|
|
361
|
+
end_date = datetime.now(tz=timezone.utc)
|
|
362
|
+
start_date = self.create_start_date(
|
|
363
|
+
end_date=end_date,
|
|
364
|
+
time_frame=self.time_frame,
|
|
365
|
+
window_size=self.window_size
|
|
366
|
+
)
|
|
367
|
+
data = self._get_data_from_storage(
|
|
368
|
+
symbol=self.symbol,
|
|
369
|
+
market=self.market,
|
|
370
|
+
time_frame=self.time_frame,
|
|
371
|
+
storage_path=self.get_storage_directory(),
|
|
372
|
+
start_date=start_date,
|
|
373
|
+
end_date=end_date
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
if data is None:
|
|
377
|
+
data = self.get_ohlcv(
|
|
378
|
+
symbol=self.symbol,
|
|
379
|
+
time_frame=self.time_frame,
|
|
380
|
+
from_timestamp=start_date,
|
|
381
|
+
market=self.market,
|
|
382
|
+
to_timestamp=end_date
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if save:
|
|
386
|
+
storage_directory = self.get_storage_directory()
|
|
387
|
+
|
|
388
|
+
if storage_directory is None:
|
|
389
|
+
raise OperationalException(
|
|
390
|
+
"Storage directory is not set for "
|
|
391
|
+
"the CCXTOHLCVDataProvider. Make sure to set the "
|
|
392
|
+
"storage directory in the configuration or "
|
|
393
|
+
"in the constructor."
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
self.save_data_to_storage(
|
|
397
|
+
symbol=self.symbol,
|
|
398
|
+
market=self.market,
|
|
399
|
+
time_frame=self.time_frame,
|
|
400
|
+
start_date=start_date,
|
|
401
|
+
end_date=end_date,
|
|
402
|
+
data=data,
|
|
403
|
+
storage_directory_path=storage_directory
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if self.pandas:
|
|
407
|
+
data = convert_polars_to_pandas(data)
|
|
408
|
+
|
|
409
|
+
return data
|
|
410
|
+
|
|
411
|
+
def get_backtest_data(
|
|
412
|
+
self,
|
|
413
|
+
backtest_index_date: datetime,
|
|
414
|
+
backtest_start_date: datetime = None,
|
|
415
|
+
backtest_end_date: datetime = None,
|
|
416
|
+
data_source: DataSource = None
|
|
417
|
+
) -> None:
|
|
418
|
+
"""
|
|
419
|
+
Fetches backtest data for a given datasource
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
backtest_index_date (datetime): The date for which to fetch
|
|
423
|
+
backtest data.
|
|
424
|
+
backtest_start_date (datetime): The start date for the
|
|
425
|
+
backtest data.
|
|
426
|
+
backtest_end_date (datetime): The end date for the
|
|
427
|
+
backtest data.
|
|
428
|
+
data_source (Optional[Datasource]): The data source for which to
|
|
429
|
+
fetch backtest data. Defaults to None.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
pl.DataFrame: The backtest data for the given datasource.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
if backtest_start_date is not None and \
|
|
436
|
+
backtest_end_date is not None:
|
|
437
|
+
|
|
438
|
+
if backtest_start_date < self._start_date_data_source:
|
|
439
|
+
|
|
440
|
+
if data_source is not None:
|
|
441
|
+
raise OperationalException(
|
|
442
|
+
f"Request data date {backtest_start_date} "
|
|
443
|
+
f"is before the range of "
|
|
444
|
+
f"the available data "
|
|
445
|
+
f"{self._start_date_data_source} "
|
|
446
|
+
f"- {self._end_date_data_source}."
|
|
447
|
+
f" for data source {data_source.identifier}."
|
|
448
|
+
f" Data source file path: "
|
|
449
|
+
f"{self.get_data_source_file_path()}"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
raise OperationalException(
|
|
453
|
+
f"Request data date {backtest_start_date} "
|
|
454
|
+
f"is before the range of "
|
|
455
|
+
f"the available data "
|
|
456
|
+
f"{self._start_date_data_source} "
|
|
457
|
+
f"- {self._end_date_data_source}."
|
|
458
|
+
f" Data source file path: "
|
|
459
|
+
f"{self.get_data_source_file_path()}"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if backtest_end_date > self._end_date_data_source:
|
|
463
|
+
|
|
464
|
+
if data_source is not None:
|
|
465
|
+
raise OperationalException(
|
|
466
|
+
f"Request data date {backtest_end_date} "
|
|
467
|
+
f"is after the range of "
|
|
468
|
+
f"the available data "
|
|
469
|
+
f"{self._start_date_data_source} "
|
|
470
|
+
f"- {self._end_date_data_source}."
|
|
471
|
+
f" for data source {data_source.identifier}."
|
|
472
|
+
f" Data source file path: "
|
|
473
|
+
f"{self.get_data_source_file_path()}"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
raise OperationalException(
|
|
477
|
+
f"Request data date {backtest_end_date} "
|
|
478
|
+
f"is after the range of "
|
|
479
|
+
f"the available data "
|
|
480
|
+
f"{self._start_date_data_source} "
|
|
481
|
+
f"- {self._end_date_data_source}."
|
|
482
|
+
f" Data source file path: "
|
|
483
|
+
f"{self.get_data_source_file_path()}"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
data = self.data.filter(
|
|
487
|
+
(pl.col("Datetime") >= backtest_start_date) &
|
|
488
|
+
(pl.col("Datetime") <= backtest_end_date)
|
|
489
|
+
)
|
|
490
|
+
else:
|
|
491
|
+
try:
|
|
492
|
+
data = self.window_cache[backtest_index_date]
|
|
493
|
+
except KeyError:
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
# Return the key in the cache that is closest to the
|
|
497
|
+
# backtest_index_date but not after it.
|
|
498
|
+
closest_key = min(
|
|
499
|
+
[k for k in self.window_cache.keys()
|
|
500
|
+
if k >= backtest_index_date]
|
|
501
|
+
)
|
|
502
|
+
data = self.window_cache[closest_key]
|
|
503
|
+
except ValueError:
|
|
504
|
+
|
|
505
|
+
if data_source is not None:
|
|
506
|
+
raise OperationalException(
|
|
507
|
+
"No OHLCV data available for the "
|
|
508
|
+
f"date: {backtest_index_date} "
|
|
509
|
+
f"within the prepared backtest data "
|
|
510
|
+
f"for data source {data_source.identifier}. "
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
raise OperationalException(
|
|
514
|
+
"No OHLCV data available for the "
|
|
515
|
+
f"date: {backtest_index_date} "
|
|
516
|
+
f"within the prepared backtest data "
|
|
517
|
+
f"for symbol {self.symbol}. "
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
if self.pandas:
|
|
521
|
+
data = convert_polars_to_pandas(data)
|
|
522
|
+
|
|
523
|
+
return data
|
|
524
|
+
|
|
525
|
+
def get_ohlcv(
|
|
526
|
+
self, symbol, time_frame, from_timestamp, market, to_timestamp=None
|
|
527
|
+
) -> pl.DataFrame:
|
|
528
|
+
"""
|
|
529
|
+
Function to retrieve ohlcv data for a symbol, time frame and market
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
symbol (str): The symbol to retrieve ohlcv data for
|
|
533
|
+
time_frame: The time frame to retrieve ohlcv data for
|
|
534
|
+
from_timestamp: The start date to retrieve ohlcv data from
|
|
535
|
+
market: The market to retrieve ohlcv data from
|
|
536
|
+
to_timestamp: The end date to retrieve ohlcv data to
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
DataFrame: The ohlcv data for the symbol, time frame and market
|
|
540
|
+
in polars DataFrame format
|
|
541
|
+
"""
|
|
542
|
+
symbol = symbol.upper()
|
|
543
|
+
market_credential = self.get_credential(market)
|
|
544
|
+
exchange = self.initialize_exchange(market, market_credential)
|
|
545
|
+
time_frame = time_frame.value
|
|
546
|
+
|
|
547
|
+
if from_timestamp > to_timestamp:
|
|
548
|
+
raise OperationalException(
|
|
549
|
+
"OHLCV data start date must be before end date"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
if self.config is not None and DATETIME_FORMAT in self.config:
|
|
553
|
+
datetime_format = self.config[DATETIME_FORMAT]
|
|
554
|
+
else:
|
|
555
|
+
datetime_format = CCXT_DATETIME_FORMAT
|
|
556
|
+
|
|
557
|
+
if not exchange.has['fetchOHLCV']:
|
|
558
|
+
raise OperationalException(
|
|
559
|
+
f"Market service {market} does not support "
|
|
560
|
+
f"functionality get_ohclvs"
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
from_timestamp = exchange.parse8601(
|
|
564
|
+
from_timestamp.strftime(datetime_format)
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if to_timestamp is None:
|
|
568
|
+
to_timestamp = exchange.milliseconds()
|
|
569
|
+
else:
|
|
570
|
+
to_timestamp = exchange.parse8601(
|
|
571
|
+
to_timestamp.strftime(datetime_format)
|
|
572
|
+
)
|
|
573
|
+
data = []
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
while from_timestamp < to_timestamp:
|
|
577
|
+
ohlcv = exchange.fetch_ohlcv(
|
|
578
|
+
symbol, time_frame, from_timestamp
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if len(ohlcv) > 0:
|
|
582
|
+
from_timestamp = \
|
|
583
|
+
ohlcv[-1][0] + \
|
|
584
|
+
exchange.parse_timeframe(time_frame) * 1000
|
|
585
|
+
else:
|
|
586
|
+
from_timestamp = to_timestamp
|
|
587
|
+
|
|
588
|
+
for candle in ohlcv:
|
|
589
|
+
datetime_stamp = parser.parse(exchange.iso8601(candle[0]))
|
|
590
|
+
|
|
591
|
+
to_timestamp_datetime = parser.parse(
|
|
592
|
+
exchange.iso8601(to_timestamp),
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if datetime_stamp <= to_timestamp_datetime:
|
|
596
|
+
datetime_stamp = datetime_stamp \
|
|
597
|
+
.strftime(datetime_format)
|
|
598
|
+
|
|
599
|
+
data.append(
|
|
600
|
+
[datetime_stamp] +
|
|
601
|
+
[float(value) for value in candle[1:]]
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
sleep(exchange.rateLimit / 1000)
|
|
605
|
+
except ccxt.NetworkError as e:
|
|
606
|
+
logger.error(
|
|
607
|
+
f"Network error occurred while fetching OHLCV data for "
|
|
608
|
+
f"{symbol} on {market} with time frame {time_frame}: {e}"
|
|
609
|
+
)
|
|
610
|
+
raise NetworkError(
|
|
611
|
+
"Network error occurred, make sure you have an active "
|
|
612
|
+
"internet connection"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Predefined column names
|
|
616
|
+
col_names = ["Datetime", "Open", "High", "Low", "Close", "Volume"]
|
|
617
|
+
|
|
618
|
+
# Combine the Series into a DataFrame with given column names
|
|
619
|
+
df = pl.DataFrame(data, schema=col_names, orient="row").with_columns(
|
|
620
|
+
pl.col("Datetime").str.to_datetime(time_unit="ms", time_zone="UTC")
|
|
621
|
+
)
|
|
622
|
+
return df
|
|
623
|
+
|
|
624
|
+
def create_start_date(self, end_date, time_frame, window_size):
|
|
625
|
+
minutes = TimeFrame.from_value(time_frame).amount_of_minutes
|
|
626
|
+
return end_date - timedelta(minutes=window_size * minutes)
|
|
627
|
+
|
|
628
|
+
def create_end_date(self, start_date, time_frame, window_size):
|
|
629
|
+
minutes = TimeFrame.from_value(time_frame).amount_of_minutes
|
|
630
|
+
return start_date + timedelta(minutes=window_size * minutes)
|
|
631
|
+
|
|
632
|
+
@staticmethod
|
|
633
|
+
def initialize_exchange(market, market_credential):
|
|
634
|
+
"""
|
|
635
|
+
Function to initialize the exchange for the market.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
market (str): The market to initialize the exchange for
|
|
639
|
+
market_credential (MarketCredential): The market credential to use
|
|
640
|
+
for the exchange
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
Exchange: CCXT exchange client
|
|
644
|
+
"""
|
|
645
|
+
market = market.lower()
|
|
646
|
+
|
|
647
|
+
if not hasattr(ccxt, market):
|
|
648
|
+
raise OperationalException(
|
|
649
|
+
f"No ccxt exchange for market id {market}"
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
exchange_class = getattr(ccxt, market)
|
|
653
|
+
|
|
654
|
+
if exchange_class is None:
|
|
655
|
+
raise OperationalException(
|
|
656
|
+
f"No market service found for market id {market}"
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
if market_credential is not None:
|
|
660
|
+
# Check the credentials for the exchange
|
|
661
|
+
CCXTOHLCVDataProvider\
|
|
662
|
+
.check_credentials(exchange_class, market_credential)
|
|
663
|
+
exchange = exchange_class({
|
|
664
|
+
'apiKey': market_credential.api_key,
|
|
665
|
+
'secret': market_credential.secret_key,
|
|
666
|
+
})
|
|
667
|
+
else:
|
|
668
|
+
exchange = exchange_class({})
|
|
669
|
+
return exchange
|
|
670
|
+
|
|
671
|
+
@staticmethod
|
|
672
|
+
def check_credentials(
|
|
673
|
+
exchange_class, market_credential: MarketCredential
|
|
674
|
+
):
|
|
675
|
+
"""
|
|
676
|
+
Function to check if the credentials are valid for the exchange.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
exchange_class: The exchange class to check the credentials for
|
|
680
|
+
market_credential: The market credential to use for the exchange
|
|
681
|
+
|
|
682
|
+
Raises:
|
|
683
|
+
OperationalException: If the credentials are not valid
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
None
|
|
687
|
+
"""
|
|
688
|
+
exchange = exchange_class()
|
|
689
|
+
credentials_info = exchange.requiredCredentials
|
|
690
|
+
market = market_credential.get_market()
|
|
691
|
+
|
|
692
|
+
if ('apiKey' in credentials_info
|
|
693
|
+
and credentials_info["apiKey"]
|
|
694
|
+
and market_credential.get_api_key() is None):
|
|
695
|
+
raise OperationalException(
|
|
696
|
+
f"Market credential for market {market}"
|
|
697
|
+
" requires an api key, either"
|
|
698
|
+
" as an argument or as an environment variable"
|
|
699
|
+
f" named as {market.upper()}_API_KEY"
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if ('secret' in credentials_info
|
|
703
|
+
and credentials_info["secret"]
|
|
704
|
+
and market_credential.get_secret_key() is None):
|
|
705
|
+
raise OperationalException(
|
|
706
|
+
f"Market credential for market {market}"
|
|
707
|
+
" requires a secret key, either"
|
|
708
|
+
" as an argument or as an environment variable"
|
|
709
|
+
f" named as {market.upper()}_SECRET_KEY"
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
def save_data_to_storage(
|
|
713
|
+
self,
|
|
714
|
+
symbol: str,
|
|
715
|
+
market: str,
|
|
716
|
+
time_frame: TimeFrame,
|
|
717
|
+
start_date: datetime,
|
|
718
|
+
end_date: datetime,
|
|
719
|
+
data: pl.DataFrame,
|
|
720
|
+
storage_directory_path: str,
|
|
721
|
+
):
|
|
722
|
+
"""
|
|
723
|
+
Function to save data to the storage path.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
symbol (str): The symbol for which the data is saved.
|
|
727
|
+
market (str): The market for which the data is saved.
|
|
728
|
+
time_frame (TimeFrame): The time frame for which the data is saved.
|
|
729
|
+
data (pl.DataFrame): The data to save.
|
|
730
|
+
storage_directory_path (str): The path to the storage directory.
|
|
731
|
+
start_date (datetime): The start date for the data.
|
|
732
|
+
end_date (datetime): The end date for the data.
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
None
|
|
736
|
+
"""
|
|
737
|
+
if storage_directory_path is None:
|
|
738
|
+
raise OperationalException(
|
|
739
|
+
"Storage path is not set. Please set the storage path "
|
|
740
|
+
"before saving data."
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
if not os.path.isdir(storage_directory_path):
|
|
744
|
+
os.makedirs(storage_directory_path)
|
|
745
|
+
|
|
746
|
+
filename = self._create_filename(
|
|
747
|
+
symbol=symbol,
|
|
748
|
+
market=market,
|
|
749
|
+
time_frame=time_frame.value,
|
|
750
|
+
start_date=start_date,
|
|
751
|
+
end_date=end_date
|
|
752
|
+
)
|
|
753
|
+
storage_path = os.path.join(storage_directory_path, filename)
|
|
754
|
+
if os.path.exists(storage_path):
|
|
755
|
+
os.remove(storage_path)
|
|
756
|
+
|
|
757
|
+
# Create the file
|
|
758
|
+
if not os.path.exists(storage_path):
|
|
759
|
+
with open(storage_path, 'w'):
|
|
760
|
+
pass
|
|
761
|
+
|
|
762
|
+
data.write_csv(storage_path)
|
|
763
|
+
|
|
764
|
+
def _create_filename(
|
|
765
|
+
self,
|
|
766
|
+
symbol: str,
|
|
767
|
+
market: str,
|
|
768
|
+
time_frame: str,
|
|
769
|
+
start_date: datetime,
|
|
770
|
+
end_date: datetime
|
|
771
|
+
) -> str:
|
|
772
|
+
"""
|
|
773
|
+
Creates a filename for the data file based on the parameters.
|
|
774
|
+
The date format is YYYYMMDDHH for both start and end dates.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
symbol (str): The symbol of the data.
|
|
778
|
+
market (str): The market of the data.
|
|
779
|
+
time_frame (str): The time frame of the data.
|
|
780
|
+
start_date (datetime): The start date of the data.
|
|
781
|
+
end_date (datetime): The end date of the data.
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
str: The generated filename.
|
|
785
|
+
"""
|
|
786
|
+
datetime_format = self.config[DATETIME_FORMAT_FILE_NAME]
|
|
787
|
+
symbol = symbol.upper().replace('/', '-')
|
|
788
|
+
start_date_str = start_date.strftime(datetime_format)
|
|
789
|
+
end_date_str = end_date.strftime(datetime_format)
|
|
790
|
+
filename = (
|
|
791
|
+
f"OHLCV_{symbol}_{market.upper()}_{time_frame}_{start_date_str}_"
|
|
792
|
+
f"{end_date_str}.csv"
|
|
793
|
+
)
|
|
794
|
+
return filename
|
|
795
|
+
|
|
796
|
+
def _get_data_from_storage(
|
|
797
|
+
self,
|
|
798
|
+
storage_path,
|
|
799
|
+
symbol: str,
|
|
800
|
+
market: str,
|
|
801
|
+
time_frame: TimeFrame,
|
|
802
|
+
start_date: datetime,
|
|
803
|
+
end_date: datetime,
|
|
804
|
+
) -> Union[pl.DataFrame, None]:
|
|
805
|
+
"""
|
|
806
|
+
Helper function to retrieve the data from the storage path if
|
|
807
|
+
it exists. If the data does not exist, it returns None.
|
|
808
|
+
"""
|
|
809
|
+
data = None
|
|
810
|
+
if storage_path is None:
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
# Loop through all files in the data storage path
|
|
814
|
+
if not os.path.isdir(storage_path):
|
|
815
|
+
logger.error(
|
|
816
|
+
f"Storage path {storage_path} does not exist or is not a "
|
|
817
|
+
"directory."
|
|
818
|
+
)
|
|
819
|
+
return None
|
|
820
|
+
|
|
821
|
+
for file_name in os.listdir(storage_path):
|
|
822
|
+
if file_name.startswith("OHLCV_") and file_name.endswith(".csv"):
|
|
823
|
+
|
|
824
|
+
try:
|
|
825
|
+
data_source_spec = self.\
|
|
826
|
+
_get_data_source_specification_from_file_name(
|
|
827
|
+
file_name
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
if data_source_spec is None:
|
|
831
|
+
continue
|
|
832
|
+
|
|
833
|
+
if data_source_spec.symbol.upper() == symbol.upper() and \
|
|
834
|
+
data_source_spec.market.upper() == market.upper() and \
|
|
835
|
+
data_source_spec.time_frame.equals(time_frame):
|
|
836
|
+
|
|
837
|
+
# Check if the data source specification matches
|
|
838
|
+
# the start and end date if its specified
|
|
839
|
+
if (data_source_spec.start_date is not None and
|
|
840
|
+
data_source_spec.end_date is not None and
|
|
841
|
+
(data_source_spec.start_date <= start_date
|
|
842
|
+
and data_source_spec.end_date >= end_date)):
|
|
843
|
+
|
|
844
|
+
# If the data source specification matches,
|
|
845
|
+
# read the file
|
|
846
|
+
file_path = os.path.join(storage_path, file_name)
|
|
847
|
+
self.data_file_path = file_path
|
|
848
|
+
|
|
849
|
+
# Read CSV as-is first
|
|
850
|
+
data = pl.read_csv(file_path, low_memory=True)
|
|
851
|
+
|
|
852
|
+
# Check what columns we have
|
|
853
|
+
if "Datetime" in data.columns:
|
|
854
|
+
# Try to parse the datetime column
|
|
855
|
+
try:
|
|
856
|
+
# Try the ISO format with timezone first
|
|
857
|
+
data = data.with_columns(
|
|
858
|
+
pl.col("Datetime").str.to_datetime(
|
|
859
|
+
format="%Y-%m-%dT%H:%M:%S%.f%z",
|
|
860
|
+
time_zone="UTC"
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
except Exception as e1:
|
|
864
|
+
try:
|
|
865
|
+
# Fallback: let Polars infer the format
|
|
866
|
+
data = data.with_columns(
|
|
867
|
+
pl.col("Datetime").str.to_datetime(
|
|
868
|
+
time_zone="UTC"
|
|
869
|
+
)
|
|
870
|
+
)
|
|
871
|
+
except Exception as e2:
|
|
872
|
+
logger.warning(
|
|
873
|
+
f"Could not parse Datetime "
|
|
874
|
+
f"column in {file_name}: "
|
|
875
|
+
f"Format error: {str(e1)}, "
|
|
876
|
+
f"Infer error: {str(e2)}"
|
|
877
|
+
)
|
|
878
|
+
continue
|
|
879
|
+
else:
|
|
880
|
+
logger.warning(
|
|
881
|
+
f"No 'Datetime' column "
|
|
882
|
+
f"found in {file_name}. "
|
|
883
|
+
f"Available columns: {data.columns}"
|
|
884
|
+
)
|
|
885
|
+
continue
|
|
886
|
+
|
|
887
|
+
# Filter by date range
|
|
888
|
+
data = data.filter(
|
|
889
|
+
(pl.col("Datetime") >= start_date) &
|
|
890
|
+
(pl.col("Datetime") <= end_date)
|
|
891
|
+
)
|
|
892
|
+
break
|
|
893
|
+
|
|
894
|
+
except Exception as e:
|
|
895
|
+
logger.warning(
|
|
896
|
+
f"Error reading data from {file_name}: {str(e)}"
|
|
897
|
+
)
|
|
898
|
+
continue
|
|
899
|
+
|
|
900
|
+
return data
|
|
901
|
+
|
|
902
|
+
def _get_data_source_specification_from_file_name(
|
|
903
|
+
self, file_name: str
|
|
904
|
+
) -> Union[DataSource, None]:
|
|
905
|
+
"""
|
|
906
|
+
Extracts the data source specification from the OHLCV data filename.
|
|
907
|
+
Given that the file name is in the format:
|
|
908
|
+
|
|
909
|
+
"OHLCV_<SYMBOL>_<MARKET>_<TIME_FRAME>_<START_DATE>_<END_DATE>.csv",
|
|
910
|
+
this function extracts all attributes and returns a DataSource object.
|
|
911
|
+
This object can then later be used to compare it to the datasource
|
|
912
|
+
object that is passed to the get_data method.
|
|
913
|
+
|
|
914
|
+
Args:
|
|
915
|
+
file_name (str): The file name from which to extract the DataSource
|
|
916
|
+
|
|
917
|
+
Returns:
|
|
918
|
+
DataSource: The extracted data source specification.
|
|
919
|
+
"""
|
|
920
|
+
|
|
921
|
+
try:
|
|
922
|
+
parts = file_name.split('_')
|
|
923
|
+
|
|
924
|
+
if len(parts) < 3:
|
|
925
|
+
return None
|
|
926
|
+
|
|
927
|
+
data_type = parts[0].upper()
|
|
928
|
+
symbol = parts[1].upper().replace('-', '/')
|
|
929
|
+
market = parts[2].upper()
|
|
930
|
+
time_frame_str = parts[3]
|
|
931
|
+
start_date_str = parts[4]
|
|
932
|
+
end_date_str = parts[5].replace('.csv', '')
|
|
933
|
+
return DataSource(
|
|
934
|
+
data_type=DataType.from_string(data_type),
|
|
935
|
+
symbol=symbol,
|
|
936
|
+
market=market,
|
|
937
|
+
time_frame=TimeFrame.from_string(time_frame_str),
|
|
938
|
+
start_date=parser.parse(
|
|
939
|
+
start_date_str
|
|
940
|
+
).replace(tzinfo=timezone.utc),
|
|
941
|
+
end_date=parser.parse(
|
|
942
|
+
end_date_str
|
|
943
|
+
).replace(tzinfo=timezone.utc)
|
|
944
|
+
)
|
|
945
|
+
except ValueError:
|
|
946
|
+
logger.info(
|
|
947
|
+
f"Could not extract data source attributes from "
|
|
948
|
+
f"file name: {file_name}. "
|
|
949
|
+
f"Expected format 'OHLCV_<SYMBOL>_<MARKET>_<TIME_FRAME>_"
|
|
950
|
+
f"<START_DATE>_<END_DATE>.csv."
|
|
951
|
+
)
|
|
952
|
+
return None
|
|
953
|
+
|
|
954
|
+
def _precompute_sliding_windows(
|
|
955
|
+
self,
|
|
956
|
+
data,
|
|
957
|
+
window_size: int,
|
|
958
|
+
time_frame: TimeFrame,
|
|
959
|
+
start_date: datetime,
|
|
960
|
+
end_date: datetime
|
|
961
|
+
) -> None:
|
|
962
|
+
"""
|
|
963
|
+
Precompute all sliding windows for fast retrieval in backtest mode.
|
|
964
|
+
|
|
965
|
+
A sliding window is calculated as a subset of the data. It will
|
|
966
|
+
take for each timestamp in the data a window of size `window_size`
|
|
967
|
+
and stores it in a cache with the last timestamp of the window.
|
|
968
|
+
|
|
969
|
+
So if the window size is 200, the first window will be
|
|
970
|
+
the first 200 rows of the data, the second window will be
|
|
971
|
+
the rows 1 to 200, the third window will be the rows
|
|
972
|
+
2 to 201, and so on until the last window which will be
|
|
973
|
+
the last 200 rows of the data.
|
|
974
|
+
|
|
975
|
+
Args:
|
|
976
|
+
data (pl.DataFrame): The data to precompute the sliding
|
|
977
|
+
windows for.
|
|
978
|
+
window_size (int): The size of the sliding window to precompute.
|
|
979
|
+
start_date (datetime, optional): The start date for the sliding
|
|
980
|
+
windows.
|
|
981
|
+
end_date (datetime, optional): The end date for the sliding
|
|
982
|
+
windows.
|
|
983
|
+
|
|
984
|
+
Returns:
|
|
985
|
+
None
|
|
986
|
+
"""
|
|
987
|
+
self.window_cache = {}
|
|
988
|
+
timestamps = data["Datetime"].to_list()
|
|
989
|
+
# Only select the entries after the start date
|
|
990
|
+
timestamps = [
|
|
991
|
+
ts for ts in timestamps if start_date <= ts <= end_date
|
|
992
|
+
]
|
|
993
|
+
|
|
994
|
+
# Create sliding windows of size <window_size> for each timestamp
|
|
995
|
+
# in the data with the given the time frame and window size
|
|
996
|
+
for timestamp in timestamps:
|
|
997
|
+
# Use timestamp as key
|
|
998
|
+
self.window_cache[timestamp] = data.filter(
|
|
999
|
+
(data["Datetime"] <= timestamp) &
|
|
1000
|
+
(data["Datetime"] >= timestamp - timedelta(
|
|
1001
|
+
minutes=time_frame.amount_of_minutes * window_size
|
|
1002
|
+
))
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
# Make sure the end datetime of the backtest is included in the
|
|
1006
|
+
# sliding windows cache
|
|
1007
|
+
if end_date not in self.window_cache:
|
|
1008
|
+
self.window_cache[end_date] = data[-window_size:]
|
|
1009
|
+
|
|
1010
|
+
def get_storage_directory(self) -> Union[str, None]:
|
|
1011
|
+
"""
|
|
1012
|
+
Get the storage directory for the OHLCV data provider.
|
|
1013
|
+
|
|
1014
|
+
Returns:
|
|
1015
|
+
Union[str, None]: The storage directory path if set,
|
|
1016
|
+
otherwise None.
|
|
1017
|
+
"""
|
|
1018
|
+
|
|
1019
|
+
if self.storage_directory is not None:
|
|
1020
|
+
return self.storage_directory
|
|
1021
|
+
|
|
1022
|
+
if self.config is not None:
|
|
1023
|
+
resource_directory = self.config.get(RESOURCE_DIRECTORY)
|
|
1024
|
+
data_directory_name = self.config.get(DATA_DIRECTORY)
|
|
1025
|
+
return os.path.join(resource_directory, data_directory_name)
|
|
1026
|
+
|
|
1027
|
+
return None
|
|
1028
|
+
|
|
1029
|
+
def copy(self, data_source) -> "CCXTOHLCVDataProvider":
|
|
1030
|
+
"""
|
|
1031
|
+
Returns a copy of the CCXTOHLCVDataProvider instance based on a
|
|
1032
|
+
given data source. The data source is previously matched
|
|
1033
|
+
with the 'has_data' method. Then a new instance of the data
|
|
1034
|
+
provider must be registered in the framework so that each
|
|
1035
|
+
data source has its own instance of the data provider.
|
|
1036
|
+
|
|
1037
|
+
Args:
|
|
1038
|
+
data_source (DataSource): The data source specification that
|
|
1039
|
+
matches a data provider.
|
|
1040
|
+
|
|
1041
|
+
Returns:
|
|
1042
|
+
DataProvider: A new instance of the data provider with the same
|
|
1043
|
+
configuration.
|
|
1044
|
+
"""
|
|
1045
|
+
# Check that the data source has the required attributes set
|
|
1046
|
+
# for usage with CCXT data providers
|
|
1047
|
+
|
|
1048
|
+
if data_source.market is None or data_source.market == "":
|
|
1049
|
+
raise OperationalException(
|
|
1050
|
+
"DataSource has not `market` attribute specified, "
|
|
1051
|
+
"please specify the market attribute in the "
|
|
1052
|
+
"data source specification before using the "
|
|
1053
|
+
"ccxt OHLCV data provider"
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
if data_source.time_frame is None or data_source.time_frame == "":
|
|
1057
|
+
raise OperationalException(
|
|
1058
|
+
"DataSource has not `time_frame` attribute specified, "
|
|
1059
|
+
"please specify the time_frame attribute in the "
|
|
1060
|
+
"data source specification before using the "
|
|
1061
|
+
"ccxt OHLCV data provider"
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
if data_source.symbol is None or data_source.symbol == "":
|
|
1065
|
+
raise OperationalException(
|
|
1066
|
+
"DataSource has not `symbol` attribute specified, "
|
|
1067
|
+
"please specify the symbol attribute in the "
|
|
1068
|
+
"data source specification before using the "
|
|
1069
|
+
"ccxt OHLCV data provider"
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
storage_path = data_source.storage_path
|
|
1073
|
+
|
|
1074
|
+
if storage_path is None:
|
|
1075
|
+
storage_path = self.get_storage_directory()
|
|
1076
|
+
|
|
1077
|
+
return CCXTOHLCVDataProvider(
|
|
1078
|
+
symbol=data_source.symbol,
|
|
1079
|
+
time_frame=data_source.time_frame,
|
|
1080
|
+
market=data_source.market,
|
|
1081
|
+
window_size=data_source.window_size,
|
|
1082
|
+
data_provider_identifier=data_source.data_provider_identifier,
|
|
1083
|
+
storage_directory=storage_path,
|
|
1084
|
+
config=self.config,
|
|
1085
|
+
pandas=data_source.pandas,
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
def get_number_of_data_points(
|
|
1089
|
+
self,
|
|
1090
|
+
start_date: datetime,
|
|
1091
|
+
end_date: datetime
|
|
1092
|
+
) -> int:
|
|
1093
|
+
|
|
1094
|
+
"""
|
|
1095
|
+
Returns the number of data points available between the given
|
|
1096
|
+
start and end dates.
|
|
1097
|
+
|
|
1098
|
+
Args:
|
|
1099
|
+
start_date (datetime): The start date for checking missing data.
|
|
1100
|
+
end_date (datetime): The end date for checking missing data.
|
|
1101
|
+
|
|
1102
|
+
Returns:
|
|
1103
|
+
int: The number of available data points between the given
|
|
1104
|
+
start and end dates.
|
|
1105
|
+
"""
|
|
1106
|
+
available_dates = [
|
|
1107
|
+
date for date in self.data["Datetime"].to_list()
|
|
1108
|
+
if start_date <= date <= end_date
|
|
1109
|
+
]
|
|
1110
|
+
return len(available_dates)
|
|
1111
|
+
|
|
1112
|
+
def get_missing_data_dates(
|
|
1113
|
+
self,
|
|
1114
|
+
start_date: datetime,
|
|
1115
|
+
end_date: datetime,
|
|
1116
|
+
) -> List[datetime]:
|
|
1117
|
+
"""
|
|
1118
|
+
Returns a list of dates for which data is missing between the
|
|
1119
|
+
given start and end dates.
|
|
1120
|
+
|
|
1121
|
+
Args:
|
|
1122
|
+
start_date (datetime): The start date for checking missing data.
|
|
1123
|
+
end_date (datetime): The end date for checking missing data.
|
|
1124
|
+
|
|
1125
|
+
Returns:
|
|
1126
|
+
List[datetime]: A list of dates for which data is missing
|
|
1127
|
+
between the given start and end dates.
|
|
1128
|
+
"""
|
|
1129
|
+
missing_dates = [
|
|
1130
|
+
date for date in self.missing_data_point_dates
|
|
1131
|
+
if start_date <= date <= end_date
|
|
1132
|
+
]
|
|
1133
|
+
return missing_dates
|
|
1134
|
+
|
|
1135
|
+
def get_data_source_file_path(self) -> Union[str, None]:
|
|
1136
|
+
"""
|
|
1137
|
+
Get the file path of the data source if stored in local storage.
|
|
1138
|
+
|
|
1139
|
+
Returns:
|
|
1140
|
+
Union[str, None]: The file path of the data source if stored
|
|
1141
|
+
locally, otherwise None.
|
|
1142
|
+
"""
|
|
1143
|
+
return self.data_file_path
|