Qubx 0.6.19__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.21__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.

@@ -81,6 +81,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
81
81
  open_order_backoff: str = "1Min",
82
82
  max_position_restore_days: int = 30,
83
83
  max_retries: int = 10,
84
+ read_only: bool = False,
84
85
  ):
85
86
  super().__init__(
86
87
  account_id=account_id,
@@ -106,6 +107,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
106
107
  self._required_instruments = set()
107
108
  self._latest_instruments = set()
108
109
  self._subscription_manager = None
110
+ self._read_only = read_only
109
111
 
110
112
  def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
111
113
  self._subscription_manager = manager
@@ -439,9 +441,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
439
441
  for order in orders:
440
442
  logger.debug(f" :: [SYNC] {order.side} {order.quantity} @ {order.price} ({order.status})")
441
443
  else:
442
- # TODO: think if this should actually be here
443
444
  # - we need to cancel the unexpected orders
444
- await self._cancel_unexpected_orders(_open_orders)
445
+ if not self._read_only:
446
+ await self._cancel_unexpected_orders(_open_orders)
445
447
 
446
448
  async def _cancel_unexpected_orders(self, open_orders: dict[str, Order]) -> None:
447
449
  _expected_orders = set(self._active_orders.keys())
@@ -12,6 +12,7 @@ from qubx.core.basics import (
12
12
  CtrlChannel,
13
13
  Instrument,
14
14
  Order,
15
+ OrderSide,
15
16
  )
16
17
  from qubx.core.errors import OrderCancellationError, OrderCreationError, create_error_event
17
18
  from qubx.core.exceptions import BadRequest, InvalidOrderParameters
@@ -63,7 +64,7 @@ class CcxtBroker(IBroker):
63
64
  def send_order_async(
64
65
  self,
65
66
  instrument: Instrument,
66
- order_side: str,
67
+ order_side: OrderSide,
67
68
  order_type: str,
68
69
  amount: float,
69
70
  price: float | None = None,
@@ -127,7 +128,7 @@ class CcxtBroker(IBroker):
127
128
  def send_order(
128
129
  self,
129
130
  instrument: Instrument,
130
- order_side: str,
131
+ order_side: OrderSide,
131
132
  order_type: str,
132
133
  amount: float,
133
134
  price: float | None = None,
@@ -195,7 +196,7 @@ class CcxtBroker(IBroker):
195
196
  async def _create_order(
196
197
  self,
197
198
  instrument: Instrument,
198
- order_side: str,
199
+ order_side: OrderSide,
199
200
  order_type: str,
200
201
  amount: float,
201
202
  price: float | None = None,
@@ -246,13 +247,12 @@ class CcxtBroker(IBroker):
246
247
  logger.error(
247
248
  f"(::_create_order) {order_side} {amount} {order_type} for {instrument.symbol} exception : {err}"
248
249
  )
249
- logger.error(traceback.format_exc())
250
250
  return None, err
251
251
 
252
252
  def _prepare_order_payload(
253
253
  self,
254
254
  instrument: Instrument,
255
- order_side: str,
255
+ order_side: OrderSide,
256
256
  order_type: str,
257
257
  amount: float,
258
258
  price: float | None = None,
@@ -263,11 +263,6 @@ class CcxtBroker(IBroker):
263
263
  params = {}
264
264
  _is_trigger_order = order_type.startswith("stop_")
265
265
 
266
- if order_type == "limit" or _is_trigger_order:
267
- params["timeInForce"] = time_in_force.upper()
268
- if price is None:
269
- raise InvalidOrderParameters(f"Price must be specified for '{order_type}' order")
270
-
271
266
  quote = self.data_provider.get_quote(instrument)
272
267
  if quote is None:
273
268
  logger.warning(f"[<y>{instrument.symbol}</y>] :: Quote is not available for order creation.")
@@ -293,10 +288,27 @@ class CcxtBroker(IBroker):
293
288
  params["type"] = "swap"
294
289
 
295
290
  ccxt_symbol = instrument_to_ccxt_symbol(instrument)
291
+
292
+ if order_type == "limit" or _is_trigger_order:
293
+ time_in_force = time_in_force.upper()
294
+ params["timeInForce"] = time_in_force
295
+ if price is None:
296
+ raise InvalidOrderParameters(f"Price must be specified for '{order_type}' order")
297
+ if order_side == "BUY" and time_in_force == "GTX" and price >= quote.ask:
298
+ logger.info(
299
+ f"[{instrument.symbol}] :: GTX BUY order price {price} is greater than ask price {quote.ask}. Setting 1 tick below ask."
300
+ )
301
+ price = quote.ask - instrument.tick_size
302
+ elif order_side == "SELL" and time_in_force == "GTX" and price <= quote.bid:
303
+ logger.info(
304
+ f"[{instrument.symbol}] :: GTX SELL order price {price} is less than bid price {quote.bid}. Setting 1 tick above bid."
305
+ )
306
+ price = quote.bid + instrument.tick_size
307
+
296
308
  return {
297
309
  "symbol": ccxt_symbol,
298
- "type": order_type,
299
- "side": order_side,
310
+ "type": order_type.lower(),
311
+ "side": order_side.lower(),
300
312
  "amount": amount,
301
313
  "price": price,
302
314
  "params": params,
@@ -336,11 +348,10 @@ class CcxtBroker(IBroker):
336
348
  logger.debug(f"[{order_id}] Could not cancel order: {err}")
337
349
  return False
338
350
  except (ccxt.NetworkError, ccxt.ExchangeError, ccxt.ExchangeNotAvailable) as e:
339
- logger.debug(f"[{order_id}] Network or exchange error while cancelling: {e}")
351
+ logger.warning(f"[{order_id}] Network or exchange error while cancelling: {e}")
340
352
  # Continue with retry logic
341
353
  except Exception as err:
342
354
  logger.error(f"Unexpected error canceling order {order_id}: {err}")
343
- logger.error(traceback.format_exc())
344
355
  return False
345
356
 
346
357
  # Common retry logic for all retryable errors
@@ -20,8 +20,9 @@ from ccxt.pro import Exchange
20
20
  from qubx import logger
21
21
  from qubx.core.basics import CtrlChannel, DataType, Instrument, ITimeProvider, dt_64
22
22
  from qubx.core.helpers import BasicScheduler
23
- from qubx.core.interfaces import IDataProvider
23
+ from qubx.core.interfaces import IDataProvider, IHealthMonitor
24
24
  from qubx.core.series import Bar, Quote
25
+ from qubx.health import DummyHealthMonitor
25
26
  from qubx.utils.misc import AsyncThreadLoop
26
27
 
27
28
  from .exceptions import CcxtLiquidationParsingError, CcxtSymbolNotRecognized
@@ -64,12 +65,14 @@ class CcxtDataProvider(IDataProvider):
64
65
  channel: CtrlChannel,
65
66
  max_ws_retries: int = 10,
66
67
  warmup_timeout: int = 120,
68
+ health_monitor: IHealthMonitor | None = None,
67
69
  ):
68
70
  self._exchange_id = str(exchange.name)
69
71
  self.time_provider = time_provider
70
72
  self.channel = channel
71
73
  self.max_ws_retries = max_ws_retries
72
74
  self._warmup_timeout = warmup_timeout
75
+ self._health_monitor = health_monitor or DummyHealthMonitor()
73
76
 
74
77
  # - create new even loop
75
78
  self._exchange = exchange
@@ -416,11 +419,13 @@ class CcxtDataProvider(IDataProvider):
416
419
  instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
417
420
  for _, ohlcvs in _data.items():
418
421
  for oh in ohlcvs:
422
+ timestamp_ns = oh[0] * 1_000_000
423
+ self._health_monitor.record_data_arrival(sub_type, dt_64(timestamp_ns, "ns"))
419
424
  channel.send(
420
425
  (
421
426
  instrument,
422
427
  sub_type,
423
- Bar(oh[0] * 1_000_000, oh[1], oh[2], oh[3], oh[4], oh[6], oh[7]),
428
+ Bar(timestamp_ns, oh[1], oh[2], oh[3], oh[4], oh[6], oh[7]),
424
429
  False, # not historical bar
425
430
  )
426
431
  )
@@ -463,7 +468,9 @@ class CcxtDataProvider(IDataProvider):
463
468
  exch_symbol = trades[0]["symbol"]
464
469
  instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
465
470
  for trade in trades:
466
- channel.send((instrument, sub_type, ccxt_convert_trade(trade), False))
471
+ converted_trade = ccxt_convert_trade(trade)
472
+ self._health_monitor.record_data_arrival(sub_type, dt_64(converted_trade.time, "ns"))
473
+ channel.send((instrument, sub_type, converted_trade, False))
467
474
 
468
475
  async def un_watch_trades(instruments: list[Instrument]):
469
476
  symbols = [_instr_to_ccxt_symbol[i] for i in instruments]
@@ -497,6 +504,7 @@ class CcxtDataProvider(IDataProvider):
497
504
  ob = ccxt_convert_orderbook(ccxt_ob, instrument, levels=depth, tick_size_pct=tick_size_pct)
498
505
  if ob is None:
499
506
  return
507
+ self._health_monitor.record_data_arrival(sub_type, dt_64(ob.time, "ns"))
500
508
  quote = ob.to_quote()
501
509
  self._last_quotes[instrument] = quote
502
510
  channel.send((instrument, sub_type, ob, False))
@@ -529,6 +537,7 @@ class CcxtDataProvider(IDataProvider):
529
537
  for exch_symbol, ccxt_ticker in ccxt_tickers.items(): # type: ignore
530
538
  instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
531
539
  quote = ccxt_convert_ticker(ccxt_ticker)
540
+ self._health_monitor.record_data_arrival(sub_type, dt_64(quote.time, "ns"))
532
541
  self._last_quotes[instrument] = quote
533
542
  channel.send((instrument, sub_type, quote, False))
534
543
 
@@ -562,8 +571,11 @@ class CcxtDataProvider(IDataProvider):
562
571
  liquidations = await self._exchange.watch_liquidations_for_symbols(symbols)
563
572
  for liquidation in liquidations:
564
573
  try:
565
- instrument = ccxt_find_instrument(liquidation["symbol"], self._exchange, _symbol_to_instrument)
566
- channel.send((instrument, sub_type, ccxt_convert_liquidation(liquidation), False))
574
+ exch_symbol = liquidation["symbol"]
575
+ instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
576
+ liquidation_event = ccxt_convert_liquidation(liquidation)
577
+ self._health_monitor.record_data_arrival(sub_type, dt_64(liquidation_event.time, "ns"))
578
+ channel.send((instrument, sub_type, liquidation_event, False))
567
579
  except CcxtLiquidationParsingError:
568
580
  logger.debug(f"Could not parse liquidation {liquidation}")
569
581
  continue
@@ -593,12 +605,17 @@ class CcxtDataProvider(IDataProvider):
593
605
  async def watch_funding_rates():
594
606
  funding_rates = await self._exchange.watch_funding_rates() # type: ignore
595
607
  instrument_to_funding_rate = {}
608
+ current_time = self.time_provider.time()
609
+
596
610
  for symbol, info in funding_rates.items():
597
611
  try:
598
612
  instrument = ccxt_find_instrument(symbol, self._exchange)
599
- instrument_to_funding_rate[instrument] = ccxt_convert_funding_rate(info)
613
+ funding_rate = ccxt_convert_funding_rate(info)
614
+ instrument_to_funding_rate[instrument] = funding_rate
615
+ self._health_monitor.record_data_arrival(sub_type, dt_64(current_time, "s"))
600
616
  except CcxtSymbolNotRecognized:
601
617
  continue
618
+
602
619
  channel.send((None, sub_type, instrument_to_funding_rate, False))
603
620
 
604
621
  async def un_watch_funding_rates():
@@ -2,7 +2,7 @@ from typing import Any
2
2
 
3
3
  from qubx import logger
4
4
  from qubx.connectors.ccxt.broker import CcxtBroker
5
- from qubx.core.basics import Instrument
5
+ from qubx.core.basics import Instrument, OrderSide
6
6
  from qubx.core.exceptions import BadRequest
7
7
 
8
8
 
@@ -21,7 +21,7 @@ class BinanceCcxtBroker(CcxtBroker):
21
21
  def _prepare_order_payload(
22
22
  self,
23
23
  instrument: Instrument,
24
- order_side: str,
24
+ order_side: OrderSide,
25
25
  order_type: str,
26
26
  amount: float,
27
27
  price: float | None = None,
@@ -42,8 +42,8 @@ class BinanceCcxtBroker(CcxtBroker):
42
42
  raise BadRequest(f"Quote is not available for price match for {instrument.symbol}")
43
43
 
44
44
  if time_in_force == "gtx" and price is not None and self.enable_price_match:
45
- if (order_side == "buy" and quote.bid - price < self.price_match_ticks * instrument.tick_size) or (
46
- order_side == "sell" and price - quote.ask < self.price_match_ticks * instrument.tick_size
45
+ if (order_side == "BUY" and quote.bid - price < self.price_match_ticks * instrument.tick_size) or (
46
+ order_side == "SELL" and price - quote.ask < self.price_match_ticks * instrument.tick_size
47
47
  ):
48
48
  params["priceMatch"] = "QUEUE"
49
49
  logger.debug(f"[<y>{instrument.symbol}</y>] :: Price match is set to QUEUE. Price will be ignored.")
qubx/core/context.py CHANGED
@@ -4,7 +4,6 @@ from typing import Any, Callable
4
4
 
5
5
  from qubx import logger
6
6
  from qubx.core.basics import (
7
- SW,
8
7
  AssetBalance,
9
8
  CtrlChannel,
10
9
  DataType,
@@ -14,6 +13,7 @@ from qubx.core.basics import (
14
13
  Order,
15
14
  OrderRequest,
16
15
  Position,
16
+ Timestamped,
17
17
  dt_64,
18
18
  )
19
19
  from qubx.core.exceptions import StrategyExceededMaxNumberOfRuntimeFailuresError
@@ -27,6 +27,7 @@ from qubx.core.interfaces import (
27
27
  IAccountProcessor,
28
28
  IBroker,
29
29
  IDataProvider,
30
+ IHealthMonitor,
30
31
  IMarketManager,
31
32
  IMetricEmitter,
32
33
  IPositionGathering,
@@ -45,6 +46,7 @@ from qubx.core.interfaces import (
45
46
  from qubx.core.loggers import StrategyLogging
46
47
  from qubx.data.readers import DataReader
47
48
  from qubx.gathering.simplest import SimplePositionGatherer
49
+ from qubx.health import DummyHealthMonitor
48
50
  from qubx.trackers.sizers import FixedSizer
49
51
 
50
52
  from .mixins import (
@@ -102,6 +104,7 @@ class StrategyContext(IStrategyContext):
102
104
  initializer: BasicStrategyInitializer | None = None,
103
105
  strategy_name: str | None = None,
104
106
  strategy_state: StrategyState | None = None,
107
+ health_monitor: IHealthMonitor | None = None,
105
108
  ) -> None:
106
109
  self.account = account
107
110
  self.strategy = self.__instantiate_strategy(strategy, config)
@@ -127,6 +130,9 @@ class StrategyContext(IStrategyContext):
127
130
  self._strategy_state = strategy_state if strategy_state is not None else StrategyState()
128
131
  self._strategy_name = strategy_name if strategy_name is not None else strategy.__class__.__name__
129
132
 
133
+ self._health_monitor = health_monitor or DummyHealthMonitor()
134
+ self.health = self._health_monitor
135
+
130
136
  __position_tracker = self.strategy.tracker(self)
131
137
  if __position_tracker is None:
132
138
  __position_tracker = StrategyContext.DEFAULT_POSITION_TRACKER()
@@ -180,6 +186,7 @@ class StrategyContext(IStrategyContext):
180
186
  scheduler=self._scheduler,
181
187
  is_simulation=self._data_provider.is_simulation,
182
188
  exporter=self._exporter,
189
+ health_monitor=self._health_monitor,
183
190
  )
184
191
  self.__post_init__()
185
192
 
@@ -245,6 +252,9 @@ class StrategyContext(IStrategyContext):
245
252
  # - start account processing
246
253
  self.account.start()
247
254
 
255
+ # - start health metrics monitor
256
+ self._health_monitor.start()
257
+
248
258
  # - update universe with initial instruments after the strategy is initialized
249
259
  self.set_universe(self._initial_instruments, skip_callback=True)
250
260
 
@@ -299,6 +309,9 @@ class StrategyContext(IStrategyContext):
299
309
  # - stop account processing
300
310
  self.account.stop()
301
311
 
312
+ # - stop health metrics monitor
313
+ self._health_monitor.stop()
314
+
302
315
  # - close logging
303
316
  self._logging.close()
304
317
 
@@ -513,22 +526,30 @@ class StrategyContext(IStrategyContext):
513
526
  def __process_incoming_data_loop(self, channel: CtrlChannel):
514
527
  logger.info("[StrategyContext] :: Start processing market data")
515
528
  while channel.control.is_set():
516
- with SW("StrategyContext._process_incoming_data"):
517
- try:
518
- # - waiting for incoming market data
519
- instrument, d_type, data, hist = channel.receive()
520
- if self.process_data(instrument, d_type, data, hist):
521
- channel.stop()
522
- break
523
- except StrategyExceededMaxNumberOfRuntimeFailuresError:
529
+ try:
530
+ # - waiting for incoming market data
531
+ instrument, d_type, data, hist = channel.receive()
532
+
533
+ _should_record = isinstance(data, Timestamped) and not hist
534
+ if _should_record:
535
+ self._health_monitor.record_start_processing(d_type, dt_64(data.time, "ns"))
536
+
537
+ if self.process_data(instrument, d_type, data, hist):
524
538
  channel.stop()
525
539
  break
526
- except Exception as e:
527
- logger.error(f"Error processing market data: {e}")
528
- logger.opt(colors=False).error(traceback.format_exc())
529
- if self._lifecycle_notifier:
530
- self._lifecycle_notifier.notify_error(self._strategy_name, e)
531
- # Don't stop the channel here, let it continue processing
540
+
541
+ if _should_record:
542
+ self._health_monitor.record_end_processing(d_type, dt_64(data.time, "ns"))
543
+
544
+ except StrategyExceededMaxNumberOfRuntimeFailuresError:
545
+ channel.stop()
546
+ break
547
+ except Exception as e:
548
+ logger.error(f"Error processing market data: {e}")
549
+ logger.opt(colors=False).error(traceback.format_exc())
550
+ if self._lifecycle_notifier:
551
+ self._lifecycle_notifier.notify_error(self._strategy_name, e)
552
+ # Don't stop the channel here, let it continue processing
532
553
  logger.info("[StrategyContext] :: Market data processing stopped")
533
554
 
534
555
  def __instantiate_strategy(self, strategy: IStrategy, config: dict[str, Any] | None) -> IStrategy:
qubx/core/deque.py ADDED
@@ -0,0 +1,182 @@
1
+ """
2
+ Fast, fixed-size circular buffer implementation using Numba for performance.
3
+ When the deque is full, pushing new elements overwrites the oldest elements.
4
+ """
5
+
6
+ import numpy as np
7
+ from numba import from_dtype, int32, types
8
+ from numba.experimental import jitclass
9
+
10
+
11
+ def create_deque_class(element_dtype: np.dtype):
12
+ """
13
+ Dynamically create and return a Deque jitclass that stores elements of the given NumPy dtype.
14
+
15
+ This is a fast, fixed-size circular buffer implementation using Numba for performance.
16
+ When the deque is full, pushing new elements overwrites the oldest elements.
17
+
18
+ Args:
19
+ element_dtype (np.dtype): The NumPy dtype for elements to store
20
+
21
+ Returns:
22
+ jitclass: A compiled Numba class for the deque
23
+
24
+ Raises:
25
+ ValueError: If the element_dtype is not supported
26
+ """
27
+ # Determine whether it's a structured dtype or a scalar dtype
28
+ if element_dtype.fields is not None:
29
+ # It's a structured dtype. Convert to a Numba 'record' type
30
+ element_type = from_dtype(element_dtype)
31
+ data_type = element_type[:] # 1D array of that struct
32
+ else:
33
+ # It's a scalar dtype (e.g., float32)
34
+ if element_dtype == np.float32:
35
+ data_type = types.float32[:]
36
+ elif element_dtype == np.float64:
37
+ data_type = types.float64[:]
38
+ elif element_dtype == np.int32:
39
+ data_type = types.int32[:]
40
+ elif element_dtype == np.int64:
41
+ data_type = types.int64[:]
42
+ else:
43
+ raise ValueError(f"Unsupported scalar dtype: {element_dtype}")
44
+
45
+ # Build the class spec
46
+ spec = [
47
+ ("data", data_type),
48
+ ("capacity", int32),
49
+ ("head", int32),
50
+ ("tail", int32),
51
+ ("size", int32),
52
+ ]
53
+
54
+ class Deque:
55
+ def __init__(self, capacity):
56
+ self.data = np.empty(capacity, dtype=element_dtype)
57
+ self.capacity = capacity
58
+ self.head = 0
59
+ self.tail = 0
60
+ self.size = 0
61
+
62
+ def push_back(self, record):
63
+ if self.size == self.capacity:
64
+ # Overwrite oldest from the front
65
+ self.head = (self.head + 1) % self.capacity
66
+ self.size -= 1
67
+
68
+ self.data[self.tail] = record
69
+ self.tail = (self.tail + 1) % self.capacity
70
+ self.size += 1
71
+
72
+ def push_front(self, record):
73
+ if self.size == self.capacity:
74
+ # Overwrite oldest from the back
75
+ self.tail = (self.tail - 1) % self.capacity
76
+ self.size -= 1
77
+
78
+ self.head = (self.head - 1) % self.capacity
79
+ self.data[self.head] = record
80
+ self.size += 1
81
+
82
+ def pop_front(self):
83
+ if self.size == 0:
84
+ raise IndexError("Deque is empty")
85
+ record = self.data[self.head]
86
+ self.head = (self.head + 1) % self.capacity
87
+ self.size -= 1
88
+ return record
89
+
90
+ def pop_back(self):
91
+ if self.size == 0:
92
+ raise IndexError("Deque is empty")
93
+ self.tail = (self.tail - 1) % self.capacity
94
+ record = self.data[self.tail]
95
+ self.size -= 1
96
+ return record
97
+
98
+ def is_empty(self):
99
+ return self.size == 0
100
+
101
+ def is_full(self):
102
+ return self.size == self.capacity
103
+
104
+ def get_size(self):
105
+ return self.size
106
+
107
+ def __len__(self):
108
+ return self.size
109
+
110
+ def front(self):
111
+ if self.size == 0:
112
+ raise IndexError("Deque is empty")
113
+ return self.data[self.head]
114
+
115
+ def back(self):
116
+ if self.size == 0:
117
+ raise IndexError("Deque is empty")
118
+ return self.data[(self.tail - 1) % self.capacity]
119
+
120
+ def __getitem__(self, idx):
121
+ if idx < 0 or idx >= self.size:
122
+ raise IndexError("Index out of bounds")
123
+ return self.data[(self.tail - idx - 1) % self.capacity]
124
+
125
+ def to_array(self):
126
+ """Return a NumPy array of the current elements in the Deque, from oldest to newest."""
127
+ out = np.empty(self.size, dtype=self.data.dtype)
128
+ for i in range(self.size):
129
+ idx = (self.head + i) % self.capacity
130
+ out[i] = self.data[idx]
131
+ return out
132
+
133
+ def push_back_fields(self): ...
134
+
135
+ # Generate push_back_fields(...) for structured dtypes
136
+ if element_dtype.fields is not None:
137
+ field_names = list(element_dtype.fields.keys())
138
+ arg_list = ", ".join(field_names)
139
+ lines = []
140
+ lines.append(f"def push_back_fields(self, {arg_list}):")
141
+ lines.append(" if self.size == self.capacity:")
142
+ lines.append(" self.head = (self.head + 1) % self.capacity")
143
+ lines.append(" self.size -= 1")
144
+ lines.append("")
145
+ for f in field_names:
146
+ lines.append(f" self.data[self.tail]['{f}'] = {f}")
147
+ lines.append(" self.tail = (self.tail + 1) % self.capacity")
148
+ lines.append(" self.size += 1")
149
+
150
+ method_src = "\n".join(lines)
151
+ tmp_ns = {}
152
+ exec(method_src, {}, tmp_ns)
153
+ push_back_fields_func = tmp_ns["push_back_fields"]
154
+ setattr(Deque, "push_back_fields", push_back_fields_func)
155
+
156
+ _Deque = jitclass(spec)(Deque)
157
+ return _Deque
158
+
159
+
160
+ # Pre-compiled deque types for common use cases
161
+ DequeFloat64 = create_deque_class(np.dtype(np.float64))
162
+ DequeFloat32 = create_deque_class(np.dtype(np.float32))
163
+ DequeInt64 = create_deque_class(np.dtype(np.int64))
164
+ DequeInt32 = create_deque_class(np.dtype(np.int32))
165
+
166
+ # Deque type for storing indicator values with timestamps
167
+ DequeIndicator = create_deque_class(
168
+ np.dtype(
169
+ [
170
+ ("timestamp", np.int64),
171
+ ("value", np.float64),
172
+ ],
173
+ align=True,
174
+ )
175
+ )
176
+
177
+ # Instance types for use in other jitclasses
178
+ DequeIndicator_instance = DequeIndicator.class_type.instance_type
179
+ DequeFloat64_instance = DequeFloat64.class_type.instance_type
180
+ DequeFloat32_instance = DequeFloat32.class_type.instance_type
181
+ DequeInt64_instance = DequeInt64.class_type.instance_type
182
+ DequeInt32_instance = DequeInt32.class_type.instance_type