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

qubx/core/helpers.py CHANGED
@@ -276,6 +276,10 @@ def _parse_schedule_spec(schedule: str) -> dict[str, str]:
276
276
  return {k: v for k, v in m.groupdict().items() if v} if m else {}
277
277
 
278
278
 
279
+ def _to_dt_64(time: float) -> np.datetime64:
280
+ return np.datetime64(int(time * 1000000000), "ns")
281
+
282
+
279
283
  def process_schedule_spec(spec_str: str | None) -> dict[str, Any]:
280
284
  AS_INT = lambda d, k: int(d.get(k, 0)) # noqa: E731
281
285
  S = lambda s: [x for x in re.split(r"[, ]", s) if x] # noqa: E731
@@ -409,11 +413,11 @@ class BasicScheduler:
409
413
  prev_time = iter.get_prev()
410
414
  next_time = iter.get_next(start_time=start_time)
411
415
  if next_time:
412
- self._scdlr.enterabs(next_time, 1, self._trigger, (event, prev_time, next_time))
416
+ self._scdlr.enterabs(next_time, 1, self._trigger, (event, _to_dt_64(prev_time), _to_dt_64(next_time)))
413
417
 
414
418
  # - update next nearest time
415
419
  self._next_times[event] = next_time
416
- self._next_nearest_time = np.datetime64(int(min(self._next_times.values()) * 1000000000), "ns")
420
+ self._next_nearest_time = _to_dt_64(min(self._next_times.values()))
417
421
  # logger.debug(f" >>> ({event}) task is scheduled at {self._next_nearest_time}")
418
422
 
419
423
  return True
qubx/core/interfaces.py CHANGED
@@ -1148,6 +1148,11 @@ class IStrategyContext(
1148
1148
  """Check if the strategy context is running in simulation mode."""
1149
1149
  return False
1150
1150
 
1151
+ @property
1152
+ def is_live_or_warmup(self) -> bool:
1153
+ """Check if the strategy context is running in live or warmup mode."""
1154
+ return not self.is_simulation or self.is_warmup_in_progress
1155
+
1151
1156
  @property
1152
1157
  def is_paper_trading(self) -> bool:
1153
1158
  """Check if the strategy context is running in simulated trading mode."""
@@ -231,18 +231,17 @@ class ProcessingManager(IProcessingManager):
231
231
 
232
232
  # FIXME: (2025-06-17) we need to refactor this to avoid doing it here !!!
233
233
  # - on trigger event we need to be sure that all instruments have finalized OHLC data
234
- if not self._is_simulation:
235
- # - - - - - - IMPORTANT NOTES - - - - - -
236
- # This is a temporary fix to ensure that all instruments have finalized OHLC data.
237
- # In live mode with multiple instruments, we can have a situation where one instrument
238
- # is updated with new bar data while other instruments are not updated yet.
239
- # This leads to a situation where indicators are not calculated correctly for all instruments in the universe.
240
- # A simple dirty solution is to update OHLC data for all instruments in the universe with the last update value but with the actual time.
241
- # This leads to finalization of OHLC data, but the open price may differ slightly from the real one.
242
- # - - - - - - - - - - - - - - - - - - - -
243
-
244
- # - finalize OHLC data for all instruments
245
- self._cache.finalize_ohlc_for_instruments(event.time, self._context.instruments)
234
+ # - - - - - - IMPORTANT NOTES - - - - - -
235
+ # This is a temporary fix to ensure that all instruments have finalized OHLC data.
236
+ # In live mode with multiple instruments, we can have a situation where one instrument
237
+ # is updated with new bar data while other instruments are not updated yet.
238
+ # This leads to a situation where indicators are not calculated correctly for all instruments in the universe.
239
+ # A simple dirty solution is to update OHLC data for all instruments in the universe with the last update value but with the actual time.
240
+ # This leads to finalization of OHLC data, but the open price may differ slightly from the real one.
241
+ # - - - - - - - - - - - - - - - - - - - -
242
+
243
+ # - finalize OHLC data for all instruments
244
+ self._cache.finalize_ohlc_for_instruments(event.time, self._context.instruments)
246
245
 
247
246
  with self._health_monitor("stg.trigger_event"):
248
247
  signals.extend(self._as_list(self._strategy.on_event(self._context, _trigger_event)))
@@ -666,6 +665,8 @@ class ProcessingManager(IProcessingManager):
666
665
  if not self._is_data_ready():
667
666
  return
668
667
  self._fit_is_running = True
668
+ current_time = data[1]
669
+ self._cache.finalize_ohlc_for_instruments(current_time, self._context.instruments)
669
670
  self._run_in_thread_pool(self.__invoke_on_fit)
670
671
 
671
672
  def _handle_ohlc(self, instrument: Instrument, event_type: str, bar: Bar) -> MarketEvent:
qubx/core/series.pxd CHANGED
@@ -35,6 +35,7 @@ cdef class TimeSeries:
35
35
  cdef class Indicator(TimeSeries):
36
36
  cdef public TimeSeries series
37
37
  cdef public TimeSeries parent
38
+ cdef unsigned short _is_initial_recalculate
38
39
 
39
40
 
40
41
  cdef class RollingSum:
qubx/core/series.pyi CHANGED
@@ -125,6 +125,8 @@ class Indicator(TimeSeries):
125
125
  def update(self, time: int, value, new_item_started: bool) -> object: ...
126
126
  @classmethod
127
127
  def wrap(cls, series: TimeSeries, *args, **kwargs) -> "Indicator": ...
128
+ @property
129
+ def is_initial_recalculate(self) -> bool: ...
128
130
 
129
131
  class IndicatorOHLC(Indicator):
130
132
  series: OHLCV
qubx/core/series.pyx CHANGED
@@ -399,6 +399,9 @@ cdef class Indicator(TimeSeries):
399
399
  series.indicators[name] = self
400
400
  self.name = name
401
401
 
402
+ # - initialize the initial recalculation flag
403
+ self._is_initial_recalculate = 0
404
+
402
405
  # - we need to make a empty copy and fill it
403
406
  self.series = self._clone_empty(series.name, series.timeframe, series.max_series_length)
404
407
  self.parent = series
@@ -407,7 +410,9 @@ cdef class Indicator(TimeSeries):
407
410
  self._on_attach_indicator(self, series)
408
411
 
409
412
  # - recalculate indicator on data as if it would being streamed
413
+ self._is_initial_recalculate = 1
410
414
  self._initial_data_recalculate(series)
415
+ self._is_initial_recalculate = 0
411
416
 
412
417
  def _on_attach_indicator(self, indicator: Indicator, indicator_input: TimeSeries):
413
418
  self.parent._on_attach_indicator(indicator, indicator_input)
@@ -435,6 +440,14 @@ cdef class Indicator(TimeSeries):
435
440
  def wrap(clz, series:TimeSeries, *args, **kwargs):
436
441
  return _wrap_indicator(series, clz, *args, **kwargs)
437
442
 
443
+ @property
444
+ def is_initial_recalculate(self) -> bool:
445
+ """
446
+ Returns True if the indicator is currently in the initial data recalculation phase,
447
+ False otherwise.
448
+ """
449
+ return self._is_initial_recalculate == 1
450
+
438
451
 
439
452
  cdef class IndicatorOHLC(Indicator):
440
453
  """
@@ -104,7 +104,7 @@ class IndicatorEmitter(Indicator):
104
104
  if new_item_started and len(self._wrapped_indicator) >= 2:
105
105
  should_emit = True
106
106
  # Use the previous (completed) value, not the current one
107
- current_value = self._wrapped_indicator[1]
107
+ current_value = self._wrapped_indicator[1] if not self.is_initial_recalculate else value
108
108
 
109
109
  # Emit if we should and the value is valid
110
110
  if should_emit and not np.isnan(current_value):
qubx/trackers/riskctrl.py CHANGED
@@ -588,6 +588,7 @@ class AtrRiskTracker(GenericRiskControllerDecorator):
588
588
  self.atr_period = atr_period
589
589
  self.atr_smoother = atr_smoother
590
590
  self._full_name = f"{self.__class__.__name__}{purpose}"
591
+ self._instrument_initialized: dict[Instrument, bool] = {}
591
592
 
592
593
  super().__init__(
593
594
  sizer,
@@ -596,33 +597,37 @@ class AtrRiskTracker(GenericRiskControllerDecorator):
596
597
  ),
597
598
  )
598
599
 
599
- def calculate_risks(self, ctx: IStrategyContext, quote: Quote | None, signal: Signal) -> Signal | None:
600
- volatility = atr(
601
- ctx.ohlc(signal.instrument, self.atr_timeframe, 2 * self.atr_period),
600
+ def _get_volatility(self, ctx: IStrategyContext, instrument: Instrument) -> list[float]:
601
+ return atr(
602
+ ctx.ohlc(instrument, self.atr_timeframe, 2 * self.atr_period),
602
603
  self.atr_period,
603
604
  smoother=self.atr_smoother,
604
605
  percentage=False,
605
606
  )
606
607
 
608
+ def update(
609
+ self, ctx: IStrategyContext, instrument: Instrument, update: Quote | Trade | Bar | OrderBook
610
+ ) -> list[TargetPosition] | TargetPosition:
611
+ if ctx.is_live_or_warmup and not self._instrument_initialized.get(instrument, False):
612
+ # - emit volatility indicator in live mode
613
+ indicator_emitter(
614
+ wrapped_indicator=self._get_volatility(ctx, instrument),
615
+ metric_emitter=ctx.emitter,
616
+ instrument=instrument,
617
+ )
618
+ self._instrument_initialized[instrument] = True
619
+
620
+ return super().update(ctx, instrument, update)
621
+
622
+ def calculate_risks(self, ctx: IStrategyContext, quote: Quote | None, signal: Signal) -> Signal | None:
623
+ volatility = self._get_volatility(ctx, signal.instrument)
624
+
607
625
  if len(volatility) < 2 or ((last_volatility := volatility[1]) is None or not np.isfinite(last_volatility)):
608
626
  logger.debug(
609
627
  f"[<y>{self._full_name}</y>(<g>{signal.instrument}</g>)] :: not enough ATR data, skipping risk calculation"
610
628
  )
611
629
  return None
612
630
 
613
- if not ctx.is_simulation:
614
- indicator_emitter(
615
- wrapped_indicator=volatility,
616
- metric_emitter=ctx.emitter,
617
- instrument=signal.instrument,
618
- )
619
- if quote is not None:
620
- mid_price = quote.mid_pice()
621
- volatility_pct = last_volatility / mid_price
622
- signal.comment += f", ATR: {volatility_pct:.2%} ({last_volatility:.4f})"
623
- signal.comment += f", stop_risk: {self.stop_risk}"
624
- signal.comment += f", take_target: {self.take_target}"
625
-
626
631
  if quote is None:
627
632
  logger.debug(
628
633
  f"[<y>{self._full_name}</y>(<g>{signal.instrument}</g>)] :: there is no actual market data, skipping risk calculation"
@@ -643,6 +648,14 @@ class AtrRiskTracker(GenericRiskControllerDecorator):
643
648
  if self.take_target:
644
649
  signal.take = entry - self.take_target * last_volatility
645
650
 
651
+ if ctx.is_live_or_warmup:
652
+ # - additional comments for live debugging
653
+ mid_price = quote.mid_price()
654
+ volatility_pct = last_volatility / mid_price
655
+ signal.comment += f", ATR: {volatility_pct:.2%} ({last_volatility:.4f})"
656
+ signal.comment += f", stop_risk: {self.stop_risk}"
657
+ signal.comment += f", take_target: {self.take_target}"
658
+
646
659
  return signal
647
660
 
648
661
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.60
3
+ Version: 0.6.61
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -41,23 +41,23 @@ qubx/core/context.py,sha256=h2KeQnVe514VDSehjEIdl2Pv8FKjYaCOhpvJZPFFON8,24107
41
41
  qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
42
42
  qubx/core/errors.py,sha256=LENtlgmVzxxUFNCsuy4PwyHYhkZkxuZQ2BPif8jaGmw,1411
43
43
  qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
44
- qubx/core/helpers.py,sha256=O6d813A5sdJfH-gfvWTpXuV-iQ4xkxxLu8iPMwAbMbw,19926
44
+ qubx/core/helpers.py,sha256=nZMe3HVheSkqczZq7-foPHfJ-49HJL69mBirBxynOBQ,20022
45
45
  qubx/core/initializer.py,sha256=YgTBs5LpIk6ZFdmMD8zCnJnVNcMh1oeYvt157jhwyMg,4242
46
- qubx/core/interfaces.py,sha256=OsPHqcjft8w6WM0zLPCicek1RA44xnqtjjtp6v6NQm8,61177
46
+ qubx/core/interfaces.py,sha256=OUv56aUNpaBn6zUgHr1k642ZwGgr_o0xb0_Uu3qmrxk,61380
47
47
  qubx/core/loggers.py,sha256=wO6UdFasWu5bNeDkN7eRVDhHUQ2Rj3Apkzk9h2q71Rk,14128
48
48
  qubx/core/lookups.py,sha256=2UmODxDeDQqi-xHOvjm2_GckC4piKI3x4ZEf5vny8YI,18275
49
49
  qubx/core/metrics.py,sha256=SLbuG66Y3sH86tDHcdoIHovkhM-oSF5o840BRChrvCE,60849
50
50
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
51
51
  qubx/core/mixins/market.py,sha256=f0PJTK5Y9nQkV2XWxytixFG1Oc6ihYVwlxIMraTnmms,4566
52
- qubx/core/mixins/processing.py,sha256=ZOfqFymyMw1FygE9GOZSG61sZcElZwiNZ1Tu7w46VZw,35214
52
+ qubx/core/mixins/processing.py,sha256=xFM6HkcucbtwQWKTmTVhxjdwVgKYcInI6fr3hnoDSRo,35252
53
53
  qubx/core/mixins/subscription.py,sha256=V_g9wCPQ8S5SHkU-qOZ84cV5nReAUrV7DoSNAGG0LPY,10372
54
54
  qubx/core/mixins/trading.py,sha256=idfRPaqrvkfMxzu9mXr9i_xfqLee-ZAOrERxkxv6Ruo,7256
55
55
  qubx/core/mixins/universe.py,sha256=mzZJA7Me6HNFbAMGg1XOpnYCMtcFKHESTiozjaXyKXY,10100
56
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=0qyuWzu4CSErr_ztC2d24JWT-XxkeL-cmJROwzkYZmo,978280
57
- qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
58
- qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
59
- qubx/core/series.pyx,sha256=ZQPp1-Kp2lX22n04gaBWak9mW139e-uc8bhsbZMohgs,46507
60
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=XheYraWWQ_7WRGxT91BngIRlnkis8fqmxx98aJ1wiUI,86568
56
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=HlWfzVKW-JNVO-O38ZnAJGKIYOKOGi-8Dy2Mcxy7r1g,982408
57
+ qubx/core/series.pxd,sha256=aI5PG1hbr827xwcnSYgGMF2IBD4GvCRby_i9lrGrJdQ,4026
58
+ qubx/core/series.pyi,sha256=wRb1_HpZC7KSTyWMi7K0BisaKSEro3LVebr8z5XzoDs,4668
59
+ qubx/core/series.pyx,sha256=TpKMCqRrzrT0cj1pASlB7mLLgMLQZwDNC5o-QOxqhRQ,46936
60
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=eIHeTmdWn8PUU7xSlZHscKyL-c0s91ep46xv1aZdeyQ,86568
61
61
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
62
62
  qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
63
63
  qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
@@ -71,7 +71,7 @@ qubx/emitters/__init__.py,sha256=11sYi8CHdPlQ5NLR84C2vUnMDkAqNIAmKo_SfqCMj2c,705
71
71
  qubx/emitters/base.py,sha256=os69vCs00eRELZn-1TYR7MZXJbnrFOfeW4UEtMLCphw,7976
72
72
  qubx/emitters/composite.py,sha256=8DsPIUtaJ95Oww9QTVVB6LR7Wcb6TJ-c1jIHMGuttz4,2784
73
73
  qubx/emitters/csv.py,sha256=lWl6sP0ke0j6kVlEbQsy11vSOHFudYHjWS9iPbq6kmo,3067
74
- qubx/emitters/indicator.py,sha256=pVZJe9DvPj3bPyVXdsj3IlQa4qt3xqiNFlAdDjsFVNY,7937
74
+ qubx/emitters/indicator.py,sha256=8opNUrkoLLcoTLISCMb7e5mlVZgdH5HduyrBxkA6sg4,7983
75
75
  qubx/emitters/prometheus.py,sha256=g2hgcV_G77fWVEXtoGJTUs4JLkB2FQXFzVY_x_sEBfc,8100
76
76
  qubx/emitters/questdb.py,sha256=vGi5r6JbKwwi8SpdXj_oG1FYrI-aEYLHYb633UAwNBk,5962
77
77
  qubx/exporters/__init__.py,sha256=7HeYHCZfKAaBVAByx9wE8DyGv6C55oeED9uUphcyjuc,360
@@ -130,7 +130,7 @@ qubx/restorers/signal.py,sha256=7n7eeRhWGUBPbg179GxFH_ifywcl3pQJbwrcDklw0N0,1460
130
130
  qubx/restorers/state.py,sha256=I1VIN0ZcOjigc3WMHIYTNJeAAbN9YB21MDcMl04ZWmY,8018
131
131
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
132
132
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=OGzTQQ0p38xqBguP3FZIQJ8Yaae55HbGFMUlMTTTgzo,654440
133
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=QdFLRXm20CACYt2TmKIsxx7XWns2_5Op1i4Exw-h0AE,662632
134
134
  qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
135
135
  qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
136
136
  qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
@@ -138,7 +138,7 @@ qubx/trackers/__init__.py,sha256=ThIP1jXaACse5hG3lZqQSlWSKYl6APxFmBHaRcVpPdU,100
138
138
  qubx/trackers/advanced.py,sha256=CONogr5hHHEwfolkegjpEz7NNk4Ruf9pOq5nKifNXVM,12761
139
139
  qubx/trackers/composite.py,sha256=Tjupx78SraXmRKkWhu8n81RkPjOgsDbXLd8yz6PhbaA,6318
140
140
  qubx/trackers/rebalancers.py,sha256=KFY7xuD4fGALiSLMas8MZ3ueRzlWt5wDT9329dlmNng,5150
141
- qubx/trackers/riskctrl.py,sha256=LaScGtZDn4hSEHx3lYQEpCT4GC7G91lgNedRvmAzLMk,34428
141
+ qubx/trackers/riskctrl.py,sha256=IuxccTgdEnN9lLMoYU8HURqPBqA3Wrk6NiOgL_O6rTM,35094
142
142
  qubx/trackers/sizers.py,sha256=OEK-IOuXiXXx8MkZiEsni5zPWFc3kun9AqApY0mIPTY,9527
143
143
  qubx/utils/__init__.py,sha256=FEPBtU3dhfLawBkAfm9FEUW4RuOY7pGCBfzDCtKjn9A,481
144
144
  qubx/utils/_pyxreloader.py,sha256=34kNd8kQi2ey_ZrGdVVUHbPrO1PEiHZDLEDBscIkT_s,12292
@@ -167,8 +167,8 @@ qubx/utils/runner/factory.py,sha256=eM4-Etcq-FewD2AjH_srFGzP413pm8er95KIZixXRpM,
167
167
  qubx/utils/runner/runner.py,sha256=m58a3kEwSs1xfgg_s9FwrQJ3AZV4Lf_VOmKDPQdaWH8,31518
168
168
  qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
169
169
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
170
- qubx-0.6.60.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
171
- qubx-0.6.60.dist-info/METADATA,sha256=L4IeWneQm6vFdNN7sBGVRwsfKZhF1eLmSvqxyPW5Q5E,4612
172
- qubx-0.6.60.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
173
- qubx-0.6.60.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
174
- qubx-0.6.60.dist-info/RECORD,,
170
+ qubx-0.6.61.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
171
+ qubx-0.6.61.dist-info/METADATA,sha256=HiAGYDLHoEaw1Sb5SCtJxOH_rx9EpmAjV18myTkoNZQ,4612
172
+ qubx-0.6.61.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
173
+ qubx-0.6.61.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
174
+ qubx-0.6.61.dist-info/RECORD,,
File without changes
File without changes