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.

Files changed (260) hide show
  1. investing_algorithm_framework/__init__.py +197 -0
  2. investing_algorithm_framework/app/__init__.py +47 -0
  3. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  4. investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
  5. investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
  6. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  7. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  8. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  9. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  10. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  11. investing_algorithm_framework/app/app.py +2204 -0
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1667 -0
  14. investing_algorithm_framework/app/eventloop.py +590 -0
  15. investing_algorithm_framework/app/reporting/__init__.py +27 -0
  16. investing_algorithm_framework/app/reporting/ascii.py +921 -0
  17. investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
  18. investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
  19. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  20. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  21. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
  22. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  23. investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
  24. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  25. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
  26. investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
  27. investing_algorithm_framework/app/reporting/generate.py +185 -0
  28. investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
  29. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
  30. investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
  31. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
  32. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  33. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  34. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  35. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  36. investing_algorithm_framework/app/stateless/__init__.py +35 -0
  37. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
  38. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
  39. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
  40. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
  41. investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
  42. investing_algorithm_framework/app/strategy.py +675 -0
  43. investing_algorithm_framework/app/task.py +41 -0
  44. investing_algorithm_framework/app/web/__init__.py +5 -0
  45. investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
  46. investing_algorithm_framework/app/web/controllers/orders.py +20 -0
  47. investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
  48. investing_algorithm_framework/app/web/controllers/positions.py +18 -0
  49. investing_algorithm_framework/app/web/create_app.py +20 -0
  50. investing_algorithm_framework/app/web/error_handler.py +59 -0
  51. investing_algorithm_framework/app/web/responses.py +20 -0
  52. investing_algorithm_framework/app/web/run_strategies.py +4 -0
  53. investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
  54. investing_algorithm_framework/app/web/schemas/order.py +12 -0
  55. investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
  56. investing_algorithm_framework/app/web/schemas/position.py +15 -0
  57. investing_algorithm_framework/app/web/setup_cors.py +6 -0
  58. investing_algorithm_framework/cli/__init__.py +0 -0
  59. investing_algorithm_framework/cli/cli.py +207 -0
  60. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
  61. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  62. investing_algorithm_framework/cli/initialize_app.py +603 -0
  63. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  64. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  65. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  66. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  67. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  68. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  69. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  70. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  71. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  72. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  73. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  74. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  75. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  76. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  77. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  78. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  79. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  80. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  81. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  82. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  83. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  84. investing_algorithm_framework/create_app.py +54 -0
  85. investing_algorithm_framework/dependency_container.py +155 -0
  86. investing_algorithm_framework/domain/__init__.py +148 -0
  87. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  88. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  89. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  90. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  91. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  92. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  93. investing_algorithm_framework/domain/backtesting/backtest_run.py +435 -0
  94. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  95. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  96. investing_algorithm_framework/domain/config.py +111 -0
  97. investing_algorithm_framework/domain/constants.py +83 -0
  98. investing_algorithm_framework/domain/data_provider.py +334 -0
  99. investing_algorithm_framework/domain/data_structures.py +42 -0
  100. investing_algorithm_framework/domain/decimal_parsing.py +40 -0
  101. investing_algorithm_framework/domain/exceptions.py +112 -0
  102. investing_algorithm_framework/domain/models/__init__.py +43 -0
  103. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  104. investing_algorithm_framework/domain/models/base_model.py +25 -0
  105. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  106. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  107. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  108. investing_algorithm_framework/domain/models/event.py +35 -0
  109. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  110. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  111. investing_algorithm_framework/domain/models/order/__init__.py +6 -0
  112. investing_algorithm_framework/domain/models/order/order.py +384 -0
  113. investing_algorithm_framework/domain/models/order/order_side.py +36 -0
  114. investing_algorithm_framework/domain/models/order/order_status.py +37 -0
  115. investing_algorithm_framework/domain/models/order/order_type.py +30 -0
  116. investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
  117. investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
  118. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
  119. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
  120. investing_algorithm_framework/domain/models/position/__init__.py +4 -0
  121. investing_algorithm_framework/domain/models/position/position.py +68 -0
  122. investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
  123. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  124. investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
  125. investing_algorithm_framework/domain/models/time_frame.py +153 -0
  126. investing_algorithm_framework/domain/models/time_interval.py +124 -0
  127. investing_algorithm_framework/domain/models/time_unit.py +149 -0
  128. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  129. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  130. investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
  131. investing_algorithm_framework/domain/models/trade/trade.py +388 -0
  132. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
  133. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  134. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
  135. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
  136. investing_algorithm_framework/domain/order_executor.py +112 -0
  137. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  138. investing_algorithm_framework/domain/positions/__init__.py +4 -0
  139. investing_algorithm_framework/domain/positions/position_size.py +41 -0
  140. investing_algorithm_framework/domain/services/__init__.py +11 -0
  141. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  142. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  143. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  144. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  145. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  146. investing_algorithm_framework/domain/stateless_actions.py +7 -0
  147. investing_algorithm_framework/domain/strategy.py +44 -0
  148. investing_algorithm_framework/domain/utils/__init__.py +27 -0
  149. investing_algorithm_framework/domain/utils/csv.py +104 -0
  150. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  151. investing_algorithm_framework/domain/utils/dates.py +57 -0
  152. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  153. investing_algorithm_framework/domain/utils/polars.py +53 -0
  154. investing_algorithm_framework/domain/utils/random.py +41 -0
  155. investing_algorithm_framework/domain/utils/signatures.py +17 -0
  156. investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
  157. investing_algorithm_framework/domain/utils/synchronized.py +12 -0
  158. investing_algorithm_framework/download_data.py +108 -0
  159. investing_algorithm_framework/infrastructure/__init__.py +50 -0
  160. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  161. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
  162. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  163. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  164. investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
  165. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
  166. investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
  167. investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
  168. investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
  169. investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
  170. investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
  171. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  172. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  173. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
  174. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
  175. investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
  176. investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
  177. investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
  178. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
  179. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  180. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  181. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
  182. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
  183. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  184. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  185. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  186. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  187. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  188. investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
  189. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  190. investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
  191. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
  192. investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
  193. investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
  194. investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
  195. investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
  196. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  197. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
  198. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
  199. investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
  200. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  201. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
  202. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  203. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  204. investing_algorithm_framework/services/__init__.py +132 -0
  205. investing_algorithm_framework/services/backtesting/__init__.py +5 -0
  206. investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
  207. investing_algorithm_framework/services/configuration_service.py +96 -0
  208. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  209. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  210. investing_algorithm_framework/services/market_credential_service.py +40 -0
  211. investing_algorithm_framework/services/metrics/__init__.py +114 -0
  212. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  213. investing_algorithm_framework/services/metrics/beta.py +0 -0
  214. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  215. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  216. investing_algorithm_framework/services/metrics/drawdown.py +181 -0
  217. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  218. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  219. investing_algorithm_framework/services/metrics/generate.py +358 -0
  220. investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
  221. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  222. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  223. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  224. investing_algorithm_framework/services/metrics/returns.py +452 -0
  225. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  226. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  227. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  228. investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
  229. investing_algorithm_framework/services/metrics/trades.py +500 -0
  230. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  231. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  232. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  233. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  234. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  235. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  236. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  237. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  238. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  239. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  240. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  241. investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
  242. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  243. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  244. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  245. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  246. investing_algorithm_framework/services/positions/__init__.py +7 -0
  247. investing_algorithm_framework/services/positions/position_service.py +210 -0
  248. investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
  249. investing_algorithm_framework/services/repository_service.py +40 -0
  250. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  251. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
  252. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
  253. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
  254. investing_algorithm_framework/services/trade_service/__init__.py +3 -0
  255. investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
  256. investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
  257. investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
  258. investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
  259. investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
  260. 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