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
@@ -0,0 +1,558 @@
1
+ import math
2
+ from collections import defaultdict, deque
3
+ from typing import Any, Iterator, TypeAlias
4
+
5
+ import pandas as pd
6
+
7
+ from qubx import logger
8
+ from qubx.core.basics import DataType, Instrument, Timestamped
9
+ from qubx.core.exceptions import SimulationError
10
+ from qubx.data.readers import (
11
+ AsDict,
12
+ AsQuotes,
13
+ AsTrades,
14
+ DataReader,
15
+ DataTransformer,
16
+ RestoredBarsFromOHLC,
17
+ RestoreQuotesFromOHLC,
18
+ RestoreTradesFromOHLC,
19
+ )
20
+
21
+ SlicerOutData: TypeAlias = tuple[str, int, Timestamped] | tuple
22
+
23
+
24
+ class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
25
+ """
26
+ This class manages seamless iteration over multiple time-series data streams,
27
+ ensuring that events are processed in the correct chronological order regardless of their source.
28
+ It supports adding / removing new data streams to the slicer on the fly (during the itration).
29
+ """
30
+
31
+ _iterators: dict[str, Iterator[list[Timestamped]]]
32
+ _buffers: dict[str, list[Timestamped]]
33
+ _keys: deque[str]
34
+ _iterating: bool
35
+
36
+ def __init__(self):
37
+ self._buffers = defaultdict(list)
38
+ self._iterators = {}
39
+ self._keys = deque()
40
+ self._iterating = False
41
+
42
+ def put(self, data: dict[str, Iterator[list[Timestamped]]]):
43
+ _rebuild = False
44
+ for k, vi in data.items():
45
+ if k not in self._keys:
46
+ self._iterators[k] = vi
47
+ self._buffers[k] = self._load_next_chunk_to_buffer(k) # do initial chunk fetching
48
+ self._keys.append(k)
49
+ _rebuild = True
50
+
51
+ # - rebuild strategy
52
+ if _rebuild and self._iterating:
53
+ self._build_initial_iteration_seq()
54
+
55
+ def __add__(self, data: dict[str, Iterator]) -> "IteratedDataStreamsSlicer":
56
+ self.put(data)
57
+ return self
58
+
59
+ def remove(self, keys: list[str] | str):
60
+ """
61
+ Remove data iterator and associated keys from the queue.
62
+ If the key is not found, it does nothing.
63
+ """
64
+ _keys = keys if isinstance(keys, list) else [keys]
65
+ _rebuild = False
66
+ for i in _keys:
67
+ if i in self._buffers:
68
+ self._buffers.pop(i)
69
+ self._iterators.pop(i)
70
+ self._keys.remove(i)
71
+ _rebuild = True
72
+
73
+ # - rebuild strategy
74
+ if _rebuild and self._iterating:
75
+ self._build_initial_iteration_seq()
76
+
77
+ def __iter__(self) -> Iterator:
78
+ self._build_initial_iteration_seq()
79
+ self._iterating = True
80
+ return self
81
+
82
+ def _build_initial_iteration_seq(self):
83
+ _init_seq = {k: self._buffers[k][-1].time for k in self._keys}
84
+ _init_seq = dict(sorted(_init_seq.items(), key=lambda item: item[1]))
85
+ self._keys = deque(_init_seq.keys())
86
+
87
+ def _load_next_chunk_to_buffer(self, index: str) -> list[Timestamped]:
88
+ return list(reversed(next(self._iterators[index])))
89
+
90
+ def _remove_iterator(self, key: str):
91
+ self._buffers.pop(key)
92
+ self._iterators.pop(key)
93
+ self._keys.remove(key)
94
+
95
+ def _pop_top(self, k: str) -> Timestamped:
96
+ """
97
+ Removes and returns the most recent timestamped data element from the buffer associated with the given key.
98
+ If the buffer is empty after popping, it attempts to load the next chunk of data into the buffer.
99
+ If no more data is available, the iterator associated with the key is removed.
100
+
101
+ Parameters:
102
+ k (str): The key identifying the data stream buffer to pop from.
103
+
104
+ Returns:
105
+ Timestamped: The most recent timestamped data element from the buffer.
106
+ """
107
+ v = (data := self._buffers[k]).pop()
108
+ if not data:
109
+ try:
110
+ data.extend(self._load_next_chunk_to_buffer(k)) # - get next chunk of data
111
+ except StopIteration:
112
+ self._remove_iterator(k) # - remove iterable data
113
+ return v
114
+
115
+ def fetch_before_time(self, key: str, time_ns: int) -> list[Timestamped]:
116
+ """
117
+ Fetches and returns all timestamped data elements from the buffer associated with the given key
118
+ that have a timestamp earlier than the specified time.
119
+
120
+ Parameters:
121
+ - key (str): The key identifying the data stream buffer to fetch from.
122
+ - time_ns (int): The timestamp in nanoseconds. All returned elements will have a timestamp less than this value.
123
+
124
+ Returns:
125
+ - list[Timestamped]: A list of timestamped data elements that occur before the specified time.
126
+ """
127
+ values = []
128
+ data = self._buffers[key]
129
+ if not data:
130
+ try:
131
+ data.extend(self._load_next_chunk_to_buffer(key)) # - get next chunk of data
132
+ except StopIteration:
133
+ self._remove_iterator(key)
134
+
135
+ # pull most past elements
136
+ v = data[-1]
137
+ while v.time < time_ns:
138
+ values.append(data.pop())
139
+ if not data:
140
+ try:
141
+ data.extend(self._load_next_chunk_to_buffer(key)) # - get next chunk of data
142
+ except StopIteration:
143
+ self._remove_iterator(key)
144
+ break
145
+ v = data[-1]
146
+
147
+ return values
148
+
149
+ def __next__(self) -> SlicerOutData:
150
+ """
151
+ Advances the iterator to the next available timestamped data element across all data streams.
152
+
153
+ Returns:
154
+ - SlicerOutData: A tuple containing the key of the data stream, the timestamp of the data element, and the data element itself.
155
+
156
+ Raises:
157
+ - StopIteration: If there are no more data elements to iterate over.
158
+ """
159
+ if not self._keys:
160
+ self._iterating = False
161
+ raise StopIteration
162
+
163
+ _min_t = math.inf
164
+ _min_k = self._keys[0]
165
+ for i in self._keys:
166
+ _x = self._buffers[i][-1]
167
+ if _x.time < _min_t:
168
+ _min_t = _x.time
169
+ _min_k = i
170
+
171
+ _v = self._pop_top(_min_k)
172
+ return (_min_k, _v.time, _v)
173
+
174
+
175
+ class DataFetcher:
176
+ _fetcher_id: str
177
+ _reader: DataReader
178
+ _requested_data_type: str
179
+ _producing_data_type: str
180
+ _params: dict[str, object]
181
+ _specs: list[str]
182
+
183
+ _transformer: DataTransformer
184
+ _timeframe: str | None = None
185
+ _warmup_period: pd.Timedelta | None = None
186
+ _chunksize: int = 5000
187
+
188
+ def __init__(
189
+ self,
190
+ fetcher_id: str,
191
+ reader: DataReader,
192
+ subtype: str,
193
+ params: dict[str, Any],
194
+ warmup_period: pd.Timedelta | None = None,
195
+ chunksize: int = 5000,
196
+ open_close_time_indent_secs=1.0, # open/close shift may depends on simulation
197
+ ) -> None:
198
+ self._fetcher_id = fetcher_id
199
+ self._params = params
200
+ self._reader = reader
201
+
202
+ match subtype:
203
+ case DataType.OHLC_QUOTES:
204
+ # - requested restore quotes from OHLC
205
+ self._transformer = RestoreQuotesFromOHLC(open_close_time_shift_secs=open_close_time_indent_secs)
206
+ self._requested_data_type = "ohlc"
207
+ self._producing_data_type = "quote"
208
+ if "timeframe" in params:
209
+ self._timeframe = params.get("timeframe", "1Min")
210
+
211
+ case DataType.OHLC_TRADES:
212
+ # - requested restore trades from OHLC
213
+ self._transformer = RestoreTradesFromOHLC(open_close_time_shift_secs=open_close_time_indent_secs)
214
+ self._requested_data_type = "ohlc"
215
+ self._producing_data_type = "trade"
216
+ if "timeframe" in params:
217
+ self._timeframe = params.get("timeframe", "1Min")
218
+
219
+ case DataType.OHLC:
220
+ # - requested restore bars from OHLC
221
+ self._transformer = RestoredBarsFromOHLC(open_close_time_shift_secs=open_close_time_indent_secs)
222
+ self._requested_data_type = "ohlc"
223
+ self._producing_data_type = "ohlc"
224
+ if "timeframe" in params:
225
+ self._timeframe = params.get("timeframe", "1Min")
226
+
227
+ case DataType.TRADE:
228
+ self._requested_data_type = "trade"
229
+ self._producing_data_type = "trade"
230
+ self._transformer = AsTrades()
231
+
232
+ case DataType.QUOTE:
233
+ self._requested_data_type = "orderbook"
234
+ self._producing_data_type = "quote" # ???
235
+ self._transformer = AsQuotes()
236
+
237
+ case _:
238
+ self._requested_data_type = subtype
239
+ self._producing_data_type = subtype
240
+ self._transformer = AsDict()
241
+
242
+ self._warmup_period = warmup_period
243
+ self._warmed = {}
244
+ self._specs = []
245
+ self._chunksize = chunksize
246
+
247
+ @staticmethod
248
+ def _make_request_id(instrument: Instrument) -> str:
249
+ return f"{instrument.exchange}:{instrument.symbol}"
250
+
251
+ def attach_instrument(self, instrument: Instrument) -> str:
252
+ _data_id = self._make_request_id(instrument)
253
+
254
+ if _data_id not in self._specs:
255
+ self._specs.append(_data_id)
256
+ self._warmed[_data_id] = False
257
+
258
+ return self._fetcher_id + "." + _data_id
259
+
260
+ def remove_instrument(self, instrument: Instrument) -> str:
261
+ _data_id = self._make_request_id(instrument)
262
+
263
+ if _data_id in self._specs:
264
+ self._specs.remove(_data_id)
265
+ del self._warmed[_data_id]
266
+
267
+ return self._fetcher_id + "." + _data_id
268
+
269
+ def has_instrument(self, instrument: Instrument) -> bool:
270
+ return self._make_request_id(instrument) in self._specs
271
+
272
+ def get_instruments_indices(self) -> list[str]:
273
+ return [self._fetcher_id + "." + i for i in self._specs]
274
+
275
+ def get_instrument_index(self, instrument: Instrument) -> str:
276
+ return self._fetcher_id + "." + self._make_request_id(instrument)
277
+
278
+ def load(
279
+ self, start: str | pd.Timestamp, end: str | pd.Timestamp, to_load: list[Instrument] | None
280
+ ) -> dict[str, Iterator]:
281
+ """
282
+ Loads data for specified instruments within a given time range.
283
+
284
+ Parameters:
285
+ - start (str | pd.Timestamp): The start time for data loading, can be a string or a pandas Timestamp.
286
+ - end (str | pd.Timestamp): The end time for data loading, can be a string or a pandas Timestamp.
287
+ - to_load (list[Instrument] | None): A list of instruments to load data for. If None, data for all subscribed instruments is loaded.
288
+
289
+ Returns:
290
+ - dict[str, Iterator]: A dictionary where keys are instrument identifiers and values are iterators over the loaded data.
291
+ """
292
+ _requests = self._specs if not to_load else set(self._make_request_id(i) for i in to_load)
293
+ _r_iters = {}
294
+
295
+ for _r in _requests: # - TODO: replace this loop with multi-instrument request after DataReader refactoring
296
+ if _r in self._specs:
297
+ _start = pd.Timestamp(start)
298
+ if self._warmup_period and not self._warmed.get(_r):
299
+ _start -= self._warmup_period
300
+ self._warmed[_r] = True
301
+
302
+ _args = dict(
303
+ data_id=_r,
304
+ start=_start,
305
+ stop=end,
306
+ transform=self._transformer,
307
+ data_type=self._requested_data_type,
308
+ chunksize=self._chunksize,
309
+ )
310
+
311
+ if self._timeframe:
312
+ _args["timeframe"] = self._timeframe
313
+
314
+ try:
315
+ _r_iters[self._fetcher_id + "." + _r] = self._reader.read(**_args) # type: ignore
316
+ except Exception as e:
317
+ logger.error(f">>> (DataFetcher::load) - failed to load <g>'{self._fetcher_id}'</g> data: {e}")
318
+ else:
319
+ raise IndexError(
320
+ f"Instrument {_r} is not subscribed for this data {self._requested_data_type} in {self._fetcher_id} !"
321
+ )
322
+
323
+ return _r_iters
324
+
325
+ def __repr__(self) -> str:
326
+ return f"{self._requested_data_type}({self._params}) (-{self._warmup_period if self._warmup_period else '--'}) [{','.join(self._specs)}] :-> {self._transformer.__class__.__name__}"
327
+
328
+
329
+ class IterableSimulationData(Iterator):
330
+ """
331
+ This class is a crucial component for backtesting system.
332
+ It provides a flexible and efficient way to simulate market data feeds for strategy testing.
333
+
334
+ Key Features:
335
+ - Supports multiple data types (OHLC, trades, quotes) and instruments.
336
+ - Allows for dynamic addition and removal of instruments during simulation.
337
+ - Handles warmup periods for data preloading.
338
+ - Manages historical and current data distinction during iteration.
339
+ - Utilizes a data slicer (IteratedDataStreamsSlicer) to merge and order data from multiple sources.
340
+
341
+ TODO:
342
+ 1. think how to provide initial "market quote" for each instrument
343
+ 2. optimization for historical data (return bunch of history instead of each bar in next(...))
344
+ """
345
+
346
+ _readers: dict[str, DataReader]
347
+ _subtyped_fetchers: dict[str, DataFetcher]
348
+ _warmups: dict[str, pd.Timedelta]
349
+ _instruments: dict[str, tuple[Instrument, DataFetcher, DataType]]
350
+ _open_close_time_indent_secs: int | float
351
+
352
+ _slicer_ctrl: IteratedDataStreamsSlicer | None = None
353
+ _slicing_iterator: Iterator | None = None
354
+ _start: pd.Timestamp | None = None
355
+ _stop: pd.Timestamp | None = None
356
+ _current_time: int | None = None
357
+
358
+ def __init__(
359
+ self,
360
+ readers: dict[str, DataReader],
361
+ open_close_time_indent_secs=1, # open/close ticks shift
362
+ ):
363
+ self._readers = dict(readers)
364
+ self._instruments = {}
365
+ self._subtyped_fetchers = {}
366
+ self._warmups = {}
367
+ self._open_close_time_indent_secs = open_close_time_indent_secs
368
+
369
+ def set_typed_reader(self, data_type: str, reader: DataReader):
370
+ self._readers[data_type] = reader
371
+ if _fetcher := self._subtyped_fetchers.get(data_type):
372
+ _fetcher._reader = reader
373
+
374
+ def set_warmup_period(self, subscription: str, warmup_period: str | None = None):
375
+ if warmup_period:
376
+ _access_key, _, _ = self._parse_subscription_spec(subscription)
377
+ self._warmups[_access_key] = pd.Timedelta(warmup_period)
378
+
379
+ def _parse_subscription_spec(self, subscription: str) -> tuple[str, str, dict[str, object]]:
380
+ _subtype, _params = DataType.from_str(subscription)
381
+ match _subtype:
382
+ case DataType.OHLC | DataType.OHLC_QUOTES:
383
+ _timeframe = _params.get("timeframe", "1Min")
384
+ _access_key = f"{_subtype}.{_timeframe}"
385
+ case DataType.TRADE | DataType.QUOTE:
386
+ _access_key = f"{_subtype}"
387
+ case _:
388
+ # - any arbitrary data type is passed as is
389
+ _params = {}
390
+ _subtype = subscription
391
+ _access_key = f"{_subtype}"
392
+ return _access_key, _subtype, _params
393
+
394
+ def add_instruments_for_subscription(self, subscription: str, instruments: list[Instrument] | Instrument):
395
+ instruments = instruments if isinstance(instruments, list) else [instruments]
396
+ _subt_key, _data_type, _params = self._parse_subscription_spec(subscription)
397
+
398
+ fetcher = self._subtyped_fetchers.get(_subt_key)
399
+ if not fetcher:
400
+ _reader = self._readers.get(_data_type)
401
+
402
+ if _reader is None:
403
+ raise SimulationError(f"No reader configured for data type: {_data_type}")
404
+
405
+ self._subtyped_fetchers[_subt_key] = (
406
+ fetcher := DataFetcher(
407
+ _subt_key,
408
+ _reader,
409
+ _data_type,
410
+ _params,
411
+ warmup_period=self._warmups.get(_subt_key),
412
+ open_close_time_indent_secs=self._open_close_time_indent_secs,
413
+ )
414
+ )
415
+
416
+ _instrs_to_preload = []
417
+ for i in instruments:
418
+ if not fetcher.has_instrument(i):
419
+ idx = fetcher.attach_instrument(i)
420
+ self._instruments[idx] = (i, fetcher, subscription) # type: ignore
421
+ _instrs_to_preload.append(i)
422
+
423
+ if self.is_running and _instrs_to_preload:
424
+ self._slicer_ctrl += fetcher.load(
425
+ pd.Timestamp(self._current_time, unit="ns"), # type: ignore
426
+ self._stop, # type: ignore
427
+ _instrs_to_preload,
428
+ )
429
+
430
+ def peek_historical_data(self, instrument: Instrument, subscription: str) -> list[Timestamped]:
431
+ """
432
+ Retrieves historical data for a specified instrument and subscription type up to the current simulation time.
433
+
434
+ Parameters:
435
+ - instrument (Instrument): instrument for which historical data is requested.
436
+ - subscription (str): type of data subscription (e.g., OHLC, trades, quotes) for the instrument.
437
+
438
+ Returns:
439
+ - list[Timestamped]: A list of historical data elements for the specified instrument and subscription type
440
+ that occurred before the current simulation time. If the simulation is not running, returns an empty list.
441
+
442
+ Raises:
443
+ SimulationError: If the instrument does not have the specified subscription in the simulation data provider.
444
+ """
445
+ if not self.has_subscription(instrument, subscription):
446
+ raise SimulationError(
447
+ f"Instrument: {instrument} has no subscription: {subscription} in this simulation data provider"
448
+ )
449
+
450
+ if not self.is_running:
451
+ return []
452
+
453
+ _subt_key, _, _ = self._parse_subscription_spec(subscription)
454
+ _i_key = self._subtyped_fetchers[_subt_key].get_instrument_index(instrument)
455
+
456
+ assert self._slicer_ctrl is not None and self._current_time is not None
457
+
458
+ # fetch historical data for current time
459
+ return self._slicer_ctrl.fetch_before_time(_i_key, self._current_time)
460
+
461
+ def get_instruments_for_subscription(self, subscription: str) -> list[Instrument]:
462
+ if subscription == DataType.ALL:
463
+ return list((i for i, *_ in self._instruments.values()))
464
+
465
+ _subt_key, _, _ = self._parse_subscription_spec(subscription)
466
+ if (fetcher := self._subtyped_fetchers.get(_subt_key)) is not None:
467
+ return [self._instruments[k][0] for k in fetcher.get_instruments_indices()]
468
+
469
+ return []
470
+
471
+ def get_subscriptions_for_instrument(self, instrument: Instrument | None) -> list[str]:
472
+ r = []
473
+ for i, f, s in self._instruments.values():
474
+ if instrument is not None:
475
+ if i == instrument:
476
+ r.append(s)
477
+ else:
478
+ r.append(s)
479
+ return list(set(r))
480
+
481
+ def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
482
+ for i, f, s in self._instruments.values():
483
+ if i == instrument and s == subscription_type:
484
+ return True
485
+ return False
486
+
487
+ def remove_instruments_from_subscription(self, subscription: str, instruments: list[Instrument] | Instrument):
488
+ def _remove_from_fetcher(_subt_key: str, instruments: list[Instrument]):
489
+ fetcher = self._subtyped_fetchers.get(_subt_key)
490
+ if not fetcher:
491
+ logger.warning(f"No configured data fetcher for '{_subt_key}' subscription !")
492
+ return
493
+
494
+ _keys_to_remove = []
495
+ for i in instruments:
496
+ # - try to remove from data fetcher
497
+ if idx := fetcher.remove_instrument(i):
498
+ if idx in self._instruments:
499
+ self._instruments.pop(idx)
500
+ _keys_to_remove.append(idx)
501
+
502
+ # print("REMOVING FROM:", _keys_to_remove)
503
+ if self.is_running and _keys_to_remove:
504
+ self._slicer_ctrl.remove(_keys_to_remove) # type: ignore
505
+
506
+ instruments = instruments if isinstance(instruments, list) else [instruments]
507
+
508
+ # - if we want to remove instruments from all subscriptions
509
+ if subscription == DataType.ALL:
510
+ _f_keys = list(self._subtyped_fetchers.keys())
511
+ for s in _f_keys:
512
+ _remove_from_fetcher(s, instruments)
513
+ return
514
+
515
+ _subt_key, _, _ = self._parse_subscription_spec(subscription)
516
+ _remove_from_fetcher(_subt_key, instruments)
517
+
518
+ @property
519
+ def is_running(self) -> bool:
520
+ return self._current_time is not None
521
+
522
+ def create_iterable(self, start: str | pd.Timestamp, stop: str | pd.Timestamp) -> Iterator:
523
+ self._start = pd.Timestamp(start)
524
+ self._stop = pd.Timestamp(stop)
525
+ self._current_time = None
526
+ self._slicer_ctrl = IteratedDataStreamsSlicer()
527
+ return self
528
+
529
+ def __iter__(self) -> Iterator:
530
+ assert self._start is not None
531
+ self._current_time = int(pd.Timestamp(self._start).timestamp() * 1e9)
532
+ _ct_timestap = pd.Timestamp(self._current_time, unit="ns")
533
+
534
+ for f in self._subtyped_fetchers.values():
535
+ logger.debug(
536
+ f" [<c>IteratedDataStreamsSlicer</c>] :: Preloading initial data for {f._fetcher_id} {self._start} : {self._stop} ..."
537
+ )
538
+ self._slicer_ctrl += f.load(_ct_timestap, self._stop, None) # type: ignore
539
+
540
+ self._slicing_iterator = iter(self._slicer_ctrl)
541
+ return self
542
+
543
+ def __next__(self) -> tuple[Instrument, str, Timestamped, bool]: # type: ignore
544
+ try:
545
+ while data := next(self._slicing_iterator): # type: ignore
546
+ k, t, v = data
547
+ instr, fetcher, subt = self._instruments[k]
548
+ data_type = fetcher._producing_data_type
549
+ _is_historical = False
550
+ if t < self._current_time: # type: ignore
551
+ _is_historical = True
552
+ else:
553
+ # only update the current time if the event is not historical
554
+ self._current_time = t
555
+
556
+ return instr, data_type, v, _is_historical
557
+ except StopIteration as e: # noqa: F841
558
+ raise StopIteration