Qubx 0.6.19__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.20__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())
@@ -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():
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
qubx/core/interfaces.py CHANGED
@@ -14,7 +14,7 @@ This module includes:
14
14
 
15
15
  import traceback
16
16
  from dataclasses import dataclass
17
- from typing import Any, Dict, List, Literal, Protocol, Set, Tuple
17
+ from typing import Any, Callable, Dict, List, Literal, Protocol, Set, Tuple
18
18
 
19
19
  import numpy as np
20
20
  import pandas as pd
@@ -1081,6 +1081,7 @@ class IStrategyContext(
1081
1081
  broker: IBroker
1082
1082
  account: IAccountProcessor
1083
1083
  emitter: "IMetricEmitter"
1084
+ health: "IHealthReader"
1084
1085
 
1085
1086
  _strategy_state: StrategyState
1086
1087
 
@@ -1208,6 +1209,214 @@ class PositionsTracker:
1208
1209
  ...
1209
1210
 
1210
1211
 
1212
+ @dataclass
1213
+ class HealthMetrics:
1214
+ """
1215
+ Health metrics for system performance.
1216
+
1217
+ All latency values are in milliseconds.
1218
+ Dropped events are reported as events per second.
1219
+ Queue size is the number of events in the processing queue.
1220
+ """
1221
+
1222
+ queue_size: float = 0.0
1223
+ drop_rate: float = 0.0
1224
+
1225
+ # Arrival latency statistics
1226
+ p50_arrival_latency: float = 0.0
1227
+ p90_arrival_latency: float = 0.0
1228
+ p99_arrival_latency: float = 0.0
1229
+
1230
+ # Queue latency statistics
1231
+ p50_queue_latency: float = 0.0
1232
+ p90_queue_latency: float = 0.0
1233
+ p99_queue_latency: float = 0.0
1234
+
1235
+ # Processing latency statistics
1236
+ p50_processing_latency: float = 0.0
1237
+ p90_processing_latency: float = 0.0
1238
+ p99_processing_latency: float = 0.0
1239
+
1240
+
1241
+ class IHealthWriter(Protocol):
1242
+ """
1243
+ Interface for recording health metrics.
1244
+ """
1245
+
1246
+ def __call__(self, event_type: str) -> "IHealthWriter":
1247
+ """
1248
+ Support for context manager usage with event type.
1249
+
1250
+ Args:
1251
+ event_type: Type of event being timed
1252
+
1253
+ Returns:
1254
+ Self for use in 'with' statement
1255
+ """
1256
+ ...
1257
+
1258
+ def __enter__(self) -> "IHealthWriter":
1259
+ """Enter context for timing measurement"""
1260
+ ...
1261
+
1262
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
1263
+ """Exit context and record timing"""
1264
+ ...
1265
+
1266
+ def record_event_dropped(self, event_type: str) -> None:
1267
+ """
1268
+ Record that an event was dropped.
1269
+
1270
+ Args:
1271
+ event_type: Type of the dropped event
1272
+ """
1273
+ ...
1274
+
1275
+ def record_data_arrival(self, event_type: str, event_time: dt_64) -> None:
1276
+ """
1277
+ Record a data arrival time.
1278
+
1279
+ Args:
1280
+ event_type: Type of event (e.g., "order_execution")
1281
+ """
1282
+ ...
1283
+
1284
+ def record_start_processing(self, event_type: str, event_time: dt_64) -> None:
1285
+ """
1286
+ Record a start processing time.
1287
+ """
1288
+ ...
1289
+
1290
+ def record_end_processing(self, event_type: str, event_time: dt_64) -> None:
1291
+ """
1292
+ Record a end processing time.
1293
+ """
1294
+ ...
1295
+
1296
+ def set_event_queue_size(self, size: int) -> None:
1297
+ """
1298
+ Set the current event queue size.
1299
+
1300
+ Args:
1301
+ size: Current size of the event queue
1302
+ """
1303
+ ...
1304
+
1305
+ def watch(self, scope_name: str = "") -> Callable[[Callable], Callable]:
1306
+ """Decorator function to time a function execution.
1307
+
1308
+ Args:
1309
+ scope_name: Name for the timing scope. If empty string is provided,
1310
+ function's qualified name will be used.
1311
+
1312
+ Returns:
1313
+ Decorator function that times the decorated function.
1314
+ """
1315
+ ...
1316
+
1317
+
1318
+ class IHealthReader(Protocol):
1319
+ """
1320
+ Interface for reading health metrics about system performance.
1321
+ """
1322
+
1323
+ def get_queue_size(self) -> int:
1324
+ """
1325
+ Get the current event queue size.
1326
+
1327
+ Returns:
1328
+ Number of events waiting to be processed
1329
+ """
1330
+ ...
1331
+
1332
+ def get_arrival_latency(self, event_type: str, percentile: float = 90) -> float:
1333
+ """
1334
+ Get latency for a specific event type.
1335
+
1336
+ Args:
1337
+ event_type: Type of event (e.g., "quote", "trade")
1338
+ percentile: Optional percentile (0-100) to retrieve (default: 90)
1339
+
1340
+ Returns:
1341
+ Latency value in milliseconds
1342
+ """
1343
+ ...
1344
+
1345
+ def get_queue_latency(self, event_type: str, percentile: float = 90) -> float:
1346
+ """
1347
+ Get queue latency for a specific event type.
1348
+ """
1349
+ ...
1350
+
1351
+ def get_processing_latency(self, event_type: str, percentile: float = 90) -> float:
1352
+ """
1353
+ Get processing latency for a specific event type.
1354
+ """
1355
+ ...
1356
+
1357
+ def get_latency(self, event_type: str, percentile: float = 90) -> float:
1358
+ """
1359
+ Get end-to-end latency for a specific event type.
1360
+ """
1361
+ ...
1362
+
1363
+ def get_execution_latency(self, scope: str, percentile: float = 90) -> float:
1364
+ """
1365
+ Get execution latency for a specific scope.
1366
+ """
1367
+ ...
1368
+
1369
+ def get_execution_latencies(self) -> dict[str, float]:
1370
+ """
1371
+ Get all execution latencies.
1372
+ """
1373
+ ...
1374
+
1375
+ def get_event_frequency(self, event_type: str) -> float:
1376
+ """
1377
+ Get the events per second for a specific event type.
1378
+
1379
+ Args:
1380
+ event_type: Type of event to get frequency for
1381
+
1382
+ Returns:
1383
+ Events per second
1384
+ """
1385
+ ...
1386
+
1387
+ def get_system_metrics(self) -> HealthMetrics:
1388
+ """
1389
+ Get system-wide metrics.
1390
+
1391
+ Returns:
1392
+ HealthMetrics:
1393
+ - avg_queue_size: Average queue size in the last window
1394
+ - avg_dropped_events: Average number of dropped events per second
1395
+ - p50_arrival_latency: Median arrival latency (ms)
1396
+ - p90_arrival_latency: 90th percentile arrival latency (ms)
1397
+ - p99_arrival_latency: 99th percentile arrival latency (ms)
1398
+ - p50_queue_latency: Median queue latency (ms)
1399
+ - p90_queue_latency: 90th percentile queue latency (ms)
1400
+ - p99_queue_latency: 99th percentile queue latency (ms)
1401
+ - p50_processing_latency: Median processing latency (ms)
1402
+ - p90_processing_latency: 90th percentile processing latency (ms)
1403
+ - p99_processing_latency: 99th percentile processing latency (ms)
1404
+ """
1405
+ ...
1406
+
1407
+
1408
+ class IHealthMonitor(IHealthWriter, IHealthReader):
1409
+ """Interface for health metrics monitoring that combines writing and reading capabilities."""
1410
+
1411
+ def start(self) -> None:
1412
+ """Start the health metrics monitor."""
1413
+ ...
1414
+
1415
+ def stop(self) -> None:
1416
+ """Stop the health metrics monitor."""
1417
+ ...
1418
+
1419
+
1211
1420
  def _unpickle_instance(chain: tuple[type], state: dict):
1212
1421
  """
1213
1422
  chain is a tuple of the *original* classes, e.g. (A, B, C).
@@ -1638,7 +1847,7 @@ class IMetricEmitter:
1638
1847
  class IStrategyLifecycleNotifier:
1639
1848
  """Interface for notifying about strategy lifecycle events."""
1640
1849
 
1641
- def notify_start(self, strategy_name: str, metadata: dict[str, any] | None = None) -> None:
1850
+ def notify_start(self, strategy_name: str, metadata: dict[str, Any] | None = None) -> None:
1642
1851
  """
1643
1852
  Notify that a strategy has started.
1644
1853
 
@@ -1648,7 +1857,7 @@ class IStrategyLifecycleNotifier:
1648
1857
  """
1649
1858
  pass
1650
1859
 
1651
- def notify_stop(self, strategy_name: str, metadata: dict[str, any] | None = None) -> None:
1860
+ def notify_stop(self, strategy_name: str, metadata: dict[str, Any] | None = None) -> None:
1652
1861
  """
1653
1862
  Notify that a strategy has stopped.
1654
1863
 
@@ -1658,7 +1867,7 @@ class IStrategyLifecycleNotifier:
1658
1867
  """
1659
1868
  pass
1660
1869
 
1661
- def notify_error(self, strategy_name: str, error: Exception, metadata: dict[str, any] | None = None) -> None:
1870
+ def notify_error(self, strategy_name: str, error: Exception, metadata: dict[str, Any] | None = None) -> None:
1662
1871
  """
1663
1872
  Notify that a strategy has encountered an error.
1664
1873