Qubx 0.6.88__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.90__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/backtester/broker.py CHANGED
@@ -6,7 +6,7 @@ from qubx.core.basics import (
6
6
  Instrument,
7
7
  Order,
8
8
  )
9
- from qubx.core.exceptions import OrderNotFound
9
+ from qubx.core.exceptions import BadRequest, OrderNotFound
10
10
  from qubx.core.interfaces import IBroker
11
11
 
12
12
  from .account import SimulatedAccountProcessor
@@ -64,20 +64,66 @@ class SimulatedBroker(IBroker):
64
64
  ) -> None:
65
65
  self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
66
66
 
67
- def cancel_order(self, order_id: str) -> Order | None:
67
+ def cancel_order(self, order_id: str) -> bool:
68
+ """Cancel an order synchronously and return success status."""
68
69
  try:
69
70
  self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
70
- return order_update.order if order_update is not None else None
71
+ return order_update is not None
71
72
  except OrderNotFound:
72
73
  # Order was already cancelled or doesn't exist
73
74
  logger.debug(f"Order {order_id} not found")
74
- return None
75
+ return False
76
+
77
+ def cancel_order_async(self, order_id: str) -> None:
78
+ """Cancel an order asynchronously (fire-and-forget)."""
79
+ # For simulation, async is same as sync since it's fast
80
+ self.cancel_order(order_id)
75
81
 
76
82
  def cancel_orders(self, instrument: Instrument) -> None:
77
83
  raise NotImplementedError("Not implemented yet")
78
84
 
79
- def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
80
- raise NotImplementedError("Not implemented yet")
85
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
86
+ """Update an existing limit order using cancel+recreate strategy.
87
+
88
+ Args:
89
+ order_id: The ID of the order to update
90
+ price: New price for the order
91
+ amount: New amount for the order
92
+
93
+ Returns:
94
+ Order: The updated (newly created) order object
95
+
96
+ Raises:
97
+ OrderNotFound: If the order is not found
98
+ BadRequest: If the order is not a limit order
99
+ """
100
+ # Get the existing order from account
101
+ active_orders = self._account.get_orders()
102
+ existing_order = active_orders.get(order_id)
103
+ if not existing_order:
104
+ raise OrderNotFound(f"Order {order_id} not found")
105
+
106
+ # Validate that it's a limit order
107
+ if existing_order.type.lower() != "limit":
108
+ raise BadRequest(
109
+ f"Order {order_id} is not a limit order (type: {existing_order.type}). Only limit orders can be updated."
110
+ )
111
+
112
+ # Cancel the existing order first
113
+ self.cancel_order(order_id)
114
+
115
+ # Create a new order with updated parameters, preserving original properties
116
+ updated_order = self.send_order(
117
+ instrument=existing_order.instrument,
118
+ order_side=existing_order.side,
119
+ order_type="limit",
120
+ amount=abs(amount),
121
+ price=price,
122
+ client_id=existing_order.client_id, # Preserve original client_id for tracking
123
+ time_in_force=existing_order.time_in_force or "gtc",
124
+ )
125
+
126
+ return updated_order
81
127
 
82
128
  def _send_execution_report(self, report: SimulatedExecutionReport | None):
83
129
  if report is None:
@@ -5,7 +5,6 @@ from typing import Any
5
5
  import pandas as pd
6
6
 
7
7
  import ccxt
8
-
9
8
  from ccxt.base.errors import ExchangeError
10
9
  from qubx import logger
11
10
  from qubx.core.basics import (
@@ -15,7 +14,7 @@ from qubx.core.basics import (
15
14
  OrderSide,
16
15
  )
17
16
  from qubx.core.errors import ErrorLevel, OrderCancellationError, OrderCreationError, create_error_event
18
- from qubx.core.exceptions import BadRequest, InvalidOrderParameters
17
+ from qubx.core.exceptions import BadRequest, InvalidOrderParameters, OrderNotFound
19
18
  from qubx.core.interfaces import (
20
19
  IAccountProcessor,
21
20
  IBroker,
@@ -61,7 +60,6 @@ class CcxtBroker(IBroker):
61
60
  """Get current AsyncThreadLoop for the exchange."""
62
61
  return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
63
62
 
64
-
65
63
  @property
66
64
  def is_simulated_trading(self) -> bool:
67
65
  return False
@@ -178,7 +176,7 @@ class CcxtBroker(IBroker):
178
176
  client_id: str | None = None,
179
177
  time_in_force: str = "gtc",
180
178
  **options,
181
- ) -> Order | None:
179
+ ) -> Order:
182
180
  """
183
181
  Submit an order and wait for the result. Exceptions will be raised on errors.
184
182
 
@@ -221,22 +219,38 @@ class CcxtBroker(IBroker):
221
219
  self._post_order_error_to_databus(
222
220
  err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
223
221
  )
224
- return None
222
+ raise err
225
223
 
226
- def cancel_order(self, order_id: str) -> Order | None:
224
+ def cancel_order(self, order_id: str) -> bool:
225
+ """Cancel an order synchronously and return success status."""
227
226
  orders = self.account.get_orders()
228
227
  if order_id not in orders:
229
228
  logger.warning(f"Order {order_id} not found in active orders")
230
- return None
229
+ return False
231
230
 
232
231
  order = orders[order_id]
233
- logger.info(f"Canceling order {order_id} ...")
232
+ logger.info(f"Canceling order {order_id} synchronously...")
234
233
 
235
- # Submit the cancellation task to the async loop without waiting for the result
236
- self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
234
+ try:
235
+ # Submit the task and wait for result
236
+ future = self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
237
+ return future.result() # This will block until completion or timeout
238
+ except Exception as e:
239
+ logger.error(f"Error during synchronous order cancellation: {e}")
240
+ return False # Return False on any error for simplicity
241
+
242
+ def cancel_order_async(self, order_id: str) -> None:
243
+ """Cancel an order asynchronously (non blocking)."""
244
+ orders = self.account.get_orders()
245
+ if order_id not in orders:
246
+ logger.warning(f"Order {order_id} not found in active orders")
247
+ return
237
248
 
238
- # Always return None as requested
239
- return None
249
+ order = orders[order_id]
250
+ logger.info(f"Canceling order {order_id} asynchronously...")
251
+
252
+ # Submit the task without waiting for result
253
+ self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
240
254
 
241
255
  async def _create_order(
242
256
  self,
@@ -298,8 +312,22 @@ class CcxtBroker(IBroker):
298
312
  logger.warning(f"[<y>{instrument.symbol}</y>] :: Quote is not available for order creation.")
299
313
  raise BadRequest(f"Quote is not available for order creation for {instrument.symbol}")
300
314
 
301
- # TODO: think about automatically setting reduce only when needed
302
- if not (reduce_only := options.get("reduceOnly", False)):
315
+ # Auto-detect if order reduces existing position
316
+ reduce_only = options.get("reduceOnly", False)
317
+ if not reduce_only:
318
+ positions = self.account.get_positions()
319
+ if instrument in positions:
320
+ position_qty = positions[instrument].quantity
321
+ # Check if order closes position AND doesn't exceed position size (which would flip to opposite side)
322
+ if (position_qty > 0 and order_side == "SELL" and abs(amount) <= abs(position_qty)) or (
323
+ position_qty < 0 and order_side == "BUY" and abs(amount) <= abs(position_qty)
324
+ ):
325
+ reduce_only = True
326
+ logger.debug(
327
+ f"[{instrument.symbol}] Auto-setting reduceOnly=True ({order_side}, position: {position_qty})"
328
+ )
329
+
330
+ if not reduce_only:
303
331
  min_notional = instrument.min_notional
304
332
  if min_notional > 0 and abs(amount) * quote.mid_price() < min_notional:
305
333
  raise InvalidOrderParameters(
@@ -364,9 +392,13 @@ class CcxtBroker(IBroker):
364
392
  while True:
365
393
  try:
366
394
  if self.enable_cancel_order_ws:
367
- await self._exchange_manager.exchange.cancel_order_ws(order_id, symbol=instrument_to_ccxt_symbol(instrument))
395
+ await self._exchange_manager.exchange.cancel_order_ws(
396
+ order_id, symbol=instrument_to_ccxt_symbol(instrument)
397
+ )
368
398
  else:
369
- await self._exchange_manager.exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(instrument))
399
+ await self._exchange_manager.exchange.cancel_order(
400
+ order_id, symbol=instrument_to_ccxt_symbol(instrument)
401
+ )
370
402
  return True
371
403
  except ccxt.OperationRejected as err:
372
404
  err_msg = str(err).lower()
@@ -375,8 +407,12 @@ class CcxtBroker(IBroker):
375
407
  # These errors might be temporary if the order is still being processed, so retry
376
408
  logger.debug(f"[{order_id}] Order not found for cancellation, might retry: {err}")
377
409
  # Continue with the retry logic instead of returning immediately
410
+ # Order cannot be cancelled (e.g., already filled)
411
+ elif "filled" in err_msg or "partially filled" in err_msg:
412
+ logger.debug(f"[{order_id}] Order cannot be cancelled - already executed: {err}")
413
+ return False # FAILURE: Order cannot be cancelled
414
+ # Other operation rejected errors - don't retry
378
415
  else:
379
- # For other operation rejected errors, don't retry
380
416
  logger.debug(f"[{order_id}] Could not cancel order: {err}")
381
417
  return False
382
418
  except (ccxt.NetworkError, ccxt.ExchangeError, ccxt.ExchangeNotAvailable) as e:
@@ -424,8 +460,112 @@ class CcxtBroker(IBroker):
424
460
  for order_id in instrument_orders:
425
461
  self.cancel_order(order_id)
426
462
 
427
- def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
428
- raise NotImplementedError("Not implemented yet")
463
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
464
+ """Update an existing limit order with new price and amount.
465
+
466
+ Args:
467
+ order_id: The ID of the order to update
468
+ price: New price for the order (already adjusted by TradingManager)
469
+ amount: New amount for the order (already adjusted by TradingManager)
470
+
471
+ Returns:
472
+ Order: The updated Order object if successful
473
+
474
+ Raises:
475
+ OrderNotFound: If the order is not found
476
+ BadRequest: If the order is not a limit order
477
+ ExchangeError: If the exchange operation fails
478
+ """
479
+ logger.debug(f"Updating order {order_id} with price={price}, amount={amount}")
480
+
481
+ active_orders = self.account.get_orders()
482
+ if order_id not in active_orders:
483
+ raise OrderNotFound(f"Order {order_id} not found in active orders")
484
+
485
+ existing_order = active_orders[order_id]
486
+
487
+ # Validate that the order can still be updated (not fully filled/closed)
488
+ updatable_statuses = ["OPEN", "NEW", "PENDING"]
489
+ if existing_order.status not in updatable_statuses:
490
+ raise BadRequest(
491
+ f"Order {order_id} with status '{existing_order.status}' cannot be updated. "
492
+ f"Only orders with status {updatable_statuses} can be updated."
493
+ )
494
+
495
+ instrument = existing_order.instrument
496
+
497
+ logger.debug(
498
+ f"[<g>{instrument.symbol}</g>] :: Updating order {order_id}: "
499
+ f"{amount} @ {price} (was: {existing_order.quantity} @ {existing_order.price})"
500
+ )
501
+
502
+ try:
503
+ # Check if exchange supports order editing
504
+ if self._exchange_manager.exchange.has.get("editOrder", False):
505
+ return self._update_order_direct(order_id, existing_order, price, amount)
506
+ else:
507
+ return self._update_order_fallback(order_id, existing_order, price, amount)
508
+ except Exception as err:
509
+ logger.error(f"Failed to update order {order_id}: {err}")
510
+ raise
511
+
512
+ def _update_order_direct(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
513
+ """Update order using exchange's native edit functionality."""
514
+ logger.debug(f"Using direct order update for {order_id}")
515
+
516
+ future_result = self._loop.submit(self._edit_order_async(order_id, existing_order, price, amount))
517
+ updated_order, error = future_result.result()
518
+
519
+ if error is not None:
520
+ raise error
521
+
522
+ if updated_order is not None:
523
+ self.account.process_order(updated_order)
524
+ logger.debug(f"Direct update successful for order {order_id}")
525
+ return updated_order
526
+ else:
527
+ raise Exception("Order update returned None without error")
528
+
529
+ def _update_order_fallback(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
530
+ """Update order using cancel+recreate strategy for exchanges without editOrder support."""
531
+ logger.debug(f"Using fallback (cancel+recreate) strategy for order {order_id}")
532
+
533
+ success = self.cancel_order(order_id)
534
+ if not success:
535
+ raise Exception(f"Failed to cancel order {order_id} during update")
536
+
537
+ updated_order = self.send_order(
538
+ instrument=existing_order.instrument,
539
+ order_side=existing_order.side,
540
+ order_type=existing_order.type,
541
+ amount=amount,
542
+ price=price,
543
+ client_id=existing_order.client_id, # Preserve original client_id for tracking
544
+ time_in_force=existing_order.time_in_force or "gtc",
545
+ )
546
+
547
+ logger.debug(f"Fallback update successful for order {order_id} -> new order {updated_order.id}")
548
+ return updated_order
549
+
550
+ async def _edit_order_async(
551
+ self, order_id: str, existing_order: Order, price: float, amount: float
552
+ ) -> tuple[Order | None, Exception | None]:
553
+ """Async helper for direct order editing."""
554
+ try:
555
+ ccxt_symbol = instrument_to_ccxt_symbol(existing_order.instrument)
556
+ ccxt_side = "buy" if existing_order.side == "BUY" else "sell"
557
+
558
+ result = await self._exchange_manager.exchange.edit_order(
559
+ id=order_id, symbol=ccxt_symbol, type="limit", side=ccxt_side, amount=amount, price=price, params={}
560
+ )
561
+
562
+ # Convert the result back to our Order format
563
+ updated_order = ccxt_convert_order_info(existing_order.instrument, result)
564
+ return updated_order, None
565
+
566
+ except Exception as err:
567
+ logger.error(f"Async edit order failed for {order_id}: {err}")
568
+ return None, err
429
569
 
430
570
  def exchange(self) -> str:
431
571
  """
@@ -18,8 +18,6 @@ from qubx.core.interfaces import IDataArrivalListener
18
18
 
19
19
  # Constants for better maintainability
20
20
  DEFAULT_CHECK_INTERVAL_SECONDS = 60.0
21
- DEFAULT_MAX_RECREATIONS = 5
22
- DEFAULT_RESET_INTERVAL_HOURS = 6.0
23
21
  SECONDS_PER_HOUR = 3600
24
22
 
25
23
  # Custom stall detection thresholds (in seconds)
@@ -30,7 +28,7 @@ STALL_THRESHOLDS = {
30
28
  "trade": 60 * 60, # 60 minutes = 3,600s
31
29
  "liquidation": 7 * 24 * SECONDS_PER_HOUR, # 7 days = 604,800s
32
30
  "ohlc": 5 * 60, # 5 minutes = 300s
33
- "quote": 5 * 60, # 5 minutes = 300s
31
+ "quote": 2 * 60, # 2 minutes = 120s
34
32
  }
35
33
  DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
36
34
 
@@ -45,7 +43,7 @@ class ExchangeManager(IDataArrivalListener):
45
43
  Key Features:
46
44
  - Explicit .exchange property for CCXT access
47
45
  - Self-contained stall detection and recreation triggering
48
- - Circuit breaker protection with recreation limits
46
+ - Automatic recreation without limits when data stalls
49
47
  - Atomic exchange transitions during recreation
50
48
  - Background monitoring thread for stall detection
51
49
  """
@@ -57,8 +55,6 @@ class ExchangeManager(IDataArrivalListener):
57
55
  exchange_name: str,
58
56
  factory_params: dict[str, Any],
59
57
  initial_exchange: Optional[cxp.Exchange] = None,
60
- max_recreations: int = DEFAULT_MAX_RECREATIONS,
61
- reset_interval_hours: float = DEFAULT_RESET_INTERVAL_HOURS,
62
58
  check_interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
63
59
  ):
64
60
  """Initialize ExchangeManager with underlying CCXT exchange.
@@ -67,19 +63,14 @@ class ExchangeManager(IDataArrivalListener):
67
63
  exchange_name: Exchange name for factory (e.g., "binance.um")
68
64
  factory_params: Parameters for get_ccxt_exchange()
69
65
  initial_exchange: Pre-created exchange instance (from factory)
70
- max_recreations: Maximum recreation attempts before giving up
71
- reset_interval_hours: Hours between recreation count resets
72
66
  check_interval_seconds: How often to check for stalls (default: 60.0)
73
67
  """
74
68
  self._exchange_name = exchange_name
75
69
  self._factory_params = factory_params.copy()
76
- self._max_recreations = max_recreations
77
- self._reset_interval_hours = reset_interval_hours
78
70
 
79
71
  # Recreation state
80
- self._recreation_count = 0
81
72
  self._recreation_lock = threading.RLock()
82
- self._last_successful_reset = time.time()
73
+ self._recreation_count = 0 # Track for logging purposes only
83
74
 
84
75
  # Stall detection state
85
76
  self._check_interval = check_interval_seconds
@@ -142,28 +133,19 @@ class ExchangeManager(IDataArrivalListener):
142
133
 
143
134
  def force_recreation(self) -> bool:
144
135
  """
145
- Force recreation due to data stalls (called by BaseHealthMonitor).
136
+ Force recreation due to data stalls.
146
137
 
147
138
  Returns:
148
- True if recreation successful, False if failed/limit exceeded
139
+ True if recreation successful, False if failed
149
140
  """
150
141
  with self._recreation_lock:
151
- # Check recreation limit
152
- if self._recreation_count >= self._max_recreations:
153
- logger.error(
154
- f"Cannot recreate {self._exchange_name}: recreation limit ({self._max_recreations}) exceeded"
155
- )
156
- return False
157
-
158
142
  logger.info(f"Stall-triggered recreation for {self._exchange_name}")
159
143
  return self._recreate_exchange()
160
144
 
161
145
  def _recreate_exchange(self) -> bool:
162
146
  """Recreate the underlying exchange (must be called with _recreation_lock held)."""
163
147
  self._recreation_count += 1
164
- logger.warning(
165
- f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count}/{self._max_recreations})"
166
- )
148
+ logger.warning(f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count})")
167
149
 
168
150
  # Create new exchange
169
151
  try:
@@ -190,18 +172,6 @@ class ExchangeManager(IDataArrivalListener):
190
172
 
191
173
  return True
192
174
 
193
- def reset_recreation_count_if_needed(self) -> None:
194
- """Reset recreation count periodically (called by monitoring loop)."""
195
- reset_interval_seconds = self._reset_interval_hours * SECONDS_PER_HOUR
196
-
197
- current_time = time.time()
198
- time_since_reset = current_time - self._last_successful_reset
199
-
200
- if time_since_reset >= reset_interval_seconds and self._recreation_count > 0:
201
- logger.info(f"Resetting recreation count for {self._exchange_name} (was {self._recreation_count})")
202
- self._recreation_count = 0
203
- self._last_successful_reset = current_time
204
-
205
175
  def on_data_arrival(self, event_type: str, event_time: dt_64) -> None:
206
176
  """Record data arrival for stall detection.
207
177
 
@@ -254,7 +224,6 @@ class ExchangeManager(IDataArrivalListener):
254
224
  while self._monitoring_enabled:
255
225
  try:
256
226
  self._check_and_handle_stalls()
257
- self.reset_recreation_count_if_needed()
258
227
  time.sleep(self._check_interval)
259
228
  except Exception as e:
260
229
  logger.error(f"Error in ExchangeManager stall detection: {e}")
@@ -85,8 +85,6 @@ def get_ccxt_exchange_manager(
85
85
  secret: str | None = None,
86
86
  loop: asyncio.AbstractEventLoop | None = None,
87
87
  use_testnet: bool = False,
88
- max_recreations: int = 3,
89
- reset_interval_hours: float = 24.0,
90
88
  check_interval_seconds: float = 30.0,
91
89
  **kwargs,
92
90
  ) -> ExchangeManager:
@@ -102,8 +100,6 @@ def get_ccxt_exchange_manager(
102
100
  secret (str, optional): The API secret. Default is None.
103
101
  loop (asyncio.AbstractEventLoop, optional): Event loop. Default is None.
104
102
  use_testnet (bool): Use testnet/sandbox mode. Default is False.
105
- max_recreations (int): Maximum recreation attempts before circuit breaker. Default is 3.
106
- reset_interval_hours (float): Hours between recreation count resets. Default is 24.0.
107
103
  check_interval_seconds (float): How often to check for stalls. Default is 30.0.
108
104
  **kwargs: Additional parameters for exchange configuration.
109
105
 
@@ -117,9 +113,7 @@ def get_ccxt_exchange_manager(
117
113
  'secret': secret,
118
114
  'loop': loop,
119
115
  'use_testnet': use_testnet,
120
- **{k: v for k, v in kwargs.items() if k not in {
121
- 'max_recreations', 'reset_interval_hours', 'check_interval_seconds'
122
- }}
116
+ **{k: v for k, v in kwargs.items() if k != 'check_interval_seconds'}
123
117
  }
124
118
 
125
119
  # Create raw CCXT exchange using public factory method
@@ -137,8 +131,6 @@ def get_ccxt_exchange_manager(
137
131
  exchange_name=exchange,
138
132
  factory_params=factory_params,
139
133
  initial_exchange=ccxt_exchange,
140
- max_recreations=max_recreations,
141
- reset_interval_hours=reset_interval_hours,
142
134
  check_interval_seconds=check_interval_seconds,
143
135
  )
144
136
 
qubx/core/context.py CHANGED
@@ -468,12 +468,22 @@ class StrategyContext(IStrategyContext):
468
468
  def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
469
469
  return self._trading_manager.close_positions(market_type, without_signals)
470
470
 
471
- def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
471
+ def cancel_order(self, order_id: str, exchange: str | None = None) -> bool:
472
+ """Cancel a specific order synchronously."""
472
473
  return self._trading_manager.cancel_order(order_id, exchange)
473
474
 
474
- def cancel_orders(self, instrument: Instrument):
475
+ def cancel_order_async(self, order_id: str, exchange: str | None = None) -> None:
476
+ """Cancel a specific order asynchronously (non blocking)."""
477
+ return self._trading_manager.cancel_order_async(order_id, exchange)
478
+
479
+ def cancel_orders(self, instrument: Instrument) -> None:
480
+ """Cancel all orders for an instrument."""
475
481
  return self._trading_manager.cancel_orders(instrument)
476
482
 
483
+ def update_order(self, order_id: str, price: float, amount: float, exchange: str | None = None) -> Order:
484
+ """Update an existing limit order with new price and amount."""
485
+ return self._trading_manager.update_order(order_id, price, amount, exchange)
486
+
477
487
  # IUniverseManager delegation
478
488
  def set_universe(
479
489
  self, instruments: list[Instrument], skip_callback: bool = False, if_has_position_then: RemovalPolicy = "close"
qubx/core/helpers.py CHANGED
@@ -237,12 +237,20 @@ class CachedMarketDataHolder:
237
237
  bought_volume_quote = volume_quote if trade.side == 1 else 0.0
238
238
  for ser in series.values():
239
239
  if len(ser) > 0:
240
- current_bar_start = floor_t64(np.datetime64(ser[0].time, 'ns'), np.timedelta64(ser.timeframe, 'ns'))
241
- trade_bar_start = floor_t64(np.datetime64(trade.time, 'ns'), np.timedelta64(ser.timeframe, 'ns'))
240
+ current_bar_start = floor_t64(np.datetime64(ser[0].time, "ns"), np.timedelta64(ser.timeframe, "ns"))
241
+ trade_bar_start = floor_t64(np.datetime64(trade.time, "ns"), np.timedelta64(ser.timeframe, "ns"))
242
242
  if trade_bar_start < current_bar_start:
243
243
  # Trade belongs to a previous bar - skip it
244
244
  continue
245
- ser.update(trade.time, trade.price, total_vol, bought_vol, volume_quote, bought_volume_quote, 1)
245
+ ser.update(
246
+ trade.time,
247
+ trade.price,
248
+ volume=total_vol,
249
+ bvolume=bought_vol,
250
+ volume_quote=volume_quote,
251
+ bought_volume_quote=bought_volume_quote,
252
+ trade_count=1,
253
+ )
246
254
 
247
255
  def finalize_ohlc_for_instruments(self, time: dt_64, instruments: list[Instrument]):
248
256
  """
qubx/core/interfaces.py CHANGED
@@ -348,14 +348,25 @@ class IBroker:
348
348
  """
349
349
  raise NotImplementedError("send_order_async is not implemented")
350
350
 
351
- def cancel_order(self, order_id: str) -> None:
352
- """Cancel an existing order (non blocking).
351
+ def cancel_order(self, order_id: str) -> bool:
352
+ """Cancel an existing order synchronously.
353
353
 
354
354
  Args:
355
355
  order_id: The ID of the order to cancel.
356
+
357
+ Returns:
358
+ bool: True if cancellation was successful, False otherwise.
356
359
  """
357
360
  raise NotImplementedError("cancel_order is not implemented")
358
361
 
362
+ def cancel_order_async(self, order_id: str) -> None:
363
+ """Cancel an existing order asynchronously (non blocking).
364
+
365
+ Args:
366
+ order_id: The ID of the order to cancel.
367
+ """
368
+ raise NotImplementedError("cancel_order_async is not implemented")
369
+
359
370
  def cancel_orders(self, instrument: Instrument) -> None:
360
371
  """Cancel all orders for an instrument.
361
372
 
@@ -364,8 +375,8 @@ class IBroker:
364
375
  """
365
376
  raise NotImplementedError("cancel_orders is not implemented")
366
377
 
367
- def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
368
- """Update an existing order.
378
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
379
+ """Update an existing order with new price and amount.
369
380
 
370
381
  Args:
371
382
  order_id: The ID of the order to update.
@@ -378,7 +389,8 @@ class IBroker:
378
389
  Raises:
379
390
  NotImplementedError: If the method is not implemented
380
391
  OrderNotFound: If the order is not found
381
- BadRequest: If the request is invalid
392
+ BadRequest: If the request is invalid (e.g., not a limit order)
393
+ InvalidOrderParameters: If the order cannot be updated
382
394
  """
383
395
  raise NotImplementedError("update_order is not implemented")
384
396
 
@@ -698,11 +710,24 @@ class ITradingManager:
698
710
  """Close all positions."""
699
711
  ...
700
712
 
701
- def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
702
- """Cancel a specific order.
713
+ def cancel_order(self, order_id: str, exchange: str | None = None) -> bool:
714
+ """Cancel a specific order synchronously.
703
715
 
704
716
  Args:
705
717
  order_id: ID of the order to cancel
718
+ exchange: Exchange to cancel on (optional)
719
+
720
+ Returns:
721
+ bool: True if cancellation was successful, False otherwise.
722
+ """
723
+ ...
724
+
725
+ def cancel_order_async(self, order_id: str, exchange: str | None = None) -> None:
726
+ """Cancel a specific order asynchronously (non blocking).
727
+
728
+ Args:
729
+ order_id: ID of the order to cancel
730
+ exchange: Exchange to cancel on (optional)
706
731
  """
707
732
  ...
708
733
 
@@ -714,6 +739,25 @@ class ITradingManager:
714
739
  """
715
740
  ...
716
741
 
742
+ def update_order(self, order_id: str, price: float, amount: float, exchange: str | None = None) -> Order:
743
+ """Update an existing limit order with new price and amount.
744
+
745
+ Args:
746
+ order_id: ID of the order to update
747
+ price: New price for the order
748
+ amount: New amount for the order
749
+ exchange: Exchange to update on (optional, defaults to first exchange)
750
+
751
+ Returns:
752
+ Order: The updated order object
753
+
754
+ Raises:
755
+ OrderNotFound: If the order is not found
756
+ BadRequest: If the order is not a limit order or other validation errors
757
+ InvalidOrderParameters: If the update parameters are invalid
758
+ """
759
+ ...
760
+
717
761
  def exchanges(self) -> list[str]: ...
718
762
 
719
763
 
@@ -159,7 +159,7 @@ class TradingManager(ITradingManager):
159
159
  def close_position(self, instrument: Instrument, without_signals: bool = False) -> None:
160
160
  position = self._account.get_position(instrument)
161
161
 
162
- if not position.is_open():
162
+ if position.quantity == 0:
163
163
  logger.debug(f"[<g>{instrument.symbol}</g>] :: Position already closed or zero size")
164
164
  return
165
165
 
@@ -196,13 +196,36 @@ class TradingManager(ITradingManager):
196
196
  for instrument in positions_to_close:
197
197
  self.close_position(instrument, without_signals)
198
198
 
199
- def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
199
+ def cancel_order(self, order_id: str, exchange: str | None = None) -> bool:
200
+ """Cancel a specific order synchronously."""
201
+ if not order_id:
202
+ return False
203
+ if exchange is None:
204
+ exchange = self._brokers[0].exchange()
205
+ try:
206
+ success = self._get_broker(exchange).cancel_order(order_id)
207
+ if success:
208
+ self._account.remove_order(order_id, exchange)
209
+ return success
210
+ except OrderNotFound:
211
+ # Order was already cancelled or doesn't exist
212
+ # Still try to remove it from account to keep state consistent
213
+ self._account.remove_order(order_id, exchange)
214
+ return False # Return False since order wasn't found
215
+ except Exception as e:
216
+ logger.error(f"Error canceling order {order_id}: {e}")
217
+ return False # Return False for any other errors
218
+
219
+ def cancel_order_async(self, order_id: str, exchange: str | None = None) -> None:
220
+ """Cancel a specific order asynchronously (non blocking)."""
200
221
  if not order_id:
201
222
  return
202
223
  if exchange is None:
203
224
  exchange = self._brokers[0].exchange()
204
225
  try:
205
- self._get_broker(exchange).cancel_order(order_id)
226
+ self._get_broker(exchange).cancel_order_async(order_id)
227
+ # Note: For async, we remove the order optimistically
228
+ # The actual removal will be confirmed via order status updates
206
229
  self._account.remove_order(order_id, exchange)
207
230
  except OrderNotFound:
208
231
  # Order was already cancelled or doesn't exist
@@ -213,6 +236,49 @@ class TradingManager(ITradingManager):
213
236
  for o in self._account.get_orders(instrument).values():
214
237
  self.cancel_order(o.id, instrument.exchange)
215
238
 
239
+ def update_order(self, order_id: str, price: float, amount: float, exchange: str | None = None) -> Order:
240
+ """Update an existing limit order with new price and amount."""
241
+ if not order_id:
242
+ raise ValueError("Order ID is required")
243
+ if exchange is None:
244
+ exchange = self._brokers[0].exchange()
245
+
246
+ # Get the existing order to determine instrument for adjustments
247
+ active_orders = self._account.get_orders()
248
+ existing_order = active_orders.get(order_id)
249
+ if not existing_order:
250
+ # Let broker handle the OrderNotFound - just pass through
251
+ logger.debug(f"Updating order {order_id}: {amount} @ {price} on {exchange}")
252
+ else:
253
+ # Apply TradingManager-level adjustments before sending to broker
254
+ instrument = existing_order.instrument
255
+ adjusted_amount = self._adjust_size(instrument, amount)
256
+ adjusted_price = self._adjust_price(instrument, price, amount)
257
+ if adjusted_price is None:
258
+ raise ValueError(f"Price adjustment failed for {instrument.symbol}")
259
+
260
+ logger.debug(
261
+ f"[<g>{instrument.symbol}</g>] :: Updating order {order_id}: "
262
+ f"{adjusted_amount} @ {adjusted_price} (was: {existing_order.quantity} @ {existing_order.price})"
263
+ )
264
+
265
+ # Update the values to use adjusted ones
266
+ amount = adjusted_amount
267
+ price = adjusted_price
268
+
269
+ try:
270
+ updated_order = self._get_broker(exchange).update_order(order_id, price, amount)
271
+
272
+ if updated_order is not None:
273
+ # Update account tracking with new order info
274
+ self._account.process_order(updated_order)
275
+ logger.info(f"[<g>{updated_order.instrument.symbol}</g>] :: Successfully updated order {order_id}")
276
+
277
+ return updated_order
278
+ except Exception as e:
279
+ logger.error(f"Error updating order {order_id}: {e}")
280
+ raise e
281
+
216
282
  def _generate_order_client_id(self, symbol: str) -> str:
217
283
  return self._client_id_store.generate_id(self._context, symbol)
218
284
 
@@ -19,6 +19,7 @@ class TargetPositionFormatter(DefaultFormatter):
19
19
  self,
20
20
  alert_name: str,
21
21
  exchange_mapping: Optional[dict[str, str]] = None,
22
+ account: Optional[IAccountViewer] = None,
22
23
  ):
23
24
  """
24
25
  Initialize the TargetPositionFormatter.
@@ -27,6 +28,7 @@ class TargetPositionFormatter(DefaultFormatter):
27
28
  alert_name: The name of the alert to include in the messages
28
29
  exchange_mapping: Optional mapping of exchange names to use in messages.
29
30
  If an exchange is not in the mapping, the instrument's exchange is used.
31
+ account: The account viewer to get account information like total capital, leverage, etc.
30
32
  """
31
33
  super().__init__()
32
34
  self.alert_name = alert_name
@@ -69,10 +71,6 @@ class TargetPositionFormatter(DefaultFormatter):
69
71
  exchange = self.exchange_mapping.get(target.instrument.exchange, target.instrument.exchange)
70
72
 
71
73
  return {
72
- "action": "TARGET_POSITION",
73
- "alertName": self.alert_name,
74
- "exchange": exchange,
75
- "symbol": target.instrument.exchange_symbol.upper(),
76
- "side": side,
77
- "leverage": leverage,
74
+ "type": "TARGET_POSITION",
75
+ "data": f'{{"action":"TARGET_POSITION","alertName":"{self.alert_name}","exchange":"{exchange}","symbol":"{target.instrument.exchange_symbol.upper()}","side":"{side}","leverage":{leverage}}}',
78
76
  }
qubx/loggers/csv.py CHANGED
@@ -31,10 +31,10 @@ class CsvFileLogsWriter(LogsWriter):
31
31
  self._hdr_sig = not os.path.exists(_sig_path)
32
32
  self._hdr_tgt = not os.path.exists(_tgt_path)
33
33
 
34
- self._pfl_file_ = open(_pfl_path, "+a", newline="")
35
- self._execs_file_ = open(_exe_path, "+a", newline="")
36
- self._sig_file_ = open(_sig_path, "+a", newline="")
37
- self._tgt_file_ = open(_tgt_path, "+a", newline="")
34
+ self._pfl_file_ = open(_pfl_path, "a", newline="")
35
+ self._execs_file_ = open(_exe_path, "a", newline="")
36
+ self._sig_file_ = open(_sig_path, "a", newline="")
37
+ self._tgt_file_ = open(_tgt_path, "a", newline="")
38
38
 
39
39
  self._pfl_writer = csv.writer(self._pfl_file_)
40
40
  self._exe_writer = csv.writer(self._execs_file_)
@@ -71,11 +71,42 @@ def ccxt_symbol_to_instrument(ccxt_exchange_name: str, market: dict[str, Any]) -
71
71
 
72
72
  mkt_type = MarketType[market["type"].upper()]
73
73
 
74
- # - extract expiry date if present
75
- expiry_date = pd.Timestamp(market["expiryDatetime"]) if "expiryDatetime" in market else None
76
- if not expiry_date and "expiry" in market:
77
- expiry_date = pd.Timestamp(int(market["expiry"]), unit="ms") if "expiry" in market else None
78
-
74
+ # - extract expiry date if present
75
+ expiry_date = None
76
+ if "expiryDatetime" in market and market["expiryDatetime"]:
77
+ expiry_date = pd.Timestamp(market["expiryDatetime"])
78
+ elif "expiry" in market and market["expiry"]:
79
+ expiry_date = pd.Timestamp(int(market["expiry"]), unit="ms")
80
+ elif "deliveryDate" in inner_info and inner_info["deliveryDate"]:
81
+ expiry_date = pd.Timestamp(int(inner_info["deliveryDate"]), unit="ms")
82
+
83
+ # - extract onboard date from multiple possible sources
84
+ onboard_date = None
85
+ try:
86
+ # Try inner_info.onboardDate first (Binance futures)
87
+ if "onboardDate" in inner_info and inner_info["onboardDate"]:
88
+ onboard_date = pd.Timestamp(int(inner_info["onboardDate"]), unit="ms")
89
+ # Try top-level created field (available for many exchanges)
90
+ elif "created" in market and market["created"]:
91
+ onboard_date = pd.Timestamp(int(market["created"]), unit="ms")
92
+ except (ValueError, TypeError, OverflowError) as e:
93
+ onboard_date = None
94
+
95
+ # - extract delist date if present
96
+ delist_date = None
97
+ try:
98
+ if "delistDate" in inner_info and inner_info["delistDate"]:
99
+ delist_date = pd.Timestamp(int(inner_info["delistDate"]), unit="ms")
100
+ # Some exchanges may have status-based delisting info
101
+ elif "deliveryDate" in inner_info and inner_info["deliveryDate"]:
102
+ delist_date = pd.Timestamp(int(inner_info["deliveryDate"]), unit="ms")
103
+ elif "status" in inner_info and inner_info["status"] in ["DELISTED", "INACTIVE"]:
104
+ # For delisted instruments, we could set delist_date to now, but it's better to leave None
105
+ # and let the 'active' field handle current status
106
+ pass
107
+ except (ValueError, TypeError, OverflowError) as e:
108
+ delist_date = None
109
+
79
110
  # - add expiry date to futures symbol if present
80
111
  if mkt_type == MarketType.FUTURE and expiry_date:
81
112
  symbol += f".{expiry_date.strftime('%Y%m%d')}"
@@ -97,8 +128,9 @@ def ccxt_symbol_to_instrument(ccxt_exchange_name: str, market: dict[str, Any]) -
97
128
  maint_margin=maint_margin,
98
129
  liquidation_fee=liquidation_fee,
99
130
  contract_size=float(market.get("contractSize", 1.0) or 1.0),
100
- onboard_date=pd.Timestamp(int(inner_info["onboardDate"]), unit="ms") if "onboardDate" in inner_info else None,
131
+ onboard_date=onboard_date,
101
132
  delivery_date=expiry_date,
133
+ delist_date=delist_date,
102
134
  inverse=market.get("inverse", False),
103
135
  )
104
136
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.88
3
+ Version: 0.6.90
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  License-File: LICENSE
6
6
  Author: Dmitry Marienko
@@ -2,7 +2,7 @@ qubx/__init__.py,sha256=hOsIaNAqETrL6SLd979Xcr5_WOAGo9q30eQkqSiv7gw,8113
2
2
  qubx/_nb_magic.py,sha256=G3LkaX_-gN5Js6xl7rjaahSs_u3dVPDRCZW0IIxPCb0,3051
3
3
  qubx/backtester/__init__.py,sha256=OhXhLmj2x6sp6k16wm5IPATvv-E2qRZVIcvttxqPgcg,176
4
4
  qubx/backtester/account.py,sha256=0yvE06icSeK2ymovvaKkuftY8Ou3Z7Y2JrDa6VtkINw,3048
5
- qubx/backtester/broker.py,sha256=lJ6PpqE2L0vKcgw2ZP7xahne4eZguJFYWXHiWP6jmXM,2949
5
+ qubx/backtester/broker.py,sha256=3J9MQqdnh2JzpKLrJ_aYqzPZEVixvYuuzg1BzfyITOc,4668
6
6
  qubx/backtester/data.py,sha256=eDS-fe44m6dKy3hp2P7Gll2HRZRpsf_EG-K52Z3zsx8,8057
7
7
  qubx/backtester/management.py,sha256=cgeO7cIyd5J9r1XYGWC9XV2oVHtgAMpQZ6rhxFYl0J4,21147
8
8
  qubx/backtester/ome.py,sha256=LnnSANMD2XBo18JtLRh96Ey9BH_StrfshQnCu2_aOc4,18646
@@ -23,11 +23,11 @@ qubx/connectors/ccxt/__init__.py,sha256=HEQ7lM9HS8sED_zfsAHrhFT7F9E7NFGAecwZwNr-
23
23
  qubx/connectors/ccxt/account.py,sha256=VyPQGlELm1lFXD4sf4zD8xaC9ZQpneB8gAVm-di4hhE,25396
24
24
  qubx/connectors/ccxt/adapters/__init__.py,sha256=4qwWer4C9fTIZKUYmcgbMFEwFuYp34UKbb-AoQNvXjc,178
25
25
  qubx/connectors/ccxt/adapters/polling_adapter.py,sha256=UrOAoIfgtoRw7gj6Bmk5lPs_UI8iOxr88DAfp5o5CF8,8962
26
- qubx/connectors/ccxt/broker.py,sha256=I6I_BynhwHadKa62nBGmDoj5LhFiEzbUq-1ZDwE25aM,16512
26
+ qubx/connectors/ccxt/broker.py,sha256=BqLKzFIYlf2Tw5LE2CWkv3UTEKMeqEpoBnEJmhMNGPE,23025
27
27
  qubx/connectors/ccxt/connection_manager.py,sha256=Jv4c7eymGm_RVHbgViy3DMv5hr_SONm0wir1enKS7V0,11621
28
28
  qubx/connectors/ccxt/data.py,sha256=cUKLWDT_d0w4ZUU-HabI5FmJzDDf0ZU9gZFzb5PU5Ek,15799
29
29
  qubx/connectors/ccxt/exceptions.py,sha256=OfZc7iMdEG8uLorcZta2NuEuJrSIqi0FG7IICmwF54M,262
30
- qubx/connectors/ccxt/exchange_manager.py,sha256=QVNY7qq2VeUbYTXODBKVy3sBdoaZ6psB3hJ-WmR7iPU,14242
30
+ qubx/connectors/ccxt/exchange_manager.py,sha256=2dlK4zNlL58r_gM1JTxzn8OVTEgkJbXXz-tvvFXfc5g,12737
31
31
  qubx/connectors/ccxt/exchanges/__init__.py,sha256=vhBxEWljNE_uan5Kxlr8WLnZhHFKdvjUt8tv2KRKFAA,3167
32
32
  qubx/connectors/ccxt/exchanges/base.py,sha256=0MGKBJhvmy89-JNp_8Dc61vEJwakWV7rL8IZUwUOsVo,2249
33
33
  qubx/connectors/ccxt/exchanges/binance/broker.py,sha256=BB2V82zaOm1EjP3GrsOqQQMeGpml6-w23iv7goKrjyU,2111
@@ -38,7 +38,7 @@ qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py,sha256=j6OB7j_yKRgitQAeJX
38
38
  qubx/connectors/ccxt/exchanges/hyperliquid/broker.py,sha256=yoht8KsiMS3F6rkyfwQ3prBn-OgnY6tCDwYJQbsq1t0,3130
39
39
  qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py,sha256=nHeMCBfIBdcWYl8s9vqyr9fxVmnzEz0uEwxQskLN6UM,20353
40
40
  qubx/connectors/ccxt/exchanges/kraken/kraken.py,sha256=ntxf41aPY0JqxT5jn-979fgQih2TvMbr_p9fvCJB9QE,414
41
- qubx/connectors/ccxt/factory.py,sha256=hX5WODwwo49J3YjGulpDyq1SKq9VvWH3otzFw6avS5I,6259
41
+ qubx/connectors/ccxt/factory.py,sha256=dyTUhgKkpq8jHofRO-H9f34EHtK4d0qtKhp2ZDBxgAc,5835
42
42
  qubx/connectors/ccxt/handlers/__init__.py,sha256=Cuo2RsHiijhBd8YIkLey9JhHkBGWRtPoeu_BEg_jlDE,879
43
43
  qubx/connectors/ccxt/handlers/base.py,sha256=1QS9uhFsisWvGwDmoAc83kLcSd1Lf3hSlnrWwKoHuAk,3334
44
44
  qubx/connectors/ccxt/handlers/factory.py,sha256=SlMAOmaUy1dQ-q5ZzrjODj_XMcLr1BORGGZ7klLNndk,4250
@@ -60,13 +60,13 @@ qubx/connectors/tardis/utils.py,sha256=epThu9DwqbDb7BgScH6fHa_FVpKUaItOqp3JwtKGc
60
60
  qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  qubx/core/account.py,sha256=GGolhM9Qzr2qstipYZ1lxuiac1aOEfQl6fnuQ1Drf5s,23920
62
62
  qubx/core/basics.py,sha256=TZPg99rkSx6x_iJX9qkl53irQx9Bc9G6WqLtzktMiZw,41437
63
- qubx/core/context.py,sha256=Yq039nsKmf-IeyvcRdnMs_phVmehoCUqFvQuVnm-d4s,26074
63
+ qubx/core/context.py,sha256=QFPVy9egHw4T35DV1ruVsxkdUelDUll9BDZ3WGNQukQ,26686
64
64
  qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
65
65
  qubx/core/errors.py,sha256=LENtlgmVzxxUFNCsuy4PwyHYhkZkxuZQ2BPif8jaGmw,1411
66
66
  qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
67
- qubx/core/helpers.py,sha256=xFcgXtNGvaz8SvLeHToePMx0tx85eX3H4ymxjz66ekg,22129
67
+ qubx/core/helpers.py,sha256=fiHOi_j9oLUkat_kOBMI-gyH6G31LSyDtvRfxH_mAas,22348
68
68
  qubx/core/initializer.py,sha256=VVti9UJDJCg0125Mm09S6Tt1foHK4XOBL0mXgDmvXes,7724
69
- qubx/core/interfaces.py,sha256=cf1-_cNGIucXI8be3BLXwBXBBGVB_88x33eh1hLsZJc,67906
69
+ qubx/core/interfaces.py,sha256=ic_5qrqXq_Cms4BIimObY1GQ5hcayhvjuV5Qs2Lfa4g,69548
70
70
  qubx/core/loggers.py,sha256=eYijsR02S5u1Hv21vjIk_dOUwOMv0fiBDYwEmFhAoWk,14344
71
71
  qubx/core/lookups.py,sha256=Bed30kPZvbTGjZ8exojhIMOIVfB46j6741yF3fXGTiM,18313
72
72
  qubx/core/metrics.py,sha256=j8fXGs946reDLugywxTuWPB8WhZbsoZpwVVDUFePcJE,81480
@@ -74,15 +74,15 @@ qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,
74
74
  qubx/core/mixins/market.py,sha256=w9OEDfy0r9xnb4KdKA-PuFsCQugNo4pMLQivGFeyqGw,6356
75
75
  qubx/core/mixins/processing.py,sha256=UqKwOzkDXvmcjvffrsR8vugq_XacwW9rcGM0TcBd9RM,44485
76
76
  qubx/core/mixins/subscription.py,sha256=2nUikNNPsMExS9yO38kNt7jk1vKE-RPm0b3h1bU6gjE,11397
77
- qubx/core/mixins/trading.py,sha256=7KwxHiPWkXGnkHS4VLaxOZ7BHULkvvqPS8ooAG1NTxM,9477
77
+ qubx/core/mixins/trading.py,sha256=hYyDSLmy9mGCCzmfvs8UBFi7yk_RMuitcTK1SVci5O4,12629
78
78
  qubx/core/mixins/universe.py,sha256=UBa3OIr2XvlK04O7YUG9c66CY8AZ5rQDSZov1rnUSjQ,10512
79
79
  qubx/core/mixins/utils.py,sha256=P71cLuqKjId8989MwOL_BtvvCnnwOFMkZyB1SY-0Ork,147
80
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=rMXJhiOIcnWLuCUGjZCa5sUsVlrm3sGVK9MGB-xoDLA,1027944
80
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=PeNuRhkuhHf7-Qs1x5uEVgutaRgX4IvX-skQh1zPwKQ,1027944
81
81
  qubx/core/series.pxd,sha256=4WpEexOBwZdKvqI81yR7wCnQh2rKgVZp9Y0ejr8B3E4,4841
82
82
  qubx/core/series.pyi,sha256=30F48-oMp-XuySh5pyuoZcV20R4yODRpYBgLv7yxhjY,5534
83
83
  qubx/core/series.pyx,sha256=9d3XjPAyI6qYLXuKXqZ3HrxBir1CRVste8GpGvgqYL4,52876
84
84
  qubx/core/stale_data_detector.py,sha256=NHnnG9NkcivC93n8QMwJUzFVQv2ziUaN-fg76ppng_c,17118
85
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=nZHmRBOiMPtCKaCgbm6KidqmZrMqhbyyGWi9lnSps-c,86568
85
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=Br9aqkN4oGmiRbU_nTg5Mibcr-VJ2eYeL5xhMUiG7YM,86568
86
86
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
87
87
  qubx/core/utils.pyx,sha256=UR9achMR-LARsztd2eelFsDsFH3n0gXACIKoGNPI9X4,1766
88
88
  qubx/data/__init__.py,sha256=BlyZ99esHLDmFA6ktNkuIce9RZO99TA1IMKWe94aI8M,599
@@ -106,7 +106,7 @@ qubx/exporters/formatters/__init__.py,sha256=nyPFrsRJczffszAV2gXE_23qwRPOkNJo72P
106
106
  qubx/exporters/formatters/base.py,sha256=rhfStE_ORLTj4dV5l10gpwmQEvGtMxeRJp8SE77IDzQ,5597
107
107
  qubx/exporters/formatters/incremental.py,sha256=V8kbxKDqjQm82wR8wDjHs2bR5WsHqSDFn_O5mI3yBxg,5260
108
108
  qubx/exporters/formatters/slack.py,sha256=MPjbEFh7PQufPdkg_Fwiu2tVw5zYJa977tCemoI790Y,7017
109
- qubx/exporters/formatters/target_position.py,sha256=UxeoVblYlYbCC_55jkq7Z0JENnFO8DIa6lgSWLM55Hw,2985
109
+ qubx/exporters/formatters/target_position.py,sha256=beUTB4R188_hbz3wrxIMMAjXHhyvEii1mBaPbxB5ESc,3138
110
110
  qubx/exporters/redis_streams.py,sha256=wu9ianwMiKfFerSaCFMNz6XMRjOMev02UN00D2pxxLg,10048
111
111
  qubx/exporters/slack.py,sha256=wnVZRwWOKq9lMQyW0MWh_6gkW1id1TUanfOKy-_clwI,7723
112
112
  qubx/features/__init__.py,sha256=ZFCX7K5bDAH7yTsG-mf8zibW8UW8GCneEagL1_p8kDQ,385
@@ -119,7 +119,7 @@ qubx/gathering/simplest.py,sha256=7SHTwK5MojtgjDR5pNObGZCD1EFK15ewG58oGonCC_8,44
119
119
  qubx/health/__init__.py,sha256=ThJTgf-CPD5tMU_emqANpnE6oXfUmzyyugfbDfzeVB0,111
120
120
  qubx/health/base.py,sha256=RIm2nc_FPBBJ9Ji3w_mlIwISjmGR8K94iqRjgVvkGSY,27941
121
121
  qubx/loggers/__init__.py,sha256=nA7nLQKkR9hIJCYQyZxikm--xsB6DaTE5itKypEPBKA,443
122
- qubx/loggers/csv.py,sha256=FCzGSPz1OvqshoRD-pHwfpndwsZbL1S4i7ou57bNAuE,4390
122
+ qubx/loggers/csv.py,sha256=Njwk3UkK_blmHZOmRB0-OvNcvOFk50-wUm9cgPwGrzs,4386
123
123
  qubx/loggers/factory.py,sha256=pDwLuFPPpoCCTiVoDrzvcAsyPFm6sS0e4Zi3j5UEhqk,1791
124
124
  qubx/loggers/inmemory.py,sha256=L1U7YrypTUhE6F9jXKljudBkScjUsUq6CQ7sihFwjeU,4198
125
125
  qubx/loggers/mongo.py,sha256=qFtUZN9SaPlM8A_C09TMrzMC2zjtiGB3GmazSi6QVQk,2769
@@ -157,7 +157,7 @@ qubx/restorers/signal.py,sha256=5nK5ji8AucyWrFBK9uW619YCI_vPRGFnuDu8JnG3B_Y,1451
157
157
  qubx/restorers/state.py,sha256=I1VIN0ZcOjigc3WMHIYTNJeAAbN9YB21MDcMl04ZWmY,8018
158
158
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
159
159
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=rWJEZ4mxaJaPFE4r5qfk36QaZoPgwaAkmR-WQ3hfRCo,804392
160
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=TWM-iBq0WMrs4zfUbKZL20MV__xPsOShYqAs5PK8l3M,804392
161
161
  qubx/ta/indicators.pxd,sha256=r9mYcpDFxn3RW5lVJ497Fiq2-eqD4k7VX0-Q0xcftkk,4913
162
162
  qubx/ta/indicators.pyi,sha256=T87VwJrBJK8EuqtoW1Dhjri05scH_Y6BXN9kex6G7mQ,2881
163
163
  qubx/ta/indicators.pyx,sha256=jJMZ7D2kUspwiZITBnFuNPNvzqUm26EtEfSzBWSrLvQ,39286
@@ -191,7 +191,7 @@ qubx/utils/charting/mpl_helpers.py,sha256=oG9aaz1jFkRHk2g-jiaLlto6mLjhFtq3KTyDUE
191
191
  qubx/utils/charting/orderbook.py,sha256=NmeXxru3CUiKLtl1DzCBbQgSdL4qmTDIxV0u81p2tTw,11083
192
192
  qubx/utils/collections.py,sha256=go2sH_q2TlXqI3Vxq8GHLfNGlYL4JwS3X1lwJWbpFLE,7425
193
193
  qubx/utils/marketdata/binance.py,sha256=hWX3noZj704JIMBqlwsXA5IzUP7EgiLiC2r12dJAKQA,11565
194
- qubx/utils/marketdata/ccxt.py,sha256=onUPYuHIjGjl3oVtiMSRqcB-Ec77Rj-PwZQ0tH-U9BA,5333
194
+ qubx/utils/marketdata/ccxt.py,sha256=K1u9R2dyIdDjKZlg7WnXLL89XuZqH5oMmIL8yUICU98,6850
195
195
  qubx/utils/marketdata/dukas.py,sha256=rFtvEE9SZDmJGS1Ka8bEjleQu_ORy4criIDHsjXE7A4,3237
196
196
  qubx/utils/misc.py,sha256=gb8z3zzVdRUIKncAmI0uMQnT4zSRo41qKh1w0WiOMAU,17360
197
197
  qubx/utils/ntp.py,sha256=yNurgbdiqKhq_dVrJ5PRnho9SzT3ijQG-Bi2sDnFSLs,1904
@@ -212,8 +212,8 @@ qubx/utils/runner/factory.py,sha256=hmtUDYNFQwVQffHEfxgrlmKwOGLcFQ6uJIH_ZLscpIY,
212
212
  qubx/utils/runner/runner.py,sha256=q3b80zD4za6dFSOA6mUCuSQXe0DKS6LwGBMxO1aL3aw,34324
213
213
  qubx/utils/time.py,sha256=xOWl_F6dOLFCmbB4xccLIx5yVt5HOH-I8ZcuowXjtBQ,11797
214
214
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
215
- qubx-0.6.88.dist-info/METADATA,sha256=vfw39Eo8Z0IIPSsRYcgeaYeAJdbjvJNkH8xbYkMHWXk,5909
216
- qubx-0.6.88.dist-info/WHEEL,sha256=RA6gLSyyVpI0R7d3ofBrM1iY5kDUsPwh15AF0XpvgQo,110
217
- qubx-0.6.88.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
218
- qubx-0.6.88.dist-info/licenses/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
219
- qubx-0.6.88.dist-info/RECORD,,
215
+ qubx-0.6.90.dist-info/METADATA,sha256=roluwar3N_k6jDaqe6-tOBmFSoHoPaJkmDKeLalYr-c,5909
216
+ qubx-0.6.90.dist-info/WHEEL,sha256=RA6gLSyyVpI0R7d3ofBrM1iY5kDUsPwh15AF0XpvgQo,110
217
+ qubx-0.6.90.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
218
+ qubx-0.6.90.dist-info/licenses/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
219
+ qubx-0.6.90.dist-info/RECORD,,
File without changes