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

@@ -44,10 +44,12 @@ class CcxtDataProvider(IDataProvider):
44
44
 
45
45
  # - subscriptions
46
46
  _subscriptions: Dict[str, Set[Instrument]]
47
+ _pending_subscriptions: Dict[str, Set[Instrument]] # Track subscriptions being established
47
48
  _sub_to_coro: Dict[str, concurrent.futures.Future]
48
49
  _sub_to_name: Dict[str, str]
49
50
  _sub_to_unsubscribe: Dict[str, Callable[[], Awaitable[None]]]
50
51
  _is_sub_name_enabled: Dict[str, bool]
52
+ _sub_connection_ready: Dict[str, bool] # Track if connection is actually ready
51
53
 
52
54
  _sub_instr_to_time: Dict[Tuple[str, Instrument], dt_64]
53
55
  _last_quotes: Dict[Instrument, Optional[Quote]]
@@ -80,10 +82,12 @@ class CcxtDataProvider(IDataProvider):
80
82
 
81
83
  self._last_quotes = defaultdict(lambda: None)
82
84
  self._subscriptions = defaultdict(set)
85
+ self._pending_subscriptions = defaultdict(set)
83
86
  self._sub_to_coro = {}
84
87
  self._sub_to_name = {}
85
88
  self._sub_to_unsubscribe = {}
86
89
  self._is_sub_name_enabled = defaultdict(lambda: False)
90
+ self._sub_connection_ready = defaultdict(lambda: False)
87
91
  self._symbol_to_instrument = {}
88
92
  self._subscribers = {
89
93
  n.split("_subscribe_")[1]: f
@@ -135,11 +139,33 @@ class CcxtDataProvider(IDataProvider):
135
139
  def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
136
140
  if not subscription_type:
137
141
  return list(self.subscribed_instruments)
138
- return list(self._subscriptions[subscription_type]) if subscription_type in self._subscriptions else []
142
+
143
+ # Return active subscriptions, fallback to pending if no active ones
144
+ _sub_type, _ = DataType.from_str(subscription_type)
145
+ if _sub_type in self._subscriptions:
146
+ return list(self._subscriptions[_sub_type])
147
+ elif _sub_type in self._pending_subscriptions:
148
+ return list(self._pending_subscriptions[_sub_type])
149
+ else:
150
+ return []
139
151
 
140
152
  def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
141
153
  sub_type, _ = DataType.from_str(subscription_type)
142
- return sub_type in self._subscriptions and instrument in self._subscriptions[sub_type]
154
+ # Only return True if subscription is actually active (not just pending)
155
+ return (
156
+ sub_type in self._subscriptions
157
+ and instrument in self._subscriptions[sub_type]
158
+ and self._sub_connection_ready.get(sub_type, False)
159
+ )
160
+
161
+ def has_pending_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
162
+ """Check if a subscription is pending (connection being established)."""
163
+ sub_type, _ = DataType.from_str(subscription_type)
164
+ return (
165
+ sub_type in self._pending_subscriptions
166
+ and instrument in self._pending_subscriptions[sub_type]
167
+ and not self._sub_connection_ready.get(subscription_type, False)
168
+ )
143
169
 
144
170
  def warmup(self, warmups: Dict[Tuple[str, Instrument], str]) -> None:
145
171
  _coros = []
@@ -207,9 +233,9 @@ class CcxtDataProvider(IDataProvider):
207
233
 
208
234
  @property
209
235
  def subscribed_instruments(self) -> Set[Instrument]:
210
- if not self._subscriptions:
211
- return set()
212
- return set.union(*self._subscriptions.values())
236
+ active = set.union(*self._subscriptions.values()) if self._subscriptions else set()
237
+ pending = set.union(*self._pending_subscriptions.values()) if self._pending_subscriptions else set()
238
+ return active.union(pending)
213
239
 
214
240
  @property
215
241
  def is_read_only(self) -> bool:
@@ -226,19 +252,29 @@ class CcxtDataProvider(IDataProvider):
226
252
  if _subscriber is None:
227
253
  raise ValueError(f"{self._exchange_id}: Subscription type {sub_type} is not supported")
228
254
 
255
+ # Save old subscription state before starting cleanup
256
+ old_sub_info = None
229
257
  if sub_type in self._sub_to_coro:
258
+ old_sub_info = {"name": self._sub_to_name[sub_type], "coro": self._sub_to_coro[sub_type]}
230
259
  logger.debug(
231
- f"<yellow>{self._exchange_id}</yellow> Canceling existing {sub_type} subscription for {self._subscriptions[_sub_type]}"
260
+ f"<yellow>{self._exchange_id}</yellow> Canceling existing {sub_type} subscription for {self._subscriptions.get(_sub_type, set())}"
232
261
  )
233
- # - wait for the subscriber to stop
234
- self._loop.submit(self._stop_subscriber(sub_type, self._sub_to_name[sub_type])).result()
262
+
263
+ # Clear state immediately to prevent interference with new subscription
235
264
  del self._sub_to_coro[sub_type]
236
265
  del self._sub_to_name[sub_type]
237
- del self._subscriptions[_sub_type]
266
+ # Clean up both active and pending subscriptions
267
+ self._subscriptions.pop(_sub_type, None)
268
+ self._pending_subscriptions.pop(_sub_type, None)
269
+ self._sub_connection_ready.pop(sub_type, None)
238
270
 
239
271
  if instruments is not None and len(instruments) == 0:
240
272
  return
241
273
 
274
+ # Mark subscription as pending (not active yet)
275
+ self._pending_subscriptions[_sub_type] = instruments
276
+ self._sub_connection_ready[sub_type] = False
277
+
242
278
  kwargs = {"instruments": instruments, **_params}
243
279
  _subscriber = self._subscribers[_sub_type]
244
280
  _subscriber_params = set(_subscriber.__code__.co_varnames[: _subscriber.__code__.co_argcount])
@@ -247,7 +283,12 @@ class CcxtDataProvider(IDataProvider):
247
283
  self._sub_to_name[sub_type] = (name := self._get_subscription_name(_sub_type, **kwargs))
248
284
  self._sub_to_coro[sub_type] = self._loop.submit(_subscriber(self, name, _sub_type, self.channel, **kwargs))
249
285
 
250
- self._subscriptions[_sub_type] = instruments
286
+ # Now stop the old subscriber after new one is started (to avoid interference)
287
+ if old_sub_info is not None:
288
+ # Stop old subscriber in background to avoid blocking
289
+ self._loop.submit(self._stop_old_subscriber(old_sub_info["name"], old_sub_info["coro"]))
290
+
291
+ # Don't set _subscriptions here - it will be set when connection is established
251
292
 
252
293
  def _time_msec_nbars_back(self, timeframe: str, nbarsback: int = 1) -> int:
253
294
  return (self.time_provider.time() - nbarsback * pd.Timedelta(timeframe)).asm8.item() // 1000000
@@ -278,6 +319,14 @@ class CcxtDataProvider(IDataProvider):
278
319
  _name += f" ({kwargs_str})"
279
320
  return _name
280
321
 
322
+ def _mark_subscription_active(self, sub_type: str) -> None:
323
+ """Mark a subscription as active once the WebSocket connection is established."""
324
+ _sub_type, _ = DataType.from_str(sub_type)
325
+ if _sub_type in self._pending_subscriptions:
326
+ self._subscriptions[_sub_type] = self._pending_subscriptions[_sub_type]
327
+ self._sub_connection_ready[sub_type] = True
328
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Subscription {sub_type} is now active")
329
+
281
330
  async def _stop_subscriber(self, sub_type: str, sub_name: str) -> None:
282
331
  try:
283
332
  self._is_sub_name_enabled[sub_name] = False # stop the subscriber
@@ -303,11 +352,57 @@ class CcxtDataProvider(IDataProvider):
303
352
  del self._sub_to_unsubscribe[sub_name]
304
353
 
305
354
  del self._is_sub_name_enabled[sub_name]
355
+
356
+ # Clean up connection state for this subscription
357
+ for sub_type, stream_name in list(self._sub_to_name.items()):
358
+ if stream_name == sub_name:
359
+ self._sub_connection_ready.pop(sub_type, None)
360
+ break
361
+
306
362
  logger.debug(f"<yellow>{self._exchange_id}</yellow> Unsubscribed from {sub_name}")
307
363
  except Exception as e:
308
364
  logger.error(f"<yellow>{self._exchange_id}</yellow> Error stopping {sub_name}")
309
365
  logger.exception(e)
310
366
 
367
+ async def _stop_old_subscriber(self, old_name: str, old_coro: concurrent.futures.Future) -> None:
368
+ """Stop an old subscriber safely without interfering with new subscriptions."""
369
+ try:
370
+ # Disable the old stream by name
371
+ self._is_sub_name_enabled[old_name] = False
372
+
373
+ # Wait for the old coroutine to finish
374
+ total_sleep_time = 0.0
375
+ while old_coro.running():
376
+ await asyncio.sleep(1.0)
377
+ total_sleep_time += 1.0
378
+ if total_sleep_time >= 20.0:
379
+ break
380
+
381
+ if old_coro.running():
382
+ logger.warning(
383
+ f"<yellow>{self._exchange_id}</yellow> Old subscriber {old_name} is still running. Cancelling it."
384
+ )
385
+ old_coro.cancel()
386
+ else:
387
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Old subscriber {old_name} has been stopped")
388
+
389
+ # Clean up old unsubscriber if it exists
390
+ if old_name in self._sub_to_unsubscribe:
391
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Calling old unsubscriber for {old_name}")
392
+ await self._sub_to_unsubscribe[old_name]()
393
+ # Use pop to safely remove, in case it was already removed
394
+ self._sub_to_unsubscribe.pop(old_name, None)
395
+
396
+ # Clean up old stream state
397
+ if old_name in self._is_sub_name_enabled:
398
+ del self._is_sub_name_enabled[old_name]
399
+
400
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Old subscription {old_name} cleaned up")
401
+
402
+ except Exception as e:
403
+ logger.error(f"<yellow>{self._exchange_id}</yellow> Error stopping old subscriber {old_name}")
404
+ logger.exception(e)
405
+
311
406
  async def _listen_to_stream(
312
407
  self,
313
408
  subscriber: Callable[[], Awaitable[None]],
@@ -322,10 +417,22 @@ class CcxtDataProvider(IDataProvider):
322
417
 
323
418
  self._is_sub_name_enabled[name] = True
324
419
  n_retry = 0
420
+ connection_established = False
421
+
325
422
  while channel.control.is_set() and self._is_sub_name_enabled[name]:
326
423
  try:
327
424
  await subscriber()
328
425
  n_retry = 0
426
+
427
+ # Mark subscription as active on first successful data reception
428
+ if not connection_established:
429
+ # Find the subscription type for this stream name
430
+ for sub_type, stream_name in self._sub_to_name.items():
431
+ if stream_name == name:
432
+ self._mark_subscription_active(sub_type)
433
+ connection_established = True
434
+ break
435
+
329
436
  if not self._is_sub_name_enabled[name]:
330
437
  break
331
438
  except CcxtSymbolNotRecognized:
@@ -616,7 +723,8 @@ class CcxtDataProvider(IDataProvider):
616
723
  for exch_symbol, ccxt_ticker in ccxt_tickers.items(): # type: ignore
617
724
  instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
618
725
  quote = ccxt_convert_ticker(ccxt_ticker)
619
- if self._last_quotes[instrument] is None or quote.time > self._last_quotes[instrument].time:
726
+ last_quote = self._last_quotes[instrument]
727
+ if last_quote is None or quote.time > last_quote.time:
620
728
  self._health_monitor.record_data_arrival(sub_type, dt_64(quote.time, "ns"))
621
729
  self._last_quotes[instrument] = quote
622
730
  channel.send((instrument, sub_type, quote, False))
@@ -86,7 +86,7 @@ class MarketManager(IMarketManager):
86
86
  _time = pd.Timestamp(self._time_provider.time())
87
87
  _timedelta = pd.Timedelta(timeframe)
88
88
  _last_bar_time = ohlc.index[-1]
89
- if _last_bar_time + _timedelta >= _time:
89
+ if _last_bar_time + _timedelta > _time:
90
90
  ohlc = ohlc.iloc[:-1]
91
91
 
92
92
  if length:
@@ -191,9 +191,9 @@ class ProcessingManager(IProcessingManager):
191
191
 
192
192
  # - if strategy still fitting - skip on_event call
193
193
  if self._fit_is_running:
194
- logger.debug(
195
- f"Skipping {self._strategy_name}::on_event({instrument}, {d_type}, [...], {is_historical}) fitting in progress (orders and deals processed)!"
196
- )
194
+ # logger.debug(
195
+ # f"Skipping {self._strategy_name}::on_event({instrument}, {d_type}, [...], {is_historical}) fitting in progress (orders and deals processed)!"
196
+ # )
197
197
  return False
198
198
 
199
199
  signals: list[Signal] | Signal = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.57
3
+ Version: 0.6.59
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -21,7 +21,7 @@ qubx/cli/tui.py,sha256=N15UiNEdnWOWYh8E9DNlQCDWdoyP6rMGMhEItogPW88,16491
21
21
  qubx/connectors/ccxt/__init__.py,sha256=HEQ7lM9HS8sED_zfsAHrhFT7F9E7NFGAecwZwNr-TDE,65
22
22
  qubx/connectors/ccxt/account.py,sha256=HILqsSPfor58NrlP0qYwO5lkNZzUBG-SR5Hy1OSa7_M,24308
23
23
  qubx/connectors/ccxt/broker.py,sha256=Hg2tC3qKPYAURGro9ONzzR7QaiI41oytyKqQlpcM5Zw,16175
24
- qubx/connectors/ccxt/data.py,sha256=COVUh37ZdCUjiDB0a38Cj9SNSV8P95mqG2B3Gc_fQ2U,30172
24
+ qubx/connectors/ccxt/data.py,sha256=gH1ug0vFvbEyUnHvgwlujsJdWbg9RXopIX3_fQhzApk,35456
25
25
  qubx/connectors/ccxt/exceptions.py,sha256=OfZc7iMdEG8uLorcZta2NuEuJrSIqi0FG7IICmwF54M,262
26
26
  qubx/connectors/ccxt/exchanges/__init__.py,sha256=dEBkyeiGEQgfyuGVhhx4ZTRIlU9e_H1m6K2ROXRpIi8,1884
27
27
  qubx/connectors/ccxt/exchanges/binance/broker.py,sha256=BB2V82zaOm1EjP3GrsOqQQMeGpml6-w23iv7goKrjyU,2111
@@ -48,16 +48,16 @@ qubx/core/loggers.py,sha256=85Xgt1-Hh-2gAlJez3TxTHr32KSWYNqNhmbeWZwhw0o,13340
48
48
  qubx/core/lookups.py,sha256=2UmODxDeDQqi-xHOvjm2_GckC4piKI3x4ZEf5vny8YI,18275
49
49
  qubx/core/metrics.py,sha256=74xIecCvlxVXl0gy0JvgjJ2X5gg-RMmVZw9hQikkHE0,60269
50
50
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
51
- qubx/core/mixins/market.py,sha256=MH7A2jZFEClrfeioio1lPgYBbxVZCL1Af-b-DSFvSM4,4567
52
- qubx/core/mixins/processing.py,sha256=pgzDCrB9A6SvsWaZ1Op-M22yvcWy6Y8L1IIef6rwpGM,30655
51
+ qubx/core/mixins/market.py,sha256=f0PJTK5Y9nQkV2XWxytixFG1Oc6ihYVwlxIMraTnmms,4566
52
+ qubx/core/mixins/processing.py,sha256=OBsr-2c-W_Iy68tL80wFqTPN3HnW1stDjpm77vFaSB0,30661
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=tsMpBriLHwK9lAVYvIrO94EIx8_ETSXUlzxN_sDOsL8,9838
56
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=CtCikl3YWB60CW205Dm9MwlVtjgcB5zuj5sXZ0PEpyo,978280
56
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=V-Kj9bhekUUV4wX6Ao1JzXG5tzHeaqruPLjG7Lo1i_A,978280
57
57
  qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
58
58
  qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
59
59
  qubx/core/series.pyx,sha256=ZQPp1-Kp2lX22n04gaBWak9mW139e-uc8bhsbZMohgs,46507
60
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=Sg9UQCj4hovrE6dWSuTEbss5s82m19b9ScA6ECE7tQ4,86568
60
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=KpHSsIjpXpVCQ8BO8gqFT64DRPezuaYxvOJ8T4kGeVY,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
@@ -129,7 +129,7 @@ qubx/restorers/signal.py,sha256=0QFoy7OzDkK6AAmJEbbmSsHwmAhjMJYYggVFuLraKjk,1089
129
129
  qubx/restorers/state.py,sha256=dLaVnUwRCNRkUqbYyi0RfZs3Q3AdglkI_qTtQ8GDD5Y,7289
130
130
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
131
131
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=RxL_qH6tyCoHddeu7BwOmhkmV2aWPWGafq-My49AoX0,654440
132
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=OV84KH_YT8D_SIQjCZWERFdmBMB4o38Q8tqdDBjSpb8,654440
133
133
  qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
134
134
  qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
135
135
  qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
@@ -166,8 +166,8 @@ qubx/utils/runner/factory.py,sha256=eM4-Etcq-FewD2AjH_srFGzP413pm8er95KIZixXRpM,
166
166
  qubx/utils/runner/runner.py,sha256=PnSQ_soWXzjcIYxG5HHUtbZnWSaoeSXr7VxbQWtOsII,31444
167
167
  qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
168
168
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
169
- qubx-0.6.57.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
170
- qubx-0.6.57.dist-info/METADATA,sha256=udbK6RAZPp6B3-XXug773pK6FZg3PFnYuUFuoYzrbOE,4612
171
- qubx-0.6.57.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
172
- qubx-0.6.57.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
173
- qubx-0.6.57.dist-info/RECORD,,
169
+ qubx-0.6.59.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
170
+ qubx-0.6.59.dist-info/METADATA,sha256=kXg2STWUJpn3dNfxzEngSiV7OMDN1unJIbcrjKzTTEE,4612
171
+ qubx-0.6.59.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
172
+ qubx-0.6.59.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
173
+ qubx-0.6.59.dist-info/RECORD,,
File without changes
File without changes