Qubx 0.5.7__cp312-cp312-manylinux_2_39_x86_64.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 Qubx might be problematic. Click here for more details.

Files changed (100) hide show
  1. qubx/__init__.py +207 -0
  2. qubx/_nb_magic.py +100 -0
  3. qubx/backtester/__init__.py +5 -0
  4. qubx/backtester/account.py +145 -0
  5. qubx/backtester/broker.py +87 -0
  6. qubx/backtester/data.py +296 -0
  7. qubx/backtester/management.py +378 -0
  8. qubx/backtester/ome.py +296 -0
  9. qubx/backtester/optimization.py +201 -0
  10. qubx/backtester/simulated_data.py +558 -0
  11. qubx/backtester/simulator.py +362 -0
  12. qubx/backtester/utils.py +780 -0
  13. qubx/cli/__init__.py +0 -0
  14. qubx/cli/commands.py +67 -0
  15. qubx/connectors/ccxt/__init__.py +0 -0
  16. qubx/connectors/ccxt/account.py +495 -0
  17. qubx/connectors/ccxt/broker.py +132 -0
  18. qubx/connectors/ccxt/customizations.py +193 -0
  19. qubx/connectors/ccxt/data.py +612 -0
  20. qubx/connectors/ccxt/exceptions.py +17 -0
  21. qubx/connectors/ccxt/factory.py +93 -0
  22. qubx/connectors/ccxt/utils.py +307 -0
  23. qubx/core/__init__.py +0 -0
  24. qubx/core/account.py +251 -0
  25. qubx/core/basics.py +850 -0
  26. qubx/core/context.py +420 -0
  27. qubx/core/exceptions.py +38 -0
  28. qubx/core/helpers.py +480 -0
  29. qubx/core/interfaces.py +1150 -0
  30. qubx/core/loggers.py +514 -0
  31. qubx/core/lookups.py +475 -0
  32. qubx/core/metrics.py +1512 -0
  33. qubx/core/mixins/__init__.py +13 -0
  34. qubx/core/mixins/market.py +94 -0
  35. qubx/core/mixins/processing.py +428 -0
  36. qubx/core/mixins/subscription.py +203 -0
  37. qubx/core/mixins/trading.py +88 -0
  38. qubx/core/mixins/universe.py +270 -0
  39. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  40. qubx/core/series.pxd +125 -0
  41. qubx/core/series.pyi +118 -0
  42. qubx/core/series.pyx +988 -0
  43. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  44. qubx/core/utils.pyi +6 -0
  45. qubx/core/utils.pyx +62 -0
  46. qubx/data/__init__.py +25 -0
  47. qubx/data/helpers.py +416 -0
  48. qubx/data/readers.py +1562 -0
  49. qubx/data/tardis.py +100 -0
  50. qubx/gathering/simplest.py +88 -0
  51. qubx/math/__init__.py +3 -0
  52. qubx/math/stats.py +129 -0
  53. qubx/pandaz/__init__.py +23 -0
  54. qubx/pandaz/ta.py +2757 -0
  55. qubx/pandaz/utils.py +638 -0
  56. qubx/resources/instruments/symbols-binance.cm.json +1 -0
  57. qubx/resources/instruments/symbols-binance.json +1 -0
  58. qubx/resources/instruments/symbols-binance.um.json +1 -0
  59. qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
  60. qubx/resources/instruments/symbols-bitfinex.json +1 -0
  61. qubx/resources/instruments/symbols-kraken.f.json +1 -0
  62. qubx/resources/instruments/symbols-kraken.json +1 -0
  63. qubx/ta/__init__.py +0 -0
  64. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  65. qubx/ta/indicators.pxd +149 -0
  66. qubx/ta/indicators.pyi +41 -0
  67. qubx/ta/indicators.pyx +787 -0
  68. qubx/trackers/__init__.py +3 -0
  69. qubx/trackers/abvanced.py +236 -0
  70. qubx/trackers/composite.py +146 -0
  71. qubx/trackers/rebalancers.py +129 -0
  72. qubx/trackers/riskctrl.py +641 -0
  73. qubx/trackers/sizers.py +235 -0
  74. qubx/utils/__init__.py +5 -0
  75. qubx/utils/_pyxreloader.py +281 -0
  76. qubx/utils/charting/lookinglass.py +1057 -0
  77. qubx/utils/charting/mpl_helpers.py +1183 -0
  78. qubx/utils/marketdata/binance.py +284 -0
  79. qubx/utils/marketdata/ccxt.py +90 -0
  80. qubx/utils/marketdata/dukas.py +130 -0
  81. qubx/utils/misc.py +541 -0
  82. qubx/utils/ntp.py +63 -0
  83. qubx/utils/numbers_utils.py +7 -0
  84. qubx/utils/orderbook.py +491 -0
  85. qubx/utils/plotting/__init__.py +0 -0
  86. qubx/utils/plotting/dashboard.py +150 -0
  87. qubx/utils/plotting/data.py +137 -0
  88. qubx/utils/plotting/interfaces.py +25 -0
  89. qubx/utils/plotting/renderers/__init__.py +0 -0
  90. qubx/utils/plotting/renderers/plotly.py +0 -0
  91. qubx/utils/runner/__init__.py +1 -0
  92. qubx/utils/runner/_jupyter_runner.pyt +60 -0
  93. qubx/utils/runner/accounts.py +88 -0
  94. qubx/utils/runner/configs.py +65 -0
  95. qubx/utils/runner/runner.py +470 -0
  96. qubx/utils/time.py +312 -0
  97. qubx-0.5.7.dist-info/METADATA +105 -0
  98. qubx-0.5.7.dist-info/RECORD +100 -0
  99. qubx-0.5.7.dist-info/WHEEL +4 -0
  100. qubx-0.5.7.dist-info/entry_points.txt +3 -0
qubx/core/utils.pyi ADDED
@@ -0,0 +1,6 @@
1
+ def recognize_time(time): ...
2
+ def time_to_str(t: int, units: str = "ns") -> str: ...
3
+ def time_delta_to_str(d: int) -> str: ...
4
+ def recognize_timeframe(timeframe): ...
5
+ def prec_ceil(a: float, precision: int) -> float: ...
6
+ def prec_floor(a: float, precision: int) -> float: ...
qubx/core/utils.pyx ADDED
@@ -0,0 +1,62 @@
1
+ from qubx.utils import convert_tf_str_td64
2
+ import numpy as np
3
+ cimport numpy as np
4
+
5
+
6
+ NS = 1_000_000_000
7
+
8
+ cpdef recognize_time(time):
9
+ return np.datetime64(time, 'ns') if isinstance(time, str) else np.datetime64(time, 'ms')
10
+
11
+
12
+ cpdef str time_to_str(long long t, str units = 'ns'):
13
+ return str(np.datetime64(t, units)) #.isoformat()
14
+
15
+
16
+ cpdef str time_delta_to_str(long long d):
17
+ """
18
+ Convert timedelta object to pretty print format
19
+
20
+ :param d:
21
+ :return:
22
+ """
23
+ days, seconds = divmod(d, 86400*NS)
24
+ hours, seconds = divmod(seconds, 3600*NS)
25
+ minutes, seconds = divmod(seconds, 60*NS)
26
+ seconds, rem = divmod(seconds, NS)
27
+ r = ''
28
+ if days > 0:
29
+ r += '%dD' % days
30
+ if hours > 0:
31
+ r += '%dh' % hours
32
+ if minutes > 0:
33
+ r += '%dMin' % minutes
34
+ if seconds > 0:
35
+ r += '%dS' % seconds
36
+ if rem > 0:
37
+ r += '%dmS' % (rem // 1000000)
38
+ return r
39
+
40
+
41
+ cpdef recognize_timeframe(timeframe):
42
+ tf = timeframe
43
+ if isinstance(timeframe, str):
44
+ tf = np.int64(convert_tf_str_td64(timeframe).item().total_seconds() * NS)
45
+
46
+ elif isinstance(timeframe, (int, float)) and timeframe >= 0:
47
+ tf = timeframe
48
+
49
+ elif isinstance(timeframe, np.timedelta64):
50
+ tf = np.int64(timeframe.item().total_seconds() * NS)
51
+
52
+ else:
53
+ raise ValueError(f'Unknown timeframe type: {timeframe} !')
54
+ return tf
55
+
56
+
57
+ cpdef double prec_ceil(double a, int precision):
58
+ return np.sign(a) * np.true_divide(np.ceil(round(abs(a) * 10**precision, precision)), 10**precision)
59
+
60
+
61
+ cpdef double prec_floor(double a, int precision):
62
+ return np.sign(a) * np.true_divide(np.floor(round(abs(a) * 10**precision, precision)), 10**precision)
qubx/data/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ __all__ = [
2
+ "DataReader",
3
+ "CsvStorageDataReader",
4
+ "MultiQdbConnector",
5
+ "QuestDBConnector",
6
+ "AsOhlcvSeries",
7
+ "AsPandasFrame",
8
+ "AsQuotes",
9
+ "AsTimestampedRecords",
10
+ "RestoreTicksFromOHLC",
11
+ "loader",
12
+ ]
13
+
14
+ from .helpers import loader
15
+ from .readers import (
16
+ AsOhlcvSeries,
17
+ AsPandasFrame,
18
+ AsQuotes,
19
+ AsTimestampedRecords,
20
+ CsvStorageDataReader,
21
+ DataReader,
22
+ MultiQdbConnector,
23
+ QuestDBConnector,
24
+ RestoreTicksFromOHLC,
25
+ )
qubx/data/helpers.py ADDED
@@ -0,0 +1,416 @@
1
+ from collections import defaultdict
2
+ from typing import Any, Dict, Iterable, List, Set, Type
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from joblib import delayed
7
+
8
+ from qubx import logger
9
+ from qubx.core.basics import DataType, ITimeProvider
10
+ from qubx.core.series import TimeSeries
11
+ from qubx.data.readers import (
12
+ CsvStorageDataReader,
13
+ DataReader,
14
+ DataTransformer,
15
+ InMemoryDataFrameReader,
16
+ MultiQdbConnector,
17
+ QuestDBConnector,
18
+ _list_to_chunked_iterator,
19
+ )
20
+ from qubx.pandaz.utils import OhlcDict, generate_equal_date_ranges, ohlc_resample, srows
21
+ from qubx.utils.misc import ProgressParallel
22
+ from qubx.utils.time import handle_start_stop
23
+
24
+
25
+ class InMemoryCachedReader(InMemoryDataFrameReader):
26
+ """
27
+ A class for caching and reading financial data from memory.
28
+
29
+ This class extends InMemoryDataFrameReader to provide efficient data caching and retrieval
30
+ for financial data from a specific exchange and timeframe.
31
+ """
32
+
33
+ exchange: str
34
+ _data_timeframe: str
35
+ _reader: DataReader
36
+ _n_jobs: int
37
+ _start: pd.Timestamp | None = None
38
+ _stop: pd.Timestamp | None = None
39
+ _symbols: list[str]
40
+
41
+ # - external data
42
+ _external: dict[str, pd.DataFrame | pd.Series]
43
+
44
+ def __init__(
45
+ self,
46
+ exchange: str,
47
+ reader: DataReader,
48
+ base_timeframe: str,
49
+ n_jobs: int = -1,
50
+ **kwargs,
51
+ ) -> None:
52
+ self._reader = reader
53
+ self._n_jobs = n_jobs
54
+ self._data_timeframe = base_timeframe
55
+ self.exchange = exchange
56
+ self._external = {}
57
+ self._symbols = []
58
+
59
+ # - copy external data
60
+ for k, v in kwargs.items():
61
+ if isinstance(v, (pd.DataFrame, pd.Series)):
62
+ self._external[k] = v
63
+
64
+ super().__init__({}, exchange)
65
+
66
+ def read(
67
+ self,
68
+ data_id: str,
69
+ start: str | None = None,
70
+ stop: str | None = None,
71
+ transform: DataTransformer = DataTransformer(),
72
+ chunksize=0,
73
+ # timeframe: str | None = None,
74
+ **kwargs,
75
+ ) -> Iterable | List:
76
+ _s_path = data_id
77
+ if not data_id.startswith(self.exchange):
78
+ _s_path = f"{self.exchange}:{data_id}"
79
+ _, symb = _s_path.split(":")
80
+
81
+ _start = str(self._start) if start is None and self._start is not None else start
82
+ _stop = str(self._stop) if stop is None and self._stop is not None else stop
83
+ if _start is None or _stop is None:
84
+ raise ValueError("Start and stop date must be provided")
85
+
86
+ # - refresh symbol's data
87
+ self._handle_symbols_data_from_to([symb], _start, _stop)
88
+
89
+ # - super InMemoryDataFrameReader supports chunked reading now
90
+ return super().read(_s_path, start, stop, transform, chunksize=chunksize, **kwargs)
91
+
92
+ def __getitem__(self, keys) -> dict[str, pd.DataFrame | pd.Series] | pd.DataFrame | pd.Series:
93
+ """
94
+ This helper mostly for using in research notebooks
95
+ """
96
+ _start: str | None = None
97
+ _stop: str | None = None
98
+ _instruments: List[str] = []
99
+ _as_dict = False
100
+
101
+ if isinstance(keys, (tuple)):
102
+ for k in keys:
103
+ if isinstance(k, slice):
104
+ _start, _stop = k.start, k.stop
105
+ if isinstance(k, (list, tuple, set)):
106
+ _instruments = list(k)
107
+ _as_dict = True
108
+ if isinstance(k, str):
109
+ _instruments.append(k)
110
+ else:
111
+ if isinstance(keys, (list, tuple)):
112
+ _instruments.extend(keys)
113
+ _as_dict = True
114
+ elif isinstance(keys, slice):
115
+ _start, _stop = keys.start, keys.stop
116
+ else:
117
+ _instruments.append(keys)
118
+ _as_dict |= len(_instruments) > 1
119
+
120
+ if not _instruments:
121
+ _instruments = list(self._data.keys())
122
+
123
+ if not _instruments:
124
+ raise ValueError("No symbols provided")
125
+
126
+ if (_start is None and self._start is None) or (_stop is None and self._stop is None):
127
+ raise ValueError("Start and stop date must be provided")
128
+
129
+ _start = str(self._start) if _start is None else _start
130
+ _stop = str(self._stop) if _stop is None else _stop
131
+
132
+ _r = self._handle_symbols_data_from_to(_instruments, _start, _stop)
133
+ if not _as_dict and len(_instruments) == 1:
134
+ return _r.get(_instruments[0], pd.DataFrame())
135
+ return _r
136
+
137
+ def _load_candle_data(
138
+ self, symbols: List[str], start: str | pd.Timestamp, stop: str | pd.Timestamp, timeframe: str
139
+ ) -> Dict[str, pd.DataFrame | pd.Series]:
140
+ _ohlcs = defaultdict(list)
141
+ _chunk_size_id_days = 30 * (4 if pd.Timedelta(timeframe) >= pd.Timedelta("1h") else 1)
142
+ _ranges = list(generate_equal_date_ranges(str(start), str(stop), _chunk_size_id_days, "D"))
143
+
144
+ # - for timeframes less than 1d generate_equal_date_ranges may skip days
145
+ # so we need to fix intervals
146
+ _es = list(zip(_ranges[:], _ranges[1:]))
147
+ _es = [(start, end[0]) for (start, _), end in _es]
148
+ _es.append((_ranges[-1][0], str(stop)))
149
+
150
+ _results = ProgressParallel(n_jobs=self._n_jobs, silent=True, total=len(_ranges))(
151
+ delayed(self._reader.get_aux_data)(
152
+ "candles", exchange=self.exchange, symbols=symbols, start=s, stop=e, timeframe=timeframe
153
+ )
154
+ for s, e in _es
155
+ )
156
+ for (s, e), data in zip(_ranges, _results):
157
+ assert isinstance(data, pd.DataFrame)
158
+ try:
159
+ # - some periods of data may be empty so just skipping it to avoid error log
160
+ if not data.empty:
161
+ data_symbols = data.index.get_level_values(1).unique()
162
+ for smb in data_symbols:
163
+ _ohlcs[smb].append(data.loc[pd.IndexSlice[:, smb], :].droplevel(1))
164
+ except Exception as exc:
165
+ logger.warning(f"(InMemoryCachedReader) Failed to load data for {s} - {e} : {str(exc)}")
166
+
167
+ ohlc = {smb.upper(): srows(*vs, keep="first") for smb, vs in _ohlcs.items() if len(vs) > 0}
168
+ return ohlc
169
+
170
+ def _handle_symbols_data_from_to(
171
+ self, symbols: List[str], start: str, stop: str
172
+ ) -> Dict[str, pd.DataFrame | pd.Series]:
173
+ # _dtf = pd.Timedelta(self._data_timeframe)
174
+ # T = lambda x: pd.Timestamp(x).floor(self._data_timeframe)
175
+ def convert_to_timestamp(x):
176
+ return pd.Timestamp(x)
177
+
178
+ _start, _stop = map(convert_to_timestamp, handle_start_stop(start, stop))
179
+
180
+ # - full interval
181
+ _new_symbols = list(set([s for s in symbols if s not in self._data]))
182
+ if _new_symbols:
183
+ _s_req = min(_start, self._start if self._start else _start)
184
+ _e_req = max(_stop, self._stop if self._stop else _stop)
185
+ logger.debug(f"(InMemoryCachedReader) Loading all data {_s_req} - {_e_req} for {','.join(_new_symbols)} ")
186
+ # _new_data = self._load_candle_data(_new_symbols, _s_req, _e_req + _dtf, self._data_timeframe)
187
+ _new_data = self._load_candle_data(_new_symbols, _s_req, _e_req, self._data_timeframe)
188
+ self._data |= _new_data
189
+
190
+ # - pre intervals
191
+ if self._start and _start < self._start:
192
+ _smbs = list(self._data.keys())
193
+ logger.debug(f"(InMemoryCachedReader) Updating {len(_smbs)} symbols pre interval {_start} : {self._start}")
194
+ # _before = self._load_candle_data(_smbs, _start, self._start + _dtf, self._data_timeframe)
195
+ _before = self._load_candle_data(_smbs, _start, self._start, self._data_timeframe)
196
+ for k, c in _before.items():
197
+ # self._data[k] = srows(c, self._data[k], keep="first")
198
+ self._data[k] = srows(c, self._data[k], keep="last")
199
+
200
+ # - post intervals
201
+ if self._stop and _stop > self._stop:
202
+ _smbs = list(self._data.keys())
203
+ logger.debug(f"(InMemoryCachedReader) Updating {len(_smbs)} symbols post interval {self._stop} : {_stop}")
204
+ # _after = self._load_candle_data(_smbs, self._stop - _dtf, _stop, self._data_timeframe)
205
+ _after = self._load_candle_data(_smbs, self._stop, _stop, self._data_timeframe)
206
+ for k, c in _after.items():
207
+ self._data[k] = srows(self._data[k], c, keep="last")
208
+
209
+ self._start = min(_start, self._start if self._start else _start)
210
+ self._stop = max(_stop, self._stop if self._stop else _stop)
211
+ return OhlcDict({s: self._data[s].loc[_start:_stop] for s in symbols if s in self._data})
212
+
213
+ def get_aux_data_ids(self) -> set[str]:
214
+ return self._reader.get_aux_data_ids() | set(self._external.keys())
215
+
216
+ def get_aux_data(self, data_id: str, **kwargs) -> Any:
217
+ _exch = kwargs.pop("exchange") if "exchange" in kwargs else None
218
+ if _exch and _exch != self.exchange:
219
+ raise ValueError(f"Exchange mismatch: expected {self.exchange}, got {_exch}")
220
+
221
+ match data_id:
222
+ # - special case for candles - it builds them from loaded ohlc data
223
+ case "candles":
224
+ return self._get_candles(**kwargs)
225
+
226
+ # - only symbols in cache
227
+ case "symbols":
228
+ return list(self._data.keys())
229
+
230
+ if data_id not in self._external:
231
+ self._external[data_id] = self._reader.get_aux_data(data_id, exchange=self.exchange)
232
+
233
+ _ext_data = self._external.get(data_id)
234
+ if _ext_data is not None:
235
+ _s = kwargs.pop("start") if "start" in kwargs else None
236
+ _e = kwargs.pop("stop") if "stop" in kwargs else None
237
+ _ext_data = _ext_data[:_e] if _e else _ext_data
238
+ _ext_data = _ext_data[_s:] if _s else _ext_data
239
+ return _ext_data
240
+
241
+ def _get_candles(
242
+ self,
243
+ symbols: List[str],
244
+ start: str | pd.Timestamp,
245
+ stop: str | pd.Timestamp,
246
+ timeframe: str = "1d",
247
+ ) -> pd.DataFrame:
248
+ _xd: Dict[str, pd.DataFrame] = self[symbols, start:stop]
249
+ _xd = ohlc_resample(_xd, timeframe) if timeframe else _xd
250
+ _r = [x.assign(symbol=s.upper(), timestamp=x.index) for s, x in _xd.items()]
251
+ return srows(*_r).set_index(["timestamp", "symbol"])
252
+
253
+ def get_names(self, **kwargs) -> list[str]:
254
+ return self._reader.get_names(**kwargs)
255
+
256
+ def get_symbols(self, exchange: str, dtype: str) -> list[str]:
257
+ if not self._symbols:
258
+ self._symbols = self._reader.get_symbols(self.exchange, DataType.OHLC)
259
+ return self._symbols
260
+
261
+ def get_time_ranges(self, symbol: str, dtype: DataType) -> tuple[Any, Any]:
262
+ _id = f"{self.exchange}:{symbol}" if not symbol.startswith(self.exchange) else symbol
263
+ return self._reader.get_time_ranges(_id, dtype)
264
+
265
+ def __str__(self) -> str:
266
+ return f"{self.__class__.__name__}(exchange={self.exchange},timeframe={self._data_timeframe})"
267
+
268
+
269
+ class TimeGuardedWrapper(DataReader):
270
+ # - currently 'known' time, can be used for limiting data
271
+ _time_guard_provider: ITimeProvider
272
+ _reader: InMemoryCachedReader
273
+
274
+ def __init__(
275
+ self,
276
+ reader: InMemoryCachedReader,
277
+ time_guard: ITimeProvider | None = None,
278
+ ) -> None:
279
+ # - if no time provider is provided, use stub
280
+ class _NoTimeGuard(ITimeProvider):
281
+ def time(self) -> np.datetime64 | None:
282
+ return None
283
+
284
+ self._time_guard_provider = time_guard if time_guard is not None else _NoTimeGuard()
285
+ self._reader = reader
286
+
287
+ def read(
288
+ self,
289
+ data_id: str,
290
+ start: str | None = None,
291
+ stop: str | None = None,
292
+ transform: DataTransformer = DataTransformer(),
293
+ chunksize=0,
294
+ # timeframe: str | None = None,
295
+ **kwargs,
296
+ ) -> Iterable | list:
297
+ xs = self._time_guarded_data(
298
+ self._reader.read(data_id, start=start, stop=stop, transform=transform, chunksize=0, **kwargs), # type: ignore
299
+ prev_bar=True,
300
+ )
301
+ return _list_to_chunked_iterator(xs, chunksize) if chunksize > 0 else xs
302
+
303
+ def get_aux_data(self, data_id: str, **kwargs) -> Any:
304
+ return self._time_guarded_data(self._reader.get_aux_data(data_id, **kwargs))
305
+
306
+ def __getitem__(self, keys):
307
+ return self._time_guarded_data(self._reader.__getitem__(keys), prev_bar=True)
308
+
309
+ def _time_guarded_data(
310
+ self, data: pd.DataFrame | pd.Series | dict[str, pd.DataFrame | pd.Series] | list, prev_bar: bool = False
311
+ ) -> pd.DataFrame | pd.Series | Dict[str, pd.DataFrame | pd.Series] | list:
312
+ """
313
+ This function is responsible for limiting the data based on a given time guard.
314
+
315
+ Parameters:
316
+ - data (pd.DataFrame | pd.Series | Dict[str, pd.DataFrame | pd.Series] | List): The data to be limited.
317
+ - prev_bar (bool, optional): If True, the time guard is applied to the previous bar. Defaults to False.
318
+
319
+ Returns:
320
+ - pd.DataFrame | pd.Series | Dict[str, pd.DataFrame | pd.Series] | List: The limited data.
321
+ """
322
+ # - when no any limits - just returns it as is
323
+ if (_c_time := self._time_guard_provider.time()) is None:
324
+ return data
325
+
326
+ def cut_dict(xs, t):
327
+ return OhlcDict({s: v.loc[:t] for s, v in xs.items()})
328
+
329
+ def cut_list_of_timestamped(xs, t):
330
+ return list(filter(lambda x: x.time <= t, xs))
331
+
332
+ def cut_list_raw(xs, t):
333
+ return list(filter(lambda x: x[0] <= t, xs))
334
+
335
+ def cut_time_series(ts, t):
336
+ return ts.loc[: str(t)]
337
+
338
+ if prev_bar:
339
+ _c_time = _c_time - pd.Timedelta(self._reader._data_timeframe)
340
+
341
+ # - input is Dict[str, pd.DataFrame]
342
+ if isinstance(data, dict):
343
+ return cut_dict(data, _c_time)
344
+
345
+ # - input is List[(time, *data)] or List[Quote | Trade | Bar]
346
+ if isinstance(data, list):
347
+ if isinstance(data[0], (list, tuple, np.ndarray)):
348
+ return cut_list_raw(data, _c_time)
349
+ else:
350
+ return cut_list_of_timestamped(data, _c_time.asm8.item())
351
+
352
+ # - input is TimeSeries
353
+ if isinstance(data, TimeSeries):
354
+ return cut_time_series(data, _c_time)
355
+
356
+ return data.loc[:_c_time]
357
+
358
+ def __str__(self) -> str:
359
+ return f"TimeGuarded @ {str(self._reader)}"
360
+
361
+
362
+ __KNOWN_READERS = {
363
+ "mqdb": MultiQdbConnector, # mqdb::xlydian-data
364
+ "multi": MultiQdbConnector,
365
+ "qdb": QuestDBConnector, # questdb::localhost
366
+ "questdb": MultiQdbConnector, # questdb::localhost
367
+ "csv": CsvStorageDataReader, # csv::path_to_storage, csv::c:/ssss/
368
+ }
369
+
370
+
371
+ def loader(
372
+ exchange: str, timeframe: str, *symbols: List[str], source: str = "mqdb::localhost", no_cache=False, **kwargs
373
+ ) -> DataReader:
374
+ """
375
+ Create and initialize an InMemoryCachedReader for a specific exchange and timeframe.
376
+
377
+ This function sets up a cached reader for financial data, optionally pre-loading
378
+ data for specified symbols from the beginning of time until now.
379
+
380
+ Args:
381
+ exchange (str): The name of the exchange to load data from.
382
+ timeframe (str): The time interval for the data (e.g., '1d' for daily, '1h' for hourly).
383
+ *symbols (List[str]): Variable number of symbol names to pre-load data for.
384
+ source (str): The data reader spec and it's parameter to use. Defaults to mqdb::localhost.
385
+ no_cache (bool): If True, data will not be cached. Defaults to False.
386
+
387
+ Returns:
388
+ InMemoryCachedReader: An initialized InMemoryCachedReader object, potentially pre-loaded with data.
389
+
390
+ Examples:
391
+ --------
392
+ >>> ld = loader("BINANCE.UM", '1h', source="mqdb::xlydian-data")
393
+ d = ld["BTCUSDT", "ETHUSDT", "SOLUSDT" , "2020-01-01":"2024-12-01"]
394
+ d('1d').close.plot()
395
+ print(d('1d'))
396
+ d("4h").close.pct_change(fill_method=None).cov()
397
+ """
398
+ if not source:
399
+ raise ValueError("Source parameter must be provided")
400
+
401
+ _rcls_par = source.split("::")
402
+ _c: Type[DataReader] | None = __KNOWN_READERS.get(_rcls_par[0])
403
+ if _c is None:
404
+ raise ValueError(
405
+ f"Unsupported data reader type: {_rcls_par[0]}. Supported names: {', '.join(__KNOWN_READERS.keys())}."
406
+ )
407
+
408
+ reader_object: DataReader = _c(_rcls_par[1]) if len(_rcls_par) else _c()
409
+ inmcr = reader_object
410
+ # - if not need to cache data
411
+ if not no_cache:
412
+ inmcr = InMemoryCachedReader(exchange, reader_object, timeframe, **kwargs)
413
+ if symbols:
414
+ # by default slicing from 1970-01-01 until now
415
+ inmcr[list(symbols), slice("1970-01-01", str(pd.Timestamp("now")))]
416
+ return inmcr