Qubx 0.6.89__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 +52 -6
- qubx/connectors/ccxt/broker.py +159 -19
- qubx/connectors/ccxt/exchange_manager.py +6 -37
- qubx/connectors/ccxt/factory.py +1 -9
- qubx/core/context.py +12 -2
- qubx/core/helpers.py +11 -3
- qubx/core/interfaces.py +51 -7
- qubx/core/mixins/trading.py +69 -3
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/exporters/formatters/target_position.py +2 -6
- qubx/loggers/csv.py +4 -4
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/marketdata/ccxt.py +38 -6
- {qubx-0.6.89.dist-info → qubx-0.6.90.dist-info}/METADATA +1 -1
- {qubx-0.6.89.dist-info → qubx-0.6.90.dist-info}/RECORD +19 -19
- {qubx-0.6.89.dist-info → qubx-0.6.90.dist-info}/WHEEL +0 -0
- {qubx-0.6.89.dist-info → qubx-0.6.90.dist-info}/entry_points.txt +0 -0
- {qubx-0.6.89.dist-info → qubx-0.6.90.dist-info}/licenses/LICENSE +0 -0
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) ->
|
|
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
|
|
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
|
|
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
|
|
80
|
-
|
|
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:
|
qubx/connectors/ccxt/broker.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
222
|
+
raise err
|
|
225
223
|
|
|
226
|
-
def cancel_order(self, order_id: str) ->
|
|
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
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
#
|
|
302
|
-
|
|
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(
|
|
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(
|
|
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
|
|
428
|
-
|
|
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":
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
136
|
+
Force recreation due to data stalls.
|
|
146
137
|
|
|
147
138
|
Returns:
|
|
148
|
-
True if recreation successful, False if failed
|
|
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}")
|
qubx/connectors/ccxt/factory.py
CHANGED
|
@@ -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
|
|
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) ->
|
|
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
|
|
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,
|
|
241
|
-
trade_bar_start = floor_t64(np.datetime64(trade.time,
|
|
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(
|
|
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) ->
|
|
352
|
-
"""Cancel an existing order
|
|
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
|
|
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) ->
|
|
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
|
|
qubx/core/mixins/trading.py
CHANGED
|
@@ -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
|
|
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) ->
|
|
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).
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
@@ -71,10 +71,6 @@ class TargetPositionFormatter(DefaultFormatter):
|
|
|
71
71
|
exchange = self.exchange_mapping.get(target.instrument.exchange, target.instrument.exchange)
|
|
72
72
|
|
|
73
73
|
return {
|
|
74
|
-
"
|
|
75
|
-
"alertName":
|
|
76
|
-
"exchange": exchange,
|
|
77
|
-
"symbol": target.instrument.exchange_symbol.upper(),
|
|
78
|
-
"side": side,
|
|
79
|
-
"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}}}',
|
|
80
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, "
|
|
35
|
-
self._execs_file_ = open(_exe_path, "
|
|
36
|
-
self._sig_file_ = open(_sig_path, "
|
|
37
|
-
self._tgt_file_ = open(_tgt_path, "
|
|
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_)
|
|
Binary file
|
qubx/utils/marketdata/ccxt.py
CHANGED
|
@@ -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
|
-
|
|
75
|
-
expiry_date =
|
|
76
|
-
if
|
|
77
|
-
expiry_date = pd.Timestamp(
|
|
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=
|
|
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
|
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
216
|
-
qubx-0.6.
|
|
217
|
-
qubx-0.6.
|
|
218
|
-
qubx-0.6.
|
|
219
|
-
qubx-0.6.
|
|
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
|
|
File without changes
|
|
File without changes
|