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

@@ -14,7 +14,7 @@ from qubx.core.basics import (
14
14
  Order,
15
15
  OrderSide,
16
16
  )
17
- from qubx.core.errors import OrderCancellationError, OrderCreationError, create_error_event
17
+ from qubx.core.errors import ErrorLevel, OrderCancellationError, OrderCreationError, create_error_event
18
18
  from qubx.core.exceptions import BadRequest, InvalidOrderParameters
19
19
  from qubx.core.interfaces import (
20
20
  IAccountProcessor,
@@ -61,6 +61,57 @@ class CcxtBroker(IBroker):
61
61
  def is_simulated_trading(self) -> bool:
62
62
  return False
63
63
 
64
+ def _post_order_error_to_databus(
65
+ self,
66
+ error: Exception,
67
+ instrument: Instrument,
68
+ order_side: OrderSide,
69
+ order_type: str,
70
+ amount: float,
71
+ price: float | None,
72
+ client_id: str | None,
73
+ time_in_force: str,
74
+ **options,
75
+ ):
76
+ level = ErrorLevel.LOW
77
+ match error:
78
+ case ccxt.InsufficientFunds():
79
+ level = ErrorLevel.HIGH
80
+ logger.error(
81
+ f"(::create_order) INSUFFICIENT FUNDS for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
82
+ )
83
+ case ccxt.OrderNotFillable():
84
+ level = ErrorLevel.LOW
85
+ logger.error(
86
+ f"(::create_order) ORDER NOT FILLEABLE for {order_side} {amount} {order_type} for [{instrument.symbol}] : {error}"
87
+ )
88
+ case ccxt.InvalidOrder():
89
+ level = ErrorLevel.LOW
90
+ logger.error(
91
+ f"(::create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
92
+ )
93
+ case ccxt.BadRequest():
94
+ level = ErrorLevel.LOW
95
+ logger.error(
96
+ f"(::create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
97
+ )
98
+ case _:
99
+ level = ErrorLevel.MEDIUM
100
+ logger.error(f"(::create_order) Unexpected error: {error}")
101
+
102
+ error_event = OrderCreationError(
103
+ timestamp=self.time_provider.time(),
104
+ message=f"Error message: {str(error)}",
105
+ level=level,
106
+ instrument=instrument,
107
+ amount=amount,
108
+ price=price,
109
+ order_type=order_type,
110
+ side=order_side,
111
+ error=error,
112
+ )
113
+ self.channel.send(create_error_event(error_event))
114
+
64
115
  def send_order_async(
65
116
  self,
66
117
  instrument: Instrument,
@@ -93,33 +144,20 @@ class CcxtBroker(IBroker):
93
144
  )
94
145
 
95
146
  if error:
96
- # Create and send an error event through the channel
97
- error_event = OrderCreationError(
98
- timestamp=self.time_provider.time(),
99
- message=str(error),
100
- instrument=instrument,
101
- amount=amount,
102
- price=price,
103
- order_type=order_type,
104
- side=order_side,
147
+ self._post_order_error_to_databus(
148
+ error, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
105
149
  )
106
- self.channel.send(create_error_event(error_event))
107
- return None
150
+ order = None
151
+
108
152
  return order
153
+
109
154
  except Exception as err:
110
155
  # Catch any unexpected errors and send them through the channel as well
111
- logger.error(f"Unexpected error in async order creation: {err}")
156
+ logger.error(f"{self.__class__.__name__} :: Unexpected error in async order creation: {err}")
112
157
  logger.error(traceback.format_exc())
113
- error_event = OrderCreationError(
114
- timestamp=self.time_provider.time(),
115
- message=f"Unexpected error: {str(err)}",
116
- instrument=instrument,
117
- amount=amount,
118
- price=price,
119
- order_type=order_type,
120
- side=order_side,
158
+ self._post_order_error_to_databus(
159
+ err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
121
160
  )
122
- self.channel.send(create_error_event(error_event))
123
161
  return None
124
162
 
125
163
  # Submit the task to the async loop
@@ -135,7 +173,7 @@ class CcxtBroker(IBroker):
135
173
  client_id: str | None = None,
136
174
  time_in_force: str = "gtc",
137
175
  **options,
138
- ) -> Order:
176
+ ) -> Order | None:
139
177
  """
140
178
  Submit an order and wait for the result. Exceptions will be raised on errors.
141
179
 
@@ -169,14 +207,16 @@ class CcxtBroker(IBroker):
169
207
 
170
208
  # If there was no error but also no order, something went wrong
171
209
  if not order and not self.enable_create_order_ws:
172
- raise ExchangeError("Order creation failed with no specific error")
210
+ raise ExchangeError(f"{self.__class__.__name__} :: Order creation failed with no specific error")
173
211
 
174
212
  return order
175
213
 
176
214
  except Exception as err:
177
215
  # This will catch any errors from future.result() or if we explicitly raise an error
178
- logger.error(f"Error in send_order: {err}")
179
- raise
216
+ self._post_order_error_to_databus(
217
+ err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
218
+ )
219
+ return None
180
220
 
181
221
  def cancel_order(self, order_id: str) -> Order | None:
182
222
  orders = self.account.get_orders()
@@ -231,25 +271,7 @@ class CcxtBroker(IBroker):
231
271
  logger.info(f"New order {order}")
232
272
  return order, None
233
273
 
234
- except ccxt.OrderNotFillable as exc:
235
- logger.error(
236
- f"(::_create_order) [{instrument.symbol}] ORDER NOT FILLEABLE for {order_side} {amount} {order_type} : {exc}"
237
- )
238
- return None, exc
239
- except ccxt.InvalidOrder as exc:
240
- logger.error(
241
- f"(::_create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {exc}"
242
- )
243
- return None, exc
244
- except ccxt.BadRequest as exc:
245
- logger.error(
246
- f"(::_create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {exc}"
247
- )
248
- return None, exc
249
274
  except Exception as err:
250
- logger.error(
251
- f"(::_create_order) {order_side} {amount} {order_type} for {instrument.symbol} exception : {err}"
252
- )
253
275
  return None, err
254
276
 
255
277
  def _prepare_order_payload(
@@ -371,6 +393,8 @@ class CcxtBroker(IBroker):
371
393
  order_id=order_id,
372
394
  message=f"Timeout reached for canceling order {order_id}",
373
395
  instrument=instrument,
396
+ level=ErrorLevel.LOW,
397
+ error=None,
374
398
  )
375
399
  )
376
400
  )
@@ -25,7 +25,7 @@ EXCHANGE_ALIASES = {
25
25
 
26
26
  CUSTOM_BROKERS = {
27
27
  "binance": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
28
- "binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
28
+ "binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
29
29
  "binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
30
30
  "binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
31
31
  "bitfinex.f": partial(CcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
@@ -3,7 +3,7 @@ from typing import Dict, List
3
3
  import ccxt.pro as cxp
4
4
  from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp
5
5
  from ccxt.async_support.base.ws.client import Client
6
- from ccxt.base.errors import ArgumentsRequired, BadRequest, NotSupported
6
+ from ccxt.base.errors import ArgumentsRequired, BadRequest, InsufficientFunds, NotSupported
7
7
  from ccxt.base.precise import Precise
8
8
  from ccxt.base.types import (
9
9
  Any,
@@ -34,7 +34,12 @@ class BinanceQV(cxp.binance):
34
34
  "name": "aggTrade",
35
35
  },
36
36
  "localOrderBookLimit": 10_000, # set a large limit to avoid cutting off the orderbook
37
- }
37
+ },
38
+ "exceptions": {
39
+ "exact": {
40
+ "-2019": InsufficientFunds, # ccxt doesn't have this code for some weird reason !!
41
+ },
42
+ },
38
43
  },
39
44
  )
40
45
 
@@ -78,6 +78,10 @@ class BitfinexF(cxp.bitfinex):
78
78
  # GTX is not supported by bitfinex, so we need to convert it to PO
79
79
  params["timeInForce"] = "PO"
80
80
  params["postOnly"] = True
81
+
82
+ if "lev" not in params:
83
+ params["lev"] = 2
84
+
81
85
  response = await super().create_order(symbol, type, side, amount, price, params)
82
86
  return response
83
87
 
@@ -90,6 +94,9 @@ class BitfinexF(cxp.bitfinex):
90
94
  params["timeInForce"] = "PO"
91
95
  params["postOnly"] = True
92
96
 
97
+ if "lev" not in params:
98
+ params["lev"] = 2
99
+
93
100
  await self.load_markets()
94
101
  market = self.market(symbol)
95
102
  request = self.create_order_request(symbol, type, side, amount, price, params)
@@ -98,14 +105,22 @@ class BitfinexF(cxp.bitfinex):
98
105
  # request["cid"] = request["newClientOrderId"]
99
106
  # del request["newClientOrderId"]
100
107
 
101
- await self.bfx.wss.inputs.submit_order(
102
- type=request["type"],
103
- symbol=request["symbol"],
104
- amount=float(request["amount"]),
105
- price=float(request["price"]),
106
- flags=request["flags"],
107
- # cid=int(request["cid"]),
108
- )
108
+ _params = {
109
+ "type": request["type"],
110
+ "symbol": request["symbol"],
111
+ "amount": float(request["amount"]),
112
+ "lev": request["lev"],
113
+ }
114
+
115
+ if "price" in request:
116
+ _params["price"] = float(request["price"])
117
+ else:
118
+ _params["price"] = None
119
+
120
+ if "flags" in request:
121
+ _params["flags"] = request["flags"]
122
+
123
+ await self.bfx.wss.inputs.submit_order(**_params)
109
124
  return self.safe_order({"info": {}}, market) # type: ignore
110
125
 
111
126
  async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order | None:
@@ -54,7 +54,7 @@ def ccxt_convert_order_info(instrument: Instrument, raw: dict[str, Any]) -> Orde
54
54
  type=_type,
55
55
  instrument=instrument,
56
56
  time=pd.Timestamp(raw["timestamp"], unit="ms"), # type: ignore
57
- quantity=amnt,
57
+ quantity=abs(amnt) * (-1 if side == "SELL" else 1),
58
58
  price=float(price) if price is not None else 0.0,
59
59
  side=side,
60
60
  status=status.upper(),
@@ -157,7 +157,7 @@ def ccxt_convert_positions(
157
157
  )
158
158
  pos = Position(
159
159
  instrument=instr,
160
- quantity=info["contracts"] * (-1 if info["side"] == "short" else 1),
160
+ quantity=abs(info["contracts"]) * (-1 if info["side"] == "short" else 1),
161
161
  pos_average_price=info["entryPrice"],
162
162
  )
163
163
  if info.get("markPrice", None) is not None:
@@ -454,7 +454,7 @@ class TardisDataProvider(IDataProvider):
454
454
 
455
455
  # Record data arrival for health monitoring
456
456
  tardis_type = data["type"]
457
- tardis_name = data["name"]
457
+ tardis_name = data["name"] if "name" in data else ""
458
458
  qubx_type = self._map_tardis_type_to_data_type(tardis_type)
459
459
  if qubx_type:
460
460
  self._health_monitor.record_data_arrival(qubx_type, dt_64(msg_time, "ns"))
qubx/core/context.py CHANGED
@@ -16,6 +16,7 @@ from qubx.core.basics import (
16
16
  Timestamped,
17
17
  dt_64,
18
18
  )
19
+ from qubx.core.errors import BaseErrorEvent, ErrorLevel
19
20
  from qubx.core.exceptions import StrategyExceededMaxNumberOfRuntimeFailuresError
20
21
  from qubx.core.helpers import (
21
22
  BasicScheduler,
@@ -286,7 +287,7 @@ class StrategyContext(IStrategyContext):
286
287
 
287
288
  # - invoke strategy's stop code
288
289
  try:
289
- if not self._strategy_state.is_warmup_in_progress:
290
+ if not self.is_warmup_in_progress:
290
291
  self.strategy.on_stop(self)
291
292
  except Exception as strat_error:
292
293
  logger.error(
@@ -327,7 +328,7 @@ class StrategyContext(IStrategyContext):
327
328
  return self._data_providers[0].is_simulation
328
329
 
329
330
  @property
330
- def is_simulated_trading(self) -> bool:
331
+ def is_paper_trading(self) -> bool:
331
332
  return self._brokers[0].is_simulated_trading
332
333
 
333
334
  # IAccountViewer delegation
@@ -536,6 +537,16 @@ class StrategyContext(IStrategyContext):
536
537
  if _should_record:
537
538
  self._health_monitor.record_start_processing(d_type, dt_64(data.time, "ns"))
538
539
 
540
+ # - notify error if error level is medium or higher
541
+ if (
542
+ self._lifecycle_notifier
543
+ and isinstance(data, BaseErrorEvent)
544
+ and data.level.value >= ErrorLevel.MEDIUM.value
545
+ ):
546
+ self._lifecycle_notifier.notify_error(
547
+ self._strategy_name, data.error or Exception("Unknown error"), {"message": str(data)}
548
+ )
549
+
539
550
  if self.process_data(instrument, d_type, data, hist):
540
551
  channel.stop()
541
552
  break
qubx/core/errors.py CHANGED
@@ -3,14 +3,27 @@ Error types that are sent through the event channel.
3
3
  """
4
4
 
5
5
  from dataclasses import dataclass
6
+ from enum import Enum
6
7
 
7
8
  from qubx.core.basics import Instrument, dt_64
8
9
 
9
10
 
11
+ class ErrorLevel(Enum):
12
+ LOW = 1 # continue trading
13
+ MEDIUM = 2 # send notifications and continue trading
14
+ HIGH = 3 # send notification and cancel orders and close positions
15
+ CRITICAL = 4 # send notification and shutdown strategy
16
+
17
+
10
18
  @dataclass
11
19
  class BaseErrorEvent:
12
20
  timestamp: dt_64
13
21
  message: str
22
+ level: ErrorLevel
23
+ error: Exception | None
24
+
25
+ def __str__(self):
26
+ return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error}"
14
27
 
15
28
 
16
29
  def create_error_event(error: BaseErrorEvent) -> tuple[None, str, BaseErrorEvent, bool]:
@@ -25,8 +38,14 @@ class OrderCreationError(BaseErrorEvent):
25
38
  order_type: str
26
39
  side: str
27
40
 
41
+ def __str__(self):
42
+ return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error} ||| Order creation error for {self.order_type} {self.side} {self.instrument} {self.amount}"
43
+
28
44
 
29
45
  @dataclass
30
46
  class OrderCancellationError(BaseErrorEvent):
31
47
  instrument: Instrument
32
48
  order_id: str
49
+
50
+ def __str__(self):
51
+ return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error} ||| Order cancellation error for {self.order_id} {self.instrument}"
qubx/core/interfaces.py CHANGED
@@ -1068,7 +1068,6 @@ class IStrategyContext(
1068
1068
  IProcessingManager,
1069
1069
  IAccountViewer,
1070
1070
  IWarmupStateSaver,
1071
- StrategyState,
1072
1071
  ):
1073
1072
  strategy: "IStrategy"
1074
1073
  initializer: "IStrategyInitializer"
@@ -1086,17 +1085,27 @@ class IStrategyContext(
1086
1085
  """Stop the strategy context."""
1087
1086
  pass
1088
1087
 
1088
+ @property
1089
+ def state(self) -> StrategyState:
1090
+ """Get the strategy state."""
1091
+ return StrategyState(**self._strategy_state.__dict__)
1092
+
1089
1093
  def is_running(self) -> bool:
1090
1094
  """Check if the strategy context is running."""
1091
1095
  return False
1092
1096
 
1097
+ @property
1098
+ def is_warmup_in_progress(self) -> bool:
1099
+ """Check if the warmup is in progress."""
1100
+ return self._strategy_state.is_warmup_in_progress
1101
+
1093
1102
  @property
1094
1103
  def is_simulation(self) -> bool:
1095
1104
  """Check if the strategy context is running in simulation mode."""
1096
1105
  return False
1097
1106
 
1098
1107
  @property
1099
- def is_simulated_trading(self) -> bool:
1108
+ def is_paper_trading(self) -> bool:
1100
1109
  """Check if the strategy context is running in simulated trading mode."""
1101
1110
  return False
1102
1111
 
qubx/core/metrics.py CHANGED
@@ -717,7 +717,7 @@ class TradingSessionResult:
717
717
  "name": self.name,
718
718
  "start": pd.Timestamp(self.start).isoformat(),
719
719
  "stop": pd.Timestamp(self.stop).isoformat(),
720
- "exchange": self.exchanges,
720
+ "exchanges": self.exchanges,
721
721
  "capital": self.capital,
722
722
  "base_currency": self.base_currency,
723
723
  "commissions": self.commissions,
@@ -824,6 +824,12 @@ class TradingSessionResult:
824
824
  info = self.info()
825
825
  if description:
826
826
  info["description"] = description
827
+ # - set name if not specified
828
+ if info.get("name") is None:
829
+ info["name"] = name
830
+
831
+ # - add numpy array representer
832
+ yaml.SafeDumper.add_representer(np.ndarray, lambda dumper, data: dumper.represent_list(data.tolist()))
827
833
  yaml.safe_dump(info, f, sort_keys=False, indent=4)
828
834
 
829
835
  # - save logs
@@ -855,15 +861,31 @@ class TradingSessionResult:
855
861
 
856
862
  with zipfile.ZipFile(path, "r") as zip_ref:
857
863
  info = yaml.safe_load(zip_ref.read("info.yml"))
858
- portfolio = pd.read_csv(zip_ref.open("portfolio.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
859
- executions = pd.read_csv(zip_ref.open("executions.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
860
- signals = pd.read_csv(zip_ref.open("signals.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
864
+ try:
865
+ portfolio = pd.read_csv(
866
+ zip_ref.open("portfolio.csv"), index_col=["timestamp"], parse_dates=["timestamp"]
867
+ )
868
+ except:
869
+ portfolio = pd.DataFrame()
870
+ try:
871
+ executions = pd.read_csv(
872
+ zip_ref.open("executions.csv"), index_col=["timestamp"], parse_dates=["timestamp"]
873
+ )
874
+ except:
875
+ executions = pd.DataFrame()
876
+ try:
877
+ signals = pd.read_csv(zip_ref.open("signals.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
878
+ except:
879
+ signals = pd.DataFrame()
861
880
 
862
881
  # load result
863
882
  _qbx_version = info.pop("qubx_version")
864
883
  _decr = info.pop("description", None)
865
884
  _perf = info.pop("performance", None)
866
885
  info["instruments"] = info.pop("symbols")
886
+ # - fix for old versions
887
+ _exch = info.pop("exchange")
888
+ info["exchanges"] = _exch if isinstance(_exch, list) else [_exch]
867
889
  tsr = TradingSessionResult(**info, portfolio_log=portfolio, executions_log=executions, signals_log=signals)
868
890
  tsr.qubx_version = _qbx_version
869
891
  tsr._metrics = _perf
qubx/data/hft.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import queue
2
- from collections import defaultdict, deque
2
+ from collections import defaultdict
3
3
  from multiprocessing import Event, Process, Queue
4
4
  from pathlib import Path
5
5
  from threading import Thread
6
- from typing import Any, Iterable, Optional, TypeAlias, TypeVar, Union
6
+ from typing import Any, Iterable, Optional, TypeAlias, TypeVar
7
7
 
8
8
  import numpy as np
9
9
  import pandas as pd
@@ -148,7 +148,7 @@ class HftChunkPrefetcher:
148
148
  enable_trade="trade" in queues,
149
149
  enable_orderbook="orderbook" in queues,
150
150
  )
151
- ctx = reader._get_or_create_context(data_id, start, stop)
151
+ ctx = reader._get_or_create_context(data_id, start.floor("d"), stop)
152
152
  instrument = reader._data_id_to_instrument[data_id]
153
153
 
154
154
  # Initialize buffers only for enabled data types
@@ -205,9 +205,11 @@ class HftChunkPrefetcher:
205
205
  ]
206
206
  )
207
207
  reader._create_buffer_if_needed("quotes", instrument, (quote_chunksize,), quote_dtype)
208
+ start_time_ns = start.value
209
+ stop_time_ns = stop.value
208
210
 
209
211
  while not stop_event.is_set():
210
- reader._next_batch(
212
+ stop_reached = reader._next_batch(
211
213
  ctx=ctx,
212
214
  instrument=instrument,
213
215
  chunksize=chunk_args["chunksize"],
@@ -216,6 +218,8 @@ class HftChunkPrefetcher:
216
218
  orderbook_period=orderbook_period,
217
219
  tick_size_pct=chunk_args["tick_size_pct"],
218
220
  depth=chunk_args["depth"],
221
+ start_time=start_time_ns,
222
+ stop_time=stop_time_ns,
219
223
  )
220
224
 
221
225
  # Get records for enabled data types
@@ -270,6 +274,9 @@ class HftChunkPrefetcher:
270
274
  for data_type in queues:
271
275
  reader._mark_processed(data_type, instrument)
272
276
 
277
+ if stop_reached:
278
+ break
279
+
273
280
  except Exception as e:
274
281
  error_queue.put(e)
275
282
  for queue in queues.values():
@@ -450,7 +457,9 @@ class HftDataReader(DataReader):
450
457
  ):
451
458
  raise ValueError(f"Data type {data_type} is not enabled")
452
459
 
453
- _start, _stop = handle_start_stop(start, stop, lambda x: pd.Timestamp(x).floor("d"))
460
+ # - handle start and stop
461
+ _start_raw, _stop = handle_start_stop(start, stop, lambda x: pd.Timestamp(x))
462
+ _start = _start_raw.floor("d") # we must to start from day's start
454
463
  assert isinstance(_start, pd.Timestamp) and isinstance(_stop, pd.Timestamp)
455
464
 
456
465
  # Check if we need to recreate the prefetcher
@@ -489,7 +498,7 @@ class HftDataReader(DataReader):
489
498
  "orderbook_interval": self.orderbook_interval,
490
499
  "trade_capacity": self.trade_capacity,
491
500
  }
492
- prefetcher.start(self.path, data_id, _start, _stop, chunk_args)
501
+ prefetcher.start(self.path, data_id, _start_raw, _stop, chunk_args)
493
502
  logger.debug(f"Started prefetcher for {data_id}")
494
503
  self._prefetchers[data_id] = prefetcher
495
504
  self._prefetcher_ranges[data_id] = (_start, _stop)
@@ -620,7 +629,9 @@ class HftDataReader(DataReader):
620
629
  orderbook_period: int,
621
630
  tick_size_pct: float,
622
631
  depth: int,
623
- ) -> None:
632
+ start_time: int,
633
+ stop_time: int,
634
+ ) -> bool:
624
635
  match data_type:
625
636
  case "quote":
626
637
  if self._instrument_to_quote_index[instrument] > 0 or not self.enable_quote:
@@ -636,8 +647,11 @@ class HftDataReader(DataReader):
636
647
  quote_index,
637
648
  trade_index,
638
649
  orderbook_index,
650
+ stop_reached,
639
651
  ) = _simulate_hft(
640
652
  ctx=ctx,
653
+ start_time=start_time,
654
+ stop_time=stop_time,
641
655
  ob_timestamp=self._instrument_to_name_to_buffer["ob_timestamp"][instrument],
642
656
  bid_price_buffer=self._instrument_to_name_to_buffer["bid_price"][instrument],
643
657
  ask_price_buffer=self._instrument_to_name_to_buffer["ask_price"][instrument],
@@ -660,6 +674,8 @@ class HftDataReader(DataReader):
660
674
  self._instrument_to_trade_index[instrument] = trade_index if self.enable_trade else 0
661
675
  self._instrument_to_orderbook_index[instrument] = orderbook_index if self.enable_orderbook else 0
662
676
 
677
+ return stop_reached
678
+
663
679
  def _create_backtest_assets(self, files: list[str], instrument: Instrument) -> list[BacktestAsset]:
664
680
  mid_price = _get_initial_mid_price(files)
665
681
  roi_lb, roi_ub = mid_price / 4, mid_price * 4
@@ -760,6 +776,8 @@ def _simulate_hft(
760
776
  trade_buffer: np.ndarray,
761
777
  quote_buffer: np.ndarray,
762
778
  batch_size: int,
779
+ start_time: int,
780
+ stop_time: int,
763
781
  interval: int = 1_000_000_000,
764
782
  orderbook_period: int = 1,
765
783
  tick_size_pct: float = 0.0,
@@ -767,12 +785,17 @@ def _simulate_hft(
767
785
  enable_quote: bool = True,
768
786
  enable_trade: bool = True,
769
787
  enable_orderbook: bool = True,
770
- ) -> tuple[int, int, int]:
788
+ ) -> tuple[int, int, int, bool]:
771
789
  orderbook_index = 0
772
790
  quote_index = 0
773
791
  trade_index = 0
792
+ stop_reached = False
774
793
 
775
794
  while ctx.elapse(interval) == 0 and orderbook_index < batch_size:
795
+ # - skip if we are before the start time
796
+ if ctx.current_timestamp < start_time:
797
+ continue
798
+
776
799
  depth = ctx.depth(0)
777
800
 
778
801
  # record quote
@@ -824,4 +847,9 @@ def _simulate_hft(
824
847
  ctx.clear_last_trades(0)
825
848
  quote_index += 1
826
849
 
827
- return quote_index, trade_index, orderbook_index
850
+ # - stop if we reached the stop time
851
+ if ctx.current_timestamp >= stop_time:
852
+ stop_reached = True
853
+ break
854
+
855
+ return quote_index, trade_index, orderbook_index, stop_reached
qubx/data/readers.py CHANGED
@@ -3,7 +3,7 @@ import os
3
3
  import re
4
4
  from functools import wraps
5
5
  from os.path import exists, join
6
- from typing import Any, Iterable, Iterator, List, Set, Union
6
+ from typing import Any, Iterable, Iterator
7
7
 
8
8
  import numpy as np
9
9
  import pandas as pd
@@ -33,7 +33,7 @@ STOCK_DAILY_SESSION = (convert_timedelta_to_numpy("9:30:00.100"), convert_timede
33
33
  CME_FUTURES_DAILY_SESSION = (convert_timedelta_to_numpy("8:30:00.100"), convert_timedelta_to_numpy("15:14:59.900"))
34
34
 
35
35
 
36
- def _recognize_t(t: Union[int, str], defaultvalue, timeunit) -> int:
36
+ def _recognize_t(t: int | str, defaultvalue, timeunit) -> int:
37
37
  if isinstance(t, (str, pd.Timestamp)):
38
38
  try:
39
39
  return np.datetime64(t, timeunit)
@@ -78,7 +78,7 @@ class DataTransformer:
78
78
  def start_transform(
79
79
  self,
80
80
  name: str,
81
- column_names: List[str],
81
+ column_names: list[str],
82
82
  start: str | None = None,
83
83
  stop: str | None = None,
84
84
  ):
@@ -94,7 +94,7 @@ class DataTransformer:
94
94
 
95
95
 
96
96
  class DataReader:
97
- def get_names(self, **kwargs) -> List[str]:
97
+ def get_names(self, **kwargs) -> list[str]:
98
98
  """
99
99
  TODO: not sure we really need this !
100
100
  """
@@ -108,10 +108,10 @@ class DataReader:
108
108
  transform: DataTransformer = DataTransformer(),
109
109
  chunksize=0,
110
110
  **kwargs,
111
- ) -> Iterator | List:
111
+ ) -> Iterator | list:
112
112
  raise NotImplementedError("read() method is not implemented")
113
113
 
114
- def get_aux_data_ids(self) -> Set[str]:
114
+ def get_aux_data_ids(self) -> set[str]:
115
115
  """
116
116
  Returns list of all auxiliary data IDs available for this data reader
117
117
  """
@@ -290,7 +290,7 @@ class CsvStorageDataReader(DataReader):
290
290
  _r.append(x.assign(symbol=symbol.upper(), timestamp=x.index)) # type: ignore
291
291
  return srows(*_r).set_index(["timestamp", "symbol"]) if _r else pd.DataFrame()
292
292
 
293
- def get_names(self, **kwargs) -> List[str]:
293
+ def get_names(self, **kwargs) -> list[str]:
294
294
  _n = []
295
295
  for root, _, files in os.walk(self.path):
296
296
  path = root.split(os.sep)
@@ -383,7 +383,7 @@ class InMemoryDataFrameReader(DataReader):
383
383
 
384
384
  Returns:
385
385
  --------
386
- Iterable | List
386
+ Iterable | list
387
387
  The processed and transformed data, either as an iterable (if chunksize > 0) or as a list.
388
388
 
389
389
  Raises:
@@ -437,7 +437,7 @@ class AsPandasFrame(DataTransformer):
437
437
  def __init__(self, timestamp_units=None) -> None:
438
438
  self.timestamp_units = timestamp_units
439
439
 
440
- def start_transform(self, name: str, column_names: List[str], **kwargs):
440
+ def start_transform(self, name: str, column_names: list[str], **kwargs):
441
441
  self._time_idx = _find_time_col_idx(column_names)
442
442
  self._column_names = column_names
443
443
  self._frame = pd.DataFrame()
@@ -480,7 +480,7 @@ class AsOhlcvSeries(DataTransformer):
480
480
  self._data_type = None
481
481
  self.timestamp_units = timestamp_units
482
482
 
483
- def start_transform(self, name: str, column_names: List[str], **kwargs):
483
+ def start_transform(self, name: str, column_names: list[str], **kwargs):
484
484
  self._time_idx = _find_time_col_idx(column_names)
485
485
  self._volume_idx = None
486
486
  self._b_volume_idx = None
@@ -539,7 +539,7 @@ class AsOhlcvSeries(DataTransformer):
539
539
  if self.timeframe:
540
540
  self._series = OHLCV(self._name, self.timeframe)
541
541
 
542
- def _proc_ohlc(self, rows_data: List[List]):
542
+ def _proc_ohlc(self, rows_data: list[list]):
543
543
  for d in rows_data:
544
544
  self._series.update_by_bar(
545
545
  _time(d[self._time_idx], self.timestamp_units),
@@ -551,21 +551,21 @@ class AsOhlcvSeries(DataTransformer):
551
551
  d[self._b_volume_idx] if self._b_volume_idx else 0,
552
552
  )
553
553
 
554
- def _proc_quotes(self, rows_data: List[List]):
554
+ def _proc_quotes(self, rows_data: list[list]):
555
555
  for d in rows_data:
556
556
  self._series.update(
557
557
  _time(d[self._time_idx], self.timestamp_units),
558
558
  (d[self._ask_idx] + d[self._bid_idx]) / 2,
559
559
  )
560
560
 
561
- def _proc_trades(self, rows_data: List[List]):
561
+ def _proc_trades(self, rows_data: list[list]):
562
562
  for d in rows_data:
563
563
  a = d[self._taker_idx] if self._taker_idx else 0
564
564
  s = d[self._size_idx]
565
565
  b = s if a else 0
566
566
  self._series.update(_time(d[self._time_idx], self.timestamp_units), d[self._price_idx], s, b)
567
567
 
568
- def process_data(self, rows_data: List[List]) -> Any:
568
+ def process_data(self, rows_data: list[list]) -> Any:
569
569
  if self._series is None:
570
570
  ts = [t[self._time_idx] for t in rows_data[:100]]
571
571
  self.timeframe = pd.Timedelta(infer_series_frequency(ts)).asm8.item()
@@ -610,7 +610,7 @@ class AsQuotes(DataTransformer):
610
610
  Data must have appropriate structure: bid, ask, bidsize, asksize and time
611
611
  """
612
612
 
613
- def start_transform(self, name: str, column_names: List[str], **kwargs):
613
+ def start_transform(self, name: str, column_names: list[str], **kwargs):
614
614
  self.buffer = list()
615
615
  self._time_idx = _find_time_col_idx(column_names)
616
616
  self._bid_idx = _find_column_index_in_list(column_names, "bid")
@@ -639,7 +639,7 @@ class AsOrderBook(DataTransformer):
639
639
  super().__init__()
640
640
  self.timestamp_units = timestamp_units
641
641
 
642
- def start_transform(self, name: str, column_names: List[str], **kwargs):
642
+ def start_transform(self, name: str, column_names: list[str], **kwargs):
643
643
  self.buffer = list()
644
644
  self._time_idx = _find_time_col_idx(column_names)
645
645
  self._top_bid_idx = _find_column_index_in_list(column_names, "top_bid")
@@ -668,7 +668,7 @@ class AsTrades(DataTransformer):
668
668
  Market maker column specifies if buyer is a maker or taker.
669
669
  """
670
670
 
671
- def start_transform(self, name: str, column_names: List[str], **kwargs):
671
+ def start_transform(self, name: str, column_names: list[str], **kwargs):
672
672
  self.buffer: list[Trade | TradeArray] = list()
673
673
  self._time_idx = _find_time_col_idx(column_names)
674
674
  self._price_idx = _find_column_index_in_list(column_names, "price")
@@ -727,7 +727,7 @@ class AsTimestampedRecords(DataTransformer):
727
727
  def __init__(self, timestamp_units: str | None = None) -> None:
728
728
  self.timestamp_units = timestamp_units
729
729
 
730
- def start_transform(self, name: str, column_names: List[str], **kwargs):
730
+ def start_transform(self, name: str, column_names: list[str], **kwargs):
731
731
  self.buffer = list()
732
732
  self._time_idx = _find_time_col_idx(column_names)
733
733
  self._column_names = column_names
@@ -792,7 +792,7 @@ class RestoredEmulatorHelper(DataTransformer):
792
792
  self._t_mid2 = dt // 2 + H1
793
793
  self._t_end = self._d_session_end - self._open_close_time_shift_secs * S1
794
794
 
795
- def start_transform(self, name: str, column_names: List[str], **kwargs):
795
+ def start_transform(self, name: str, column_names: list[str], **kwargs):
796
796
  self.buffer = []
797
797
  # - it will fail if receive data doesn't look as ohlcv
798
798
  self._time_idx = _find_time_col_idx(column_names)
@@ -989,7 +989,7 @@ class RestoredBarsFromOHLC(RestoredEmulatorHelper):
989
989
  ):
990
990
  super().__init__(daily_session_start_end, timestamp_units, open_close_time_shift_secs)
991
991
 
992
- def process_data(self, rows_data: List[List]) -> Any:
992
+ def process_data(self, rows_data: list[list]) -> Any:
993
993
  if rows_data is None:
994
994
  return
995
995
 
@@ -1033,7 +1033,7 @@ class AsDict(DataTransformer):
1033
1033
  Tries to keep incoming data as list of dictionaries with preprocessed time
1034
1034
  """
1035
1035
 
1036
- def start_transform(self, name: str, column_names: List[str], **kwargs):
1036
+ def start_transform(self, name: str, column_names: list[str], **kwargs):
1037
1037
  self.buffer = list()
1038
1038
  self._time_idx = _find_time_col_idx(column_names)
1039
1039
  self._column_names = column_names
@@ -1360,7 +1360,7 @@ class QuestDBConnector(DataReader):
1360
1360
  return pd.DataFrame()
1361
1361
  return df.set_index(["timestamp", "symbol", "metric"]).value.unstack("metric")
1362
1362
 
1363
- def get_names(self) -> List[str]:
1363
+ def get_names(self) -> list[str]:
1364
1364
  return self._get_names(self._builder)
1365
1365
 
1366
1366
  @_retry
qubx/features/core.py CHANGED
@@ -35,13 +35,14 @@ class FeatureManager:
35
35
  """
36
36
  Subscribes the given feature provider to the manager.
37
37
  """
38
- # - add the required subscriptions to the manager and update routing table
39
- self.feature_providers.append(feature_provider)
40
- for input_name in feature_provider.inputs():
41
- self.subscription_to_providers[input_name].append(feature_provider)
42
-
43
- # - notify the feature provider that it has been subscribed
44
- feature_provider.on_subscribe(self)
38
+ # - add the required subscriptions to the manager and update routing table if not already added
39
+ if feature_provider not in self.feature_providers:
40
+ self.feature_providers.append(feature_provider)
41
+ for input_name in feature_provider.inputs():
42
+ self.subscription_to_providers[input_name].append(feature_provider)
43
+
44
+ # - notify the feature provider that it has been subscribed
45
+ feature_provider.on_subscribe(self)
45
46
  return self
46
47
 
47
48
  def __str__(self) -> str:
@@ -68,7 +68,7 @@ def __interceptor_on_universe_change(func):
68
68
 
69
69
  # print new portfolio
70
70
  print(" - New Universe - ")
71
- portfolio()
71
+ portfolio(True)
72
72
 
73
73
  return result
74
74
  return _intercepted
@@ -113,6 +113,9 @@ class ActiveInstrument:
113
113
  def trade(self, qty: float, price=None, tif='gtc', **options):
114
114
  return ctx.trade(self._instrument, qty, price, tif, **options)
115
115
 
116
+ def trade_a(self, qty: float, price=None, tif='gtc', **options):
117
+ return ctx.trade_async(self._instrument, qty, price, tif, **options)
118
+
116
119
  def signal(self, s: float, price: float | None = None,
117
120
  stop: float | None = None,
118
121
  take: float | None = None,
@@ -174,6 +177,10 @@ def orders(instrument: Instrument | ActiveInstrument | None=None):
174
177
  def trade(instrument: Instrument | ActiveInstrument, qty: float, price=None, tif='gtc'):
175
178
  return ctx.trade(instrument if isinstance(instrument, Instrument) else instrument._instrument, qty, price, tif)
176
179
 
180
+
181
+ def trade_a(instrument: Instrument | ActiveInstrument, qty: float, price=None, tif='gtc'):
182
+ return ctx.trade_async(instrument if isinstance(instrument, Instrument) else instrument._instrument, qty, price, tif)
183
+
177
184
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
178
185
  def portfolio(all=True):
179
186
  from tabulate import tabulate
@@ -536,7 +536,7 @@ def _create_broker(
536
536
  enable_mm=_enable_mm,
537
537
  )
538
538
  return get_ccxt_broker(
539
- exchange_name, exchange, channel, time_provider, account, data_provider, **exchange_config.params
539
+ exchange_name, exchange, channel, time_provider, account, data_provider, **params
540
540
  )
541
541
  case "paper":
542
542
  assert isinstance(account, SimulatedAccountProcessor)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.35
3
+ Version: 0.6.37
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -19,51 +19,51 @@ qubx/cli/misc.py,sha256=tP28QxLEzuP8R2xnt8g3JTs9Z7aYy4iVWY4g3VzKTsQ,14777
19
19
  qubx/cli/release.py,sha256=JYdNt_ZM9jarmYiRDtKqbRqqllzm2Qwi6VggokB2j8A,28167
20
20
  qubx/connectors/ccxt/__init__.py,sha256=HEQ7lM9HS8sED_zfsAHrhFT7F9E7NFGAecwZwNr-TDE,65
21
21
  qubx/connectors/ccxt/account.py,sha256=HILqsSPfor58NrlP0qYwO5lkNZzUBG-SR5Hy1OSa7_M,24308
22
- qubx/connectors/ccxt/broker.py,sha256=PqGQU1CfwpGtjahynzIZul2hCOpVdkqu3B03aCulpcM,15408
22
+ qubx/connectors/ccxt/broker.py,sha256=Hg2tC3qKPYAURGro9ONzzR7QaiI41oytyKqQlpcM5Zw,16175
23
23
  qubx/connectors/ccxt/data.py,sha256=COVUh37ZdCUjiDB0a38Cj9SNSV8P95mqG2B3Gc_fQ2U,30172
24
24
  qubx/connectors/ccxt/exceptions.py,sha256=OfZc7iMdEG8uLorcZta2NuEuJrSIqi0FG7IICmwF54M,262
25
- qubx/connectors/ccxt/exchanges/__init__.py,sha256=eOWj-VN1ZpMX89hSzsVxdl1Knxae-29kPl_bUHbX--k,1885
25
+ qubx/connectors/ccxt/exchanges/__init__.py,sha256=dEBkyeiGEQgfyuGVhhx4ZTRIlU9e_H1m6K2ROXRpIi8,1884
26
26
  qubx/connectors/ccxt/exchanges/binance/broker.py,sha256=BB2V82zaOm1EjP3GrsOqQQMeGpml6-w23iv7goKrjyU,2111
27
- qubx/connectors/ccxt/exchanges/binance/exchange.py,sha256=ywmf373kVkXXw0h1PTqi7dg_3-sb7ZYKC8Y3qTmDtsY,24561
28
- qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py,sha256=SoMXtcPhuPU2Bi1kAymaq9VA9vHWPNt7l40EJMehMsk,10381
27
+ qubx/connectors/ccxt/exchanges/binance/exchange.py,sha256=qdyqiNTIVolpIwQ2g4Vrc5ibYUdS9moN7qt_UYNnezs,24794
28
+ qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py,sha256=Q6inz-43fDrl--UsnTaDMGHWWkUlcmzbuMLMkcQDVyk,10682
29
29
  qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py,sha256=zrnA6GJiNddoM5JF-SlFFO4FpITDO5rGaU9ipshUvAY,1603
30
30
  qubx/connectors/ccxt/exchanges/kraken/kraken.py,sha256=RFrnvr1L1NZYoKYWR5_L8vVkpMXtY7UDkWRnHeoasDU,351
31
31
  qubx/connectors/ccxt/factory.py,sha256=T0cMSH5m6-T2LXrbZHM9uCSOYOfZf-bh1fAOXAoFhF4,3226
32
32
  qubx/connectors/ccxt/reader.py,sha256=qaZIaOZkRf3Rz31ZrEqqAv4kATk5zDlSq-LK1jziBs8,8314
33
- qubx/connectors/ccxt/utils.py,sha256=hin4NgFJCH8fwQMZsjnMCwtP4KiITm5xgkiBnWgzMkI,11733
34
- qubx/connectors/tardis/data.py,sha256=S8YjJ35_GXc3o8_VRPAM3L4GSnADPPdV46y0EgFd0NU,30919
33
+ qubx/connectors/ccxt/utils.py,sha256=7BRmVT3hbGFbhRpJXTJ3lYX7R6VXi5aPxNYj1EjxObY,11775
34
+ qubx/connectors/tardis/data.py,sha256=JYkU9PTxIL4m0L2bLROzKtiiJSRbk48GDR4wnrL9ENM,30945
35
35
  qubx/connectors/tardis/utils.py,sha256=epThu9DwqbDb7BgScH6fHa_FVpKUaItOqp3JwtKGc5g,9092
36
36
  qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  qubx/core/account.py,sha256=4_XskMLR9Uh-rpJBDYMrceYiGMvAZw56k1ve-unIW8w,19417
38
38
  qubx/core/basics.py,sha256=PaprFa6sscrYPZfxImWOPLfVoVlu3PtcXTWbz0ZMtEk,28864
39
- qubx/core/context.py,sha256=R6otCA09Eqb4d0xA-HOGDAv4NjpLwJc3RzLs4QHXzg4,22316
39
+ qubx/core/context.py,sha256=ZetAOnQ9pfUiMAFln02zJr4FePTjKaY5OSIVwaMPhnE,22822
40
40
  qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
41
- qubx/core/errors.py,sha256=kWCK6o0-mm87VUhhlGKqwTpvdDXAza7YRRjeyz-vwfI,609
41
+ qubx/core/errors.py,sha256=LENtlgmVzxxUFNCsuy4PwyHYhkZkxuZQ2BPif8jaGmw,1411
42
42
  qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
43
43
  qubx/core/helpers.py,sha256=qzRsttt4sMYMarDWMzWvc3b2W-Qp9qAQwFiQBljAsA0,19722
44
44
  qubx/core/initializer.py,sha256=PUiD_cIjvGpuPjYyRpUjpwm3xNQ2Kipa8bAhbtxCQRo,3935
45
- qubx/core/interfaces.py,sha256=VKIgEhRaDj9EMf1UUcMlai2BLrw0UNPU1-2akU5q0Ng,57955
45
+ qubx/core/interfaces.py,sha256=CzIl8tB6ImQkDcZEmhpstwHPOCY8NhZxXmBHLQUAieI,58253
46
46
  qubx/core/loggers.py,sha256=eYhJANHYwz1heeFMa5V7jYCL196wkTSvj6c-8lkPj1Y,19567
47
47
  qubx/core/lookups.py,sha256=n5ZjjEhhRvmidCB-Cubr1b0Opm6lf_QVZNEWa_BOQG0,19376
48
- qubx/core/metrics.py,sha256=5zdr4uBJJTVf-X2L_7dhfFZOlYNLP1_idETGoZqxdbg,57913
48
+ qubx/core/metrics.py,sha256=Gq3Ultwn5meICfyauBUJrBS4nffSxFVH3OF6N1Y0xgo,58664
49
49
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
50
50
  qubx/core/mixins/market.py,sha256=lBappEimPhIuI0vmUvwVlIztkYjlEjJBpP-AdpfudII,3948
51
51
  qubx/core/mixins/processing.py,sha256=dqehukrfqcLy5BeILKnkpHCvva4SbLKj1ZbQdnByu1k,24552
52
52
  qubx/core/mixins/subscription.py,sha256=V_g9wCPQ8S5SHkU-qOZ84cV5nReAUrV7DoSNAGG0LPY,10372
53
53
  qubx/core/mixins/trading.py,sha256=idfRPaqrvkfMxzu9mXr9i_xfqLee-ZAOrERxkxv6Ruo,7256
54
54
  qubx/core/mixins/universe.py,sha256=L3s2Jw46_J1iDh4622Gk_LvCjol4W7mflBwEHrLfZEw,9899
55
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=FnN1en4og23NDYguU3uOYcYa-kt6m6-kH3UrVXCniLE,978280
55
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=xDkkXCrX_5IEVqcTRyEEW_6yHElmh3wbdMOnWGrPqb0,978280
56
56
  qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
57
57
  qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
58
58
  qubx/core/series.pyx,sha256=7cM3zZThW59waHiYcZmMxvYj-HYD7Ej_l7nKA4emPjE,46477
59
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=btF3eoTRmCq9jLbwBm7ZvM4U-7W0pGUo7DXxQop3ZZs,86568
59
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=dbfYcmWGKzRq-kIx0d7CPbIMobWlyFuRzl_oHxEtYgM,86568
60
60
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
61
61
  qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
62
62
  qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
63
63
  qubx/data/composite.py,sha256=bcFJzIzR2-IfVW8Ot3cUibKS8smnmRbHipd8ztIuScs,18015
64
64
  qubx/data/helpers.py,sha256=VcXBl1kfWzAOqrjadKrP9WemGjJIB0q3xascbesErh4,16268
65
- qubx/data/hft.py,sha256=Y1QVg3eXDt9ZtdnrNk7xreblEa0Mc4Jrprt0dVaoOn0,32797
66
- qubx/data/readers.py,sha256=Y6bU4jyeRGB9mPdQwi9dD0PTuiQH-OVAY9N-6cM3HVo,62527
65
+ qubx/data/hft.py,sha256=be7AwzTOjqqCENn0ClrZoHDyKv3SFG66IyTp8QadHlM,33687
66
+ qubx/data/readers.py,sha256=H68n38VLMjjk8R5FW7URGLcJCh0MREKFGdMGgWCWzhU,62503
67
67
  qubx/data/registry.py,sha256=45mjy5maBSO6cf-0zfIRRDs8b0VDW7wHSPn43aRjv-o,3883
68
68
  qubx/data/tardis.py,sha256=VMw13tIIlrGZwermKvdFRSNtLUiJDGOKW4l6WuAMQSA,33747
69
69
  qubx/emitters/__init__.py,sha256=tpJ9OoW-gycTBXGJ0647tT8-dVBmq23T2wMX_kmk3nM,565
@@ -81,7 +81,7 @@ qubx/exporters/formatters/slack.py,sha256=MPjbEFh7PQufPdkg_Fwiu2tVw5zYJa977tCemo
81
81
  qubx/exporters/redis_streams.py,sha256=8Cd39kAXUYSOS6-dQMSm1PpeQ4urOGVq0oe3dAXwUEI,8924
82
82
  qubx/exporters/slack.py,sha256=wnVZRwWOKq9lMQyW0MWh_6gkW1id1TUanfOKy-_clwI,7723
83
83
  qubx/features/__init__.py,sha256=ZFCX7K5bDAH7yTsG-mf8zibW8UW8GCneEagL1_p8kDQ,385
84
- qubx/features/core.py,sha256=N_4a2IqjuoYzUMIQkQ3Hf6vmaZ7BtrdegzUtb5MH31Q,10487
84
+ qubx/features/core.py,sha256=eXa1qIu-LXo40td1X4EUBFQ5jJcSTuaQIi-562bPCoM,10587
85
85
  qubx/features/orderbook.py,sha256=idmBEYDMMNBkQopHKQs_oEmQMPP9gNJpgHVmUBlXeek,1288
86
86
  qubx/features/price.py,sha256=fyjrHwHR0ftjGNXlpEx8Hjyfi3d1lBNFjsbe6qLFMqo,699
87
87
  qubx/features/trades.py,sha256=d0dhmbemOOOJcXSet6n9sBI4-LVl9-oFh6Of7cRoY-4,3582
@@ -117,7 +117,7 @@ qubx/restorers/signal.py,sha256=DBLqA7vDhoMTAzUC4N9UerrO0GbjeHdTeMoCz7U7iI8,6621
117
117
  qubx/restorers/state.py,sha256=duXyEHQhS1zdNdo3VKscMhieZ5sYNlfE4S_pPXQ1Tuw,4109
118
118
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
119
119
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=lmn5jOH43-uSKHjA3wwcQP6-WHZYAzPsx3Yeo5xoOxg,654440
120
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=aQuDwEXqPC-9q3Ue_oHiyIZ-ecql-PYm_FG1yFpWfbg,654440
121
121
  qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
122
122
  qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
123
123
  qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
@@ -147,15 +147,15 @@ qubx/utils/plotting/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
147
147
  qubx/utils/plotting/renderers/plotly.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
148
148
  qubx/utils/questdb.py,sha256=TdjmlGPoZXdjidZ_evcBIkFtoL4nGQXPR4IQSUc6IvA,2509
149
149
  qubx/utils/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
- qubx/utils/runner/_jupyter_runner.pyt,sha256=41dLQeI2EL4wJjBDht2qKbgISgS5DtmJ6XtPWE9NWGs,9243
150
+ qubx/utils/runner/_jupyter_runner.pyt,sha256=fDj4AUs25jsdGmY9DDeSFufH1JkVhLFwy0BOmVO7nIU,9609
151
151
  qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
152
152
  qubx/utils/runner/configs.py,sha256=4lonQgksh4wDygsN67lIidVRIUksskWuhL25A2IZwho,3694
153
153
  qubx/utils/runner/factory.py,sha256=vQ2dBTbrQE9YH__-TvuFzGF-E1li-vt_qQum9GHa11g,11666
154
- qubx/utils/runner/runner.py,sha256=DWc85M5thSQsuiV9FQiowJ3VF9DbwPiG9RE5Lc-7XEk,29314
154
+ qubx/utils/runner/runner.py,sha256=TF-nD-EH8k1U2Wkjk70eEVketU6HtWpTFmFH7vjE9Z8,29298
155
155
  qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
156
156
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
157
- qubx-0.6.35.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
158
- qubx-0.6.35.dist-info/METADATA,sha256=vhl17QmSnb0raya3HhxLpxOPNDOi1YTXdQdvlpmxGH8,4492
159
- qubx-0.6.35.dist-info/WHEEL,sha256=XjdW4AGUgFDhpG9b3b2KPhtR_JLZvHyfemLgJJwcqOI,110
160
- qubx-0.6.35.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
161
- qubx-0.6.35.dist-info/RECORD,,
157
+ qubx-0.6.37.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
158
+ qubx-0.6.37.dist-info/METADATA,sha256=wxX0lvx6Mb2XSM4_ZHwHs9X_uq5qI3SG0t1IOyW-8TM,4492
159
+ qubx-0.6.37.dist-info/WHEEL,sha256=XjdW4AGUgFDhpG9b3b2KPhtR_JLZvHyfemLgJJwcqOI,110
160
+ qubx-0.6.37.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
161
+ qubx-0.6.37.dist-info/RECORD,,
File without changes
File without changes