Qubx 0.4.0__cp311-cp311-manylinux_2_35_x86_64.whl → 0.4.3__cp311-cp311-manylinux_2_35_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.

qubx/backtester/queue.py CHANGED
@@ -17,7 +17,7 @@ _SW = Stopwatch()
17
17
 
18
18
 
19
19
  class DataLoader:
20
- _TYPE_MAPPERS = {"agg_trade": "trade", "ohlc": "bar", "ohlcv": "bar"}
20
+ _TYPE_MAPPERS = {"agg_trade": "trade", "bar": "ohlc", "ohlcv": "ohlc"}
21
21
 
22
22
  def __init__(
23
23
  self,
@@ -25,7 +25,7 @@ class DataLoader:
25
25
  reader: DataReader,
26
26
  instrument: Instrument,
27
27
  timeframe: str | None,
28
- preload_bars: int = 0,
28
+ warmup_period: str | None = None,
29
29
  data_type: str = "ohlc",
30
30
  output_type: str | None = None, # transfomer can somtimes map to a different output type
31
31
  chunksize: int = 5_000,
@@ -34,7 +34,7 @@ class DataLoader:
34
34
  self._spec = f"{instrument.exchange}:{instrument.symbol}"
35
35
  self._reader = reader
36
36
  self._transformer = transformer
37
- self._init_bars_required = preload_bars
37
+ self._warmup_period = warmup_period
38
38
  self._timeframe = timeframe
39
39
  self._data_type = data_type
40
40
  self._output_type = output_type
@@ -43,8 +43,8 @@ class DataLoader:
43
43
 
44
44
  def load(self, start: str | pd.Timestamp, end: str | pd.Timestamp) -> Iterator:
45
45
  if self._first_load:
46
- if self._init_bars_required > 0 and self._timeframe:
47
- start = pd.Timestamp(start) - self._init_bars_required * pd.Timedelta(self._timeframe)
46
+ if self._warmup_period:
47
+ start = pd.Timestamp(start) - pd.Timedelta(self._warmup_period)
48
48
  self._first_load = False
49
49
 
50
50
  args = dict(
@@ -11,7 +11,7 @@ from qubx import lookup, logger, QubxLogConfig
11
11
  from qubx.core.account import AccountProcessor
12
12
  from qubx.core.helpers import BasicScheduler
13
13
  from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
14
- from qubx.core.series import Quote
14
+ from qubx.core.series import Quote, time_as_nsec
15
15
  from qubx.core.basics import (
16
16
  ITimeProvider,
17
17
  Instrument,
@@ -23,6 +23,7 @@ from qubx.core.basics import (
23
23
  TradingSessionResult,
24
24
  TransactionCostsCalculator,
25
25
  dt_64,
26
+ SubscriptionType,
26
27
  )
27
28
  from qubx.core.series import TimeSeries, Trade, Quote, Bar, OHLCV
28
29
  from qubx.core.interfaces import (
@@ -32,7 +33,6 @@ from qubx.core.interfaces import (
32
33
  PositionsTracker,
33
34
  IStrategyContext,
34
35
  TriggerEvent,
35
- SubscriptionType,
36
36
  )
37
37
 
38
38
  from qubx.core.context import StrategyContext
@@ -50,6 +50,7 @@ from qubx.data.readers import (
50
50
  )
51
51
  from qubx.pandaz.utils import scols
52
52
  from qubx.utils.misc import ProgressParallel
53
+ from qubx.utils.time import infer_series_frequency
53
54
  from joblib import delayed
54
55
  from .queue import DataLoader, SimulatedDataQueue, EventBatcher
55
56
 
@@ -287,22 +288,21 @@ class SimulatedTrading(ITradingServiceProvider):
287
288
  return [o for ome in self._ome.values() for o in ome.get_open_orders()]
288
289
 
289
290
  def get_position(self, instrument: Instrument) -> Position:
290
- symbol = instrument.symbol
291
-
292
- if symbol not in self.acc._positions:
293
- # - initiolize OME for this instrument
294
- self._ome[instrument] = OrdersManagementEngine(
295
- instrument=instrument,
296
- time_provider=self,
297
- tcc=self._fees_calculator, # type: ignore
298
- fill_stop_order_at_price=self._fill_stop_order_at_price,
299
- )
300
-
301
- # - initiolize empty position
302
- position = Position(instrument) # type: ignore
303
- self._half_tick_size[instrument] = instrument.min_tick / 2 # type: ignore
304
- self.acc.attach_positions(position)
291
+ if instrument in self.acc.positions:
292
+ return self.acc.positions[instrument]
293
+
294
+ # - initiolize OME for this instrument
295
+ self._ome[instrument] = OrdersManagementEngine(
296
+ instrument=instrument,
297
+ time_provider=self,
298
+ tcc=self._fees_calculator, # type: ignore
299
+ fill_stop_order_at_price=self._fill_stop_order_at_price,
300
+ )
305
301
 
302
+ # - initiolize empty position
303
+ position = Position(instrument) # type: ignore
304
+ self._half_tick_size[instrument] = instrument.min_tick / 2 # type: ignore
305
+ self.acc.attach_positions(position)
306
306
  return self.acc._positions[instrument]
307
307
 
308
308
  def time(self) -> dt_64:
@@ -391,7 +391,7 @@ class SimulatedExchange(IBrokerServiceProvider):
391
391
  _scheduler: BasicScheduler
392
392
  _current_time: dt_64
393
393
  _hist_data_type: str
394
- _loaders: dict[str, dict[str, DataLoader]]
394
+ _loaders: dict[Instrument, dict[str, DataLoader]]
395
395
  _pregenerated_signals: Dict[Instrument, pd.Series]
396
396
 
397
397
  def __init__(
@@ -432,22 +432,22 @@ class SimulatedExchange(IBrokerServiceProvider):
432
432
  self,
433
433
  instruments: list[Instrument],
434
434
  subscription_type: str,
435
+ warmup_period: str | None = None,
435
436
  timeframe: str | None = None,
436
- nback: int = 0,
437
437
  **kwargs,
438
438
  ) -> bool:
439
439
  units = kwargs.get("timestamp_units", "ns")
440
440
 
441
441
  for instr in instruments:
442
442
  logger.debug(
443
- f"SimulatedExchangeService :: subscribe :: {instr.symbol}({subscription_type}, {nback}, {timeframe})"
443
+ f"SimulatedExchangeService :: subscribe :: {instr.symbol}({subscription_type}, {warmup_period}, {timeframe})"
444
444
  )
445
445
  self._symbol_to_instrument[instr.symbol] = instr
446
446
 
447
447
  _params: Dict[str, Any] = dict(
448
448
  reader=self._reader,
449
449
  instrument=instr,
450
- preload_bars=nback,
450
+ warmup_period=warmup_period,
451
451
  timeframe=timeframe,
452
452
  )
453
453
 
@@ -470,23 +470,34 @@ class SimulatedExchange(IBrokerServiceProvider):
470
470
 
471
471
  # - add loader for this instrument
472
472
  ldr = DataLoader(**_params)
473
- self._loaders[instr.symbol][subscription_type] = ldr
473
+ self._loaders[instr][subscription_type] = ldr
474
474
  self._data_queue += ldr
475
475
 
476
476
  return True
477
477
 
478
- def unsubscribe(self, subscription_type: str, instruments: List[Instrument]) -> bool:
478
+ def unsubscribe(self, instruments: List[Instrument], subscription_type: str | None) -> bool:
479
479
  for instr in instruments:
480
480
  if instr.symbol in self._loaders:
481
- logger.debug(f"SimulatedExchangeService :: unsubscribe :: {instr.symbol} :: {subscription_type}")
482
- self._data_queue -= self._loaders[instr.symbol].pop(subscription_type)
483
- if not self._loaders[instr.symbol]:
484
- self._loaders.pop(instr.symbol)
481
+ if subscription_type:
482
+ logger.debug(f"SimulatedExchangeService :: unsubscribe :: {instr.symbol} :: {subscription_type}")
483
+ self._data_queue -= self._loaders[instr].pop(subscription_type)
484
+ if not self._loaders[instr]:
485
+ self._loaders.pop(instr)
486
+ else:
487
+ logger.debug(f"SimulatedExchangeService :: unsubscribe :: {instr.symbol}")
488
+ for ldr in self._loaders[instr].values():
489
+ self._data_queue -= ldr
490
+ self._loaders.pop(instr)
485
491
  return True
486
492
 
487
493
  def has_subscription(self, subscription_type: str, instrument: Instrument) -> bool:
488
494
  return instrument in self._loaders and subscription_type in self._loaders[instrument]
489
495
 
496
+ def get_subscriptions(self, instrument: Instrument) -> Dict[str, Dict[str, Any]]:
497
+ # TODO: implement
498
+ # return {k: v.params for k, v in self._loaders[instrument].items()}
499
+ return {}
500
+
490
501
  def _try_add_process_signals(self, start: str | pd.Timestamp, end: str | pd.Timestamp) -> None:
491
502
  if self._pregenerated_signals:
492
503
  for s, v in self._pregenerated_signals.items():
@@ -593,17 +604,34 @@ class SimulatedExchange(IBrokerServiceProvider):
593
604
 
594
605
  def get_historical_ohlcs(self, instrument: Instrument, timeframe: str, nbarsback: int) -> list[Bar]:
595
606
  start = pd.Timestamp(self.time())
596
- end = start - nbarsback * pd.Timedelta(timeframe)
607
+ end = start - nbarsback * (_timeframe := pd.Timedelta(timeframe))
597
608
  _spec = f"{instrument.exchange}:{instrument.symbol}"
598
- records = self._reader.read(
599
- data_id=_spec, start=start, stop=end, transform=AsTimestampedRecords() # type: ignore
609
+ return self._convert_records_to_bars(
610
+ self._reader.read(data_id=_spec, start=start, stop=end, transform=AsTimestampedRecords()),
611
+ time_as_nsec(self.time()),
612
+ _timeframe.asm8.item()
600
613
  )
601
- if not records:
602
- return []
603
- return [
604
- Bar(np.datetime64(r["timestamp_ns"], "ns").item(), r["open"], r["high"], r["low"], r["close"], r["volume"])
605
- for r in records
606
- ]
614
+
615
+ def _convert_records_to_bars(self, records: List[Dict[str, Any]], cut_time_ns: int, timeframe_ns: int) -> List[Bar]:
616
+ """
617
+ Convert records to bars and we need to cut last bar up to the cut_time_ns
618
+ """
619
+ bars = []
620
+
621
+ _data_tf = infer_series_frequency([r['timestamp_ns'] for r in records[:100]])
622
+ timeframe_ns = _data_tf.item()
623
+
624
+ if records is not None:
625
+ for r in records:
626
+ _bts_0 = np.datetime64(r["timestamp_ns"], "ns").item()
627
+ o, h, l, c, v = r["open"], r["high"], r["low"], r["close"], r["volume"]
628
+
629
+ if _bts_0 <= cut_time_ns and cut_time_ns < _bts_0 + timeframe_ns:
630
+ break
631
+
632
+ bars.append(Bar(_bts_0, o, h, l, c, v))
633
+
634
+ return bars
607
635
 
608
636
  def set_generated_signals(self, signals: pd.Series | pd.DataFrame):
609
637
  logger.debug(f"Using pre-generated signals:\n {str(signals.count()).strip('ndtype: int64')}")
@@ -896,11 +924,6 @@ class SignalsProxy(IStrategy):
896
924
  def on_init(self, ctx: IStrategyContext):
897
925
  ctx.set_base_subscription(SubscriptionType.OHLC, timeframe=self.timeframe)
898
926
 
899
- def on_fit(
900
- self, ctx: IStrategyContext, fit_time: str | pd.Timestamp, previous_fit_time: str | pd.Timestamp | None = None
901
- ):
902
- return None
903
-
904
927
  def on_event(self, ctx: IStrategyContext, event: TriggerEvent) -> Optional[List[Signal]]:
905
928
  if event.data and event.type == "event":
906
929
  signal = event.data.get("order")