Qubx 0.6.73__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.74__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/connectors/ccxt/broker.py +19 -12
- qubx/connectors/ccxt/connection_manager.py +8 -2
- qubx/connectors/ccxt/data.py +97 -53
- qubx/connectors/ccxt/exchange_manager.py +339 -0
- qubx/connectors/ccxt/factory.py +83 -5
- qubx/connectors/ccxt/handlers/base.py +6 -5
- qubx/connectors/ccxt/handlers/factory.py +7 -7
- qubx/connectors/ccxt/handlers/funding_rate.py +8 -10
- qubx/connectors/ccxt/handlers/liquidation.py +6 -6
- qubx/connectors/ccxt/handlers/ohlc.py +14 -12
- qubx/connectors/ccxt/handlers/open_interest.py +6 -4
- qubx/connectors/ccxt/handlers/orderbook.py +10 -10
- qubx/connectors/ccxt/handlers/quote.py +10 -7
- qubx/connectors/ccxt/handlers/trade.py +8 -5
- qubx/connectors/ccxt/subscription_orchestrator.py +12 -7
- qubx/connectors/ccxt/warmup_service.py +9 -3
- qubx/connectors/tardis/data.py +1 -1
- qubx/core/context.py +7 -3
- qubx/core/interfaces.py +20 -3
- qubx/core/metrics.py +10 -3
- qubx/core/mixins/market.py +17 -3
- qubx/core/mixins/processing.py +5 -2
- qubx/core/mixins/subscription.py +13 -4
- qubx/core/mixins/trading.py +26 -13
- qubx/core/mixins/utils.py +4 -0
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pyx +1 -1
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/emitters/indicator.py +5 -5
- qubx/exporters/formatters/incremental.py +4 -4
- qubx/health/base.py +26 -19
- qubx/pandaz/ta.py +1 -12
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pxd +10 -0
- qubx/ta/indicators.pyi +8 -2
- qubx/ta/indicators.pyx +83 -6
- qubx/trackers/riskctrl.py +400 -1
- qubx/utils/charting/lookinglass.py +11 -1
- qubx/utils/runner/runner.py +7 -8
- {qubx-0.6.73.dist-info → qubx-0.6.74.dist-info}/METADATA +1 -1
- {qubx-0.6.73.dist-info → qubx-0.6.74.dist-info}/RECORD +44 -42
- {qubx-0.6.73.dist-info → qubx-0.6.74.dist-info}/LICENSE +0 -0
- {qubx-0.6.73.dist-info → qubx-0.6.74.dist-info}/WHEEL +0 -0
- {qubx-0.6.73.dist-info → qubx-0.6.74.dist-info}/entry_points.txt +0 -0
qubx/connectors/ccxt/broker.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Any
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
|
|
7
7
|
import ccxt
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
from ccxt.base.errors import ExchangeError
|
|
10
10
|
from qubx import logger
|
|
11
11
|
from qubx.core.basics import (
|
|
@@ -24,16 +24,16 @@ from qubx.core.interfaces import (
|
|
|
24
24
|
)
|
|
25
25
|
from qubx.utils.misc import AsyncThreadLoop
|
|
26
26
|
|
|
27
|
+
from .exchange_manager import ExchangeManager
|
|
27
28
|
from .utils import ccxt_convert_order_info, instrument_to_ccxt_symbol
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class CcxtBroker(IBroker):
|
|
31
|
-
|
|
32
|
-
_loop: AsyncThreadLoop
|
|
32
|
+
_exchange_manager: ExchangeManager
|
|
33
33
|
|
|
34
34
|
def __init__(
|
|
35
35
|
self,
|
|
36
|
-
|
|
36
|
+
exchange_manager: ExchangeManager,
|
|
37
37
|
channel: CtrlChannel,
|
|
38
38
|
time_provider: ITimeProvider,
|
|
39
39
|
account: IAccountProcessor,
|
|
@@ -44,19 +44,24 @@ class CcxtBroker(IBroker):
|
|
|
44
44
|
enable_create_order_ws: bool = False,
|
|
45
45
|
enable_cancel_order_ws: bool = False,
|
|
46
46
|
):
|
|
47
|
-
self.
|
|
48
|
-
self.ccxt_exchange_id = str(exchange.name)
|
|
47
|
+
self._exchange_manager = exchange_manager
|
|
48
|
+
self.ccxt_exchange_id = str(self._exchange_manager.exchange.name)
|
|
49
49
|
self.channel = channel
|
|
50
50
|
self.time_provider = time_provider
|
|
51
51
|
self.account = account
|
|
52
52
|
self.data_provider = data_provider
|
|
53
|
-
self._loop = AsyncThreadLoop(exchange.asyncio_loop)
|
|
54
53
|
self.cancel_timeout = cancel_timeout
|
|
55
54
|
self.cancel_retry_interval = cancel_retry_interval
|
|
56
55
|
self.max_cancel_retries = max_cancel_retries
|
|
57
56
|
self.enable_create_order_ws = enable_create_order_ws
|
|
58
57
|
self.enable_cancel_order_ws = enable_cancel_order_ws
|
|
59
58
|
|
|
59
|
+
@property
|
|
60
|
+
def _loop(self) -> AsyncThreadLoop:
|
|
61
|
+
"""Get current AsyncThreadLoop for the exchange."""
|
|
62
|
+
return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
|
|
63
|
+
|
|
64
|
+
|
|
60
65
|
@property
|
|
61
66
|
def is_simulated_trading(self) -> bool:
|
|
62
67
|
return False
|
|
@@ -255,9 +260,9 @@ class CcxtBroker(IBroker):
|
|
|
255
260
|
instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
256
261
|
)
|
|
257
262
|
if self.enable_create_order_ws:
|
|
258
|
-
r = await self.
|
|
263
|
+
r = await self._exchange_manager.exchange.create_order_ws(**payload)
|
|
259
264
|
else:
|
|
260
|
-
r = await self.
|
|
265
|
+
r = await self._exchange_manager.exchange.create_order(**payload)
|
|
261
266
|
|
|
262
267
|
if r is None:
|
|
263
268
|
msg = "(::_create_order) No response from exchange"
|
|
@@ -294,12 +299,14 @@ class CcxtBroker(IBroker):
|
|
|
294
299
|
raise BadRequest(f"Quote is not available for order creation for {instrument.symbol}")
|
|
295
300
|
|
|
296
301
|
# TODO: think about automatically setting reduce only when needed
|
|
297
|
-
if not options.get("reduceOnly", False):
|
|
302
|
+
if not (reduce_only := options.get("reduceOnly", False)):
|
|
298
303
|
min_notional = instrument.min_notional
|
|
299
304
|
if min_notional > 0 and abs(amount) * quote.mid_price() < min_notional:
|
|
300
305
|
raise InvalidOrderParameters(
|
|
301
306
|
f"[{instrument.symbol}] Order amount {amount} is too small. Minimum notional is {min_notional}"
|
|
302
307
|
)
|
|
308
|
+
else:
|
|
309
|
+
params["reduceOnly"] = reduce_only
|
|
303
310
|
|
|
304
311
|
# - handle trigger (stop) orders
|
|
305
312
|
if _is_trigger_order:
|
|
@@ -357,9 +364,9 @@ class CcxtBroker(IBroker):
|
|
|
357
364
|
while True:
|
|
358
365
|
try:
|
|
359
366
|
if self.enable_cancel_order_ws:
|
|
360
|
-
await self.
|
|
367
|
+
await self._exchange_manager.exchange.cancel_order_ws(order_id, symbol=instrument_to_ccxt_symbol(instrument))
|
|
361
368
|
else:
|
|
362
|
-
await self.
|
|
369
|
+
await self._exchange_manager.exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(instrument))
|
|
363
370
|
return True
|
|
364
371
|
except ccxt.OperationRejected as err:
|
|
365
372
|
err_msg = str(err).lower()
|
|
@@ -19,6 +19,7 @@ from qubx.core.basics import CtrlChannel
|
|
|
19
19
|
from qubx.utils.misc import AsyncThreadLoop
|
|
20
20
|
|
|
21
21
|
from .exceptions import CcxtSymbolNotRecognized
|
|
22
|
+
from .exchange_manager import ExchangeManager
|
|
22
23
|
from .subscription_manager import SubscriptionManager
|
|
23
24
|
|
|
24
25
|
|
|
@@ -36,13 +37,13 @@ class ConnectionManager:
|
|
|
36
37
|
def __init__(
|
|
37
38
|
self,
|
|
38
39
|
exchange_id: str,
|
|
39
|
-
|
|
40
|
+
exchange_manager: ExchangeManager,
|
|
40
41
|
max_ws_retries: int = 10,
|
|
41
42
|
subscription_manager: SubscriptionManager | None = None,
|
|
42
43
|
cleanup_timeout: float = 3.0,
|
|
43
44
|
):
|
|
44
45
|
self._exchange_id = exchange_id
|
|
45
|
-
self.
|
|
46
|
+
self._exchange_manager = exchange_manager
|
|
46
47
|
self.max_ws_retries = max_ws_retries
|
|
47
48
|
self._subscription_manager = subscription_manager
|
|
48
49
|
self._cleanup_timeout = cleanup_timeout
|
|
@@ -54,6 +55,11 @@ class ConnectionManager:
|
|
|
54
55
|
# Connection tracking
|
|
55
56
|
self._stream_to_coro: dict[str, concurrent.futures.Future] = {}
|
|
56
57
|
|
|
58
|
+
@property
|
|
59
|
+
def _loop(self) -> AsyncThreadLoop:
|
|
60
|
+
"""Get current AsyncThreadLoop from exchange manager."""
|
|
61
|
+
return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
|
|
62
|
+
|
|
57
63
|
def set_subscription_manager(self, subscription_manager: SubscriptionManager) -> None:
|
|
58
64
|
"""Set the subscription manager for state coordination."""
|
|
59
65
|
self._subscription_manager = subscription_manager
|
qubx/connectors/ccxt/data.py
CHANGED
|
@@ -4,19 +4,17 @@ from typing import Dict, List, Optional, Set, Tuple
|
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
|
|
7
|
-
import ccxt.pro as cxp
|
|
8
|
-
|
|
9
7
|
# CCXT exceptions are now handled in ConnectionManager
|
|
10
|
-
from ccxt.pro import Exchange
|
|
11
8
|
from qubx import logger
|
|
12
|
-
from qubx.core.basics import CtrlChannel, DataType, Instrument, ITimeProvider
|
|
9
|
+
from qubx.core.basics import CtrlChannel, DataType, Instrument, ITimeProvider, dt_64
|
|
13
10
|
from qubx.core.helpers import BasicScheduler
|
|
14
|
-
from qubx.core.interfaces import IDataProvider, IHealthMonitor
|
|
11
|
+
from qubx.core.interfaces import IDataArrivalListener, IDataProvider, IHealthMonitor
|
|
15
12
|
from qubx.core.series import Bar, Quote
|
|
16
13
|
from qubx.health import DummyHealthMonitor
|
|
17
14
|
from qubx.utils.misc import AsyncThreadLoop
|
|
18
15
|
|
|
19
16
|
from .connection_manager import ConnectionManager
|
|
17
|
+
from .exchange_manager import ExchangeManager
|
|
20
18
|
from .handlers import DataTypeHandlerFactory
|
|
21
19
|
from .handlers.ohlc import OhlcDataHandler
|
|
22
20
|
from .subscription_config import SubscriptionConfiguration
|
|
@@ -27,42 +25,46 @@ from .warmup_service import WarmupService
|
|
|
27
25
|
|
|
28
26
|
class CcxtDataProvider(IDataProvider):
|
|
29
27
|
time_provider: ITimeProvider
|
|
30
|
-
|
|
28
|
+
_exchange_manager: ExchangeManager
|
|
31
29
|
_scheduler: BasicScheduler | None = None
|
|
32
30
|
|
|
33
31
|
# Core state - still needed
|
|
34
32
|
_last_quotes: dict[Instrument, Optional[Quote]]
|
|
35
|
-
_loop: AsyncThreadLoop
|
|
36
33
|
_warmup_timeout: int
|
|
37
34
|
|
|
38
35
|
def __init__(
|
|
39
36
|
self,
|
|
40
|
-
|
|
37
|
+
exchange_manager: ExchangeManager,
|
|
41
38
|
time_provider: ITimeProvider,
|
|
42
39
|
channel: CtrlChannel,
|
|
43
40
|
max_ws_retries: int = 10,
|
|
44
41
|
warmup_timeout: int = 120,
|
|
45
42
|
health_monitor: IHealthMonitor | None = None,
|
|
46
43
|
):
|
|
47
|
-
|
|
44
|
+
# Store the exchange manager (always ExchangeManager now)
|
|
45
|
+
self._exchange_manager = exchange_manager
|
|
46
|
+
|
|
48
47
|
self.time_provider = time_provider
|
|
49
48
|
self.channel = channel
|
|
50
49
|
self.max_ws_retries = max_ws_retries
|
|
51
50
|
self._warmup_timeout = warmup_timeout
|
|
52
51
|
self._health_monitor = health_monitor or DummyHealthMonitor()
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
self._data_arrival_listeners: List[IDataArrivalListener] = [
|
|
54
|
+
self._health_monitor,
|
|
55
|
+
self._exchange_manager
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
logger.debug(f"Registered {len(self._data_arrival_listeners)} data arrival listeners")
|
|
57
59
|
|
|
58
|
-
#
|
|
59
|
-
self.
|
|
60
|
+
# Core components - access exchange directly via exchange_manager.exchange
|
|
61
|
+
self._exchange_id = str(self._exchange_manager.exchange.name)
|
|
60
62
|
|
|
61
63
|
# Initialize composed components
|
|
62
64
|
self._subscription_manager = SubscriptionManager()
|
|
63
65
|
self._connection_manager = ConnectionManager(
|
|
64
66
|
exchange_id=self._exchange_id,
|
|
65
|
-
|
|
67
|
+
exchange_manager=self._exchange_manager,
|
|
66
68
|
max_ws_retries=max_ws_retries,
|
|
67
69
|
subscription_manager=self._subscription_manager,
|
|
68
70
|
)
|
|
@@ -70,13 +72,13 @@ class CcxtDataProvider(IDataProvider):
|
|
|
70
72
|
exchange_id=self._exchange_id,
|
|
71
73
|
subscription_manager=self._subscription_manager,
|
|
72
74
|
connection_manager=self._connection_manager,
|
|
73
|
-
|
|
75
|
+
exchange_manager=self._exchange_manager,
|
|
74
76
|
)
|
|
75
77
|
|
|
76
78
|
# Data type handler factory for clean separation of data processing logic
|
|
77
79
|
self._data_type_handler_factory = DataTypeHandlerFactory(
|
|
78
80
|
data_provider=self,
|
|
79
|
-
|
|
81
|
+
exchange_manager=self._exchange_manager,
|
|
80
82
|
exchange_id=self._exchange_id,
|
|
81
83
|
)
|
|
82
84
|
|
|
@@ -85,47 +87,42 @@ class CcxtDataProvider(IDataProvider):
|
|
|
85
87
|
handler_factory=self._data_type_handler_factory,
|
|
86
88
|
channel=channel,
|
|
87
89
|
exchange_id=self._exchange_id,
|
|
88
|
-
|
|
90
|
+
exchange_manager=self._exchange_manager,
|
|
89
91
|
warmup_timeout=warmup_timeout,
|
|
90
92
|
)
|
|
91
93
|
|
|
92
94
|
# Quote caching for synthetic quote generation
|
|
93
95
|
self._last_quotes = defaultdict(lambda: None)
|
|
94
96
|
|
|
95
|
-
|
|
97
|
+
# Start ExchangeManager monitoring
|
|
98
|
+
self._exchange_manager.start_monitoring()
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Set up global exception handler for the CCXT async loop to handle unretrieved futures.
|
|
100
|
+
# Register recreation callback for automatic resubscription
|
|
101
|
+
self._exchange_manager.register_recreation_callback(self._handle_exchange_recreation)
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
per-symbol futures that complete with UnsubscribeError during resubscription.
|
|
103
|
-
"""
|
|
104
|
-
asyncio_loop = self._exchange.asyncio_loop
|
|
103
|
+
logger.info(f"<yellow>{self._exchange_id}</yellow> Initialized")
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
@property
|
|
106
|
+
def _loop(self) -> AsyncThreadLoop:
|
|
107
|
+
"""Get current AsyncThreadLoop for the exchange."""
|
|
108
|
+
return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
|
|
109
|
+
|
|
110
|
+
def notify_data_arrival(self, event_type: str, event_time: dt_64) -> None:
|
|
111
|
+
"""Notify all registered listeners about data arrival.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
event_type: Type of data event (e.g., "ohlcv:BTC/USDT:1m")
|
|
115
|
+
event_time: Timestamp of the data event
|
|
116
|
+
"""
|
|
117
|
+
for listener in self._data_arrival_listeners:
|
|
118
|
+
try:
|
|
119
|
+
listener.on_data_arrival(event_type, event_time)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error notifying data arrival listener {type(listener).__name__}: {e}")
|
|
109
122
|
|
|
110
|
-
# Handle expected CCXT UnsubscribeError during resubscription
|
|
111
|
-
if exception and "UnsubscribeError" in str(type(exception)):
|
|
112
|
-
return
|
|
113
123
|
|
|
114
|
-
# Handle other CCXT-related exceptions quietly if they're in our exchange context
|
|
115
|
-
if exception and any(
|
|
116
|
-
keyword in str(exception) for keyword in [self._exchange.id, "ohlcv", "orderbook", "ticker"]
|
|
117
|
-
):
|
|
118
|
-
return
|
|
119
124
|
|
|
120
|
-
# For all other exceptions, use the default handler
|
|
121
|
-
if hasattr(loop, "default_exception_handler"):
|
|
122
|
-
loop.default_exception_handler(context)
|
|
123
|
-
else:
|
|
124
|
-
# Fallback logging if no default handler
|
|
125
|
-
logger.warning(f"Unhandled asyncio exception: {context}")
|
|
126
125
|
|
|
127
|
-
# Set the custom exception handler on the CCXT loop
|
|
128
|
-
asyncio_loop.set_exception_handler(handle_ccxt_exception)
|
|
129
126
|
|
|
130
127
|
@property
|
|
131
128
|
def is_simulation(self) -> bool:
|
|
@@ -155,7 +152,7 @@ class CcxtDataProvider(IDataProvider):
|
|
|
155
152
|
subscription_type=subscription_type,
|
|
156
153
|
instruments=_updated_instruments,
|
|
157
154
|
handler=handler,
|
|
158
|
-
exchange=self.
|
|
155
|
+
exchange=self._exchange_manager.exchange,
|
|
159
156
|
channel=self.channel,
|
|
160
157
|
**_params,
|
|
161
158
|
)
|
|
@@ -196,7 +193,7 @@ class CcxtDataProvider(IDataProvider):
|
|
|
196
193
|
subscription_type=subscription_type,
|
|
197
194
|
instruments=remaining_instruments,
|
|
198
195
|
handler=handler,
|
|
199
|
-
exchange=self.
|
|
196
|
+
exchange=self._exchange_manager.exchange,
|
|
200
197
|
channel=self.channel,
|
|
201
198
|
**_params,
|
|
202
199
|
)
|
|
@@ -262,19 +259,66 @@ class CcxtDataProvider(IDataProvider):
|
|
|
262
259
|
except Exception as e:
|
|
263
260
|
logger.error(f"Error stopping subscription {subscription_type}: {e}")
|
|
264
261
|
|
|
262
|
+
# Stop ExchangeManager monitoring
|
|
263
|
+
self._exchange_manager.stop_monitoring()
|
|
264
|
+
|
|
265
265
|
# Close exchange connection
|
|
266
|
-
if hasattr(self.
|
|
267
|
-
future = self._loop.submit(self.
|
|
266
|
+
if hasattr(self._exchange_manager.exchange, "close"):
|
|
267
|
+
future = self._loop.submit(self._exchange_manager.exchange.close()) # type: ignore
|
|
268
268
|
# Wait for 5 seconds for connection to close
|
|
269
269
|
future.result(5)
|
|
270
270
|
else:
|
|
271
|
-
del self.
|
|
271
|
+
del self._exchange_manager
|
|
272
272
|
|
|
273
273
|
# Note: AsyncThreadLoop stop is handled by its own lifecycle
|
|
274
274
|
|
|
275
275
|
except Exception as e:
|
|
276
276
|
logger.error(f"Error during close: {e}")
|
|
277
277
|
|
|
278
|
+
def _handle_exchange_recreation(self) -> None:
|
|
279
|
+
"""Handle exchange recreation by resubscribing to all active subscriptions."""
|
|
280
|
+
logger.info(f"<yellow>{self._exchange_id}</yellow> Handling exchange recreation - resubscribing to active subscriptions")
|
|
281
|
+
|
|
282
|
+
# Get snapshot of current subscriptions before cleanup
|
|
283
|
+
active_subscriptions = self._subscription_manager.get_subscriptions()
|
|
284
|
+
|
|
285
|
+
resubscription_data = []
|
|
286
|
+
for subscription_type in active_subscriptions:
|
|
287
|
+
instruments = self._subscription_manager.get_subscribed_instruments(subscription_type)
|
|
288
|
+
if instruments:
|
|
289
|
+
resubscription_data.append((subscription_type, instruments))
|
|
290
|
+
|
|
291
|
+
logger.info(f"<yellow>{self._exchange_id}</yellow> Found {len(resubscription_data)} active subscriptions to recreate")
|
|
292
|
+
|
|
293
|
+
# Track success/failure counts for reporting
|
|
294
|
+
successful_resubscriptions = 0
|
|
295
|
+
failed_resubscriptions = 0
|
|
296
|
+
|
|
297
|
+
# Clean resubscription: unsubscribe then subscribe for each subscription type
|
|
298
|
+
for subscription_type, instruments in resubscription_data:
|
|
299
|
+
try:
|
|
300
|
+
logger.info(f"<yellow>{self._exchange_id}</yellow> Resubscribing to {subscription_type} with {len(instruments)} instruments")
|
|
301
|
+
|
|
302
|
+
self.unsubscribe(subscription_type, instruments)
|
|
303
|
+
|
|
304
|
+
# Resubscribe with reset=True to ensure clean state
|
|
305
|
+
self.subscribe(subscription_type, instruments, reset=True)
|
|
306
|
+
|
|
307
|
+
successful_resubscriptions += 1
|
|
308
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> Successfully resubscribed to {subscription_type}")
|
|
309
|
+
|
|
310
|
+
except Exception as e:
|
|
311
|
+
failed_resubscriptions += 1
|
|
312
|
+
logger.error(f"<yellow>{self._exchange_id}</yellow> Failed to resubscribe to {subscription_type}: {e}")
|
|
313
|
+
# Continue with other subscriptions even if one fails
|
|
314
|
+
|
|
315
|
+
# Report final status
|
|
316
|
+
total_subscriptions = len(resubscription_data)
|
|
317
|
+
if failed_resubscriptions == 0:
|
|
318
|
+
logger.info(f"<yellow>{self._exchange_id}</yellow> Exchange recreation resubscription completed successfully ({total_subscriptions}/{total_subscriptions})")
|
|
319
|
+
else:
|
|
320
|
+
logger.warning(f"<yellow>{self._exchange_id}</yellow> Exchange recreation resubscription completed with errors ({successful_resubscriptions}/{total_subscriptions} successful)")
|
|
321
|
+
|
|
278
322
|
@property
|
|
279
323
|
def subscribed_instruments(self) -> Set[Instrument]:
|
|
280
324
|
"""Get all subscribed instruments (delegated to subscription manager)."""
|
|
@@ -282,7 +326,7 @@ class CcxtDataProvider(IDataProvider):
|
|
|
282
326
|
|
|
283
327
|
@property
|
|
284
328
|
def is_read_only(self) -> bool:
|
|
285
|
-
_key = self.
|
|
329
|
+
_key = self._exchange_manager.exchange.apiKey
|
|
286
330
|
return _key is None or _key == ""
|
|
287
331
|
|
|
288
332
|
def _time_msec_nbars_back(self, timeframe: str, nbarsback: int = 1) -> int:
|
|
@@ -293,9 +337,9 @@ class CcxtDataProvider(IDataProvider):
|
|
|
293
337
|
_t = re.match(r"(\d+)(\w+)", timeframe)
|
|
294
338
|
timeframe = f"{_t[1]}{_t[2][0].lower()}" if _t and len(_t.groups()) > 1 else timeframe
|
|
295
339
|
|
|
296
|
-
tframe = self.
|
|
340
|
+
tframe = self._exchange_manager.exchange.find_timeframe(timeframe)
|
|
297
341
|
if tframe is None:
|
|
298
|
-
raise ValueError(f"timeframe {timeframe} is not supported by {self.
|
|
342
|
+
raise ValueError(f"timeframe {timeframe} is not supported by {self._exchange_manager.exchange.name}")
|
|
299
343
|
|
|
300
344
|
return tframe
|
|
301
345
|
|