walrasquant-lib 0.4.20__py3-none-any.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.
- walrasquant/__init__.py +7 -0
- walrasquant/aggregation.py +449 -0
- walrasquant/backends/__init__.py +5 -0
- walrasquant/backends/db.py +109 -0
- walrasquant/backends/db_memory.py +61 -0
- walrasquant/backends/db_postgresql.py +321 -0
- walrasquant/backends/db_sqlite.py +310 -0
- walrasquant/base/__init__.py +24 -0
- walrasquant/base/api_client.py +46 -0
- walrasquant/base/connector.py +863 -0
- walrasquant/base/ems.py +794 -0
- walrasquant/base/exchange.py +213 -0
- walrasquant/base/oms.py +428 -0
- walrasquant/base/retry.py +220 -0
- walrasquant/base/sms.py +545 -0
- walrasquant/base/ws_client.py +408 -0
- walrasquant/config.py +284 -0
- walrasquant/constants.py +413 -0
- walrasquant/core/__init__.py +0 -0
- walrasquant/core/cache.py +688 -0
- walrasquant/core/clock.py +59 -0
- walrasquant/core/connection.py +41 -0
- walrasquant/core/entity.py +504 -0
- walrasquant/core/nautilius_core.py +103 -0
- walrasquant/core/registry.py +41 -0
- walrasquant/engine.py +745 -0
- walrasquant/error.py +34 -0
- walrasquant/exchange/__init__.py +13 -0
- walrasquant/exchange/base_factory.py +172 -0
- walrasquant/exchange/binance/__init__.py +30 -0
- walrasquant/exchange/binance/connector.py +1093 -0
- walrasquant/exchange/binance/constants.py +934 -0
- walrasquant/exchange/binance/ems.py +140 -0
- walrasquant/exchange/binance/error.py +48 -0
- walrasquant/exchange/binance/exchange.py +144 -0
- walrasquant/exchange/binance/factory.py +115 -0
- walrasquant/exchange/binance/oms.py +1807 -0
- walrasquant/exchange/binance/rest_api.py +1653 -0
- walrasquant/exchange/binance/schema.py +1063 -0
- walrasquant/exchange/binance/websockets.py +389 -0
- walrasquant/exchange/bitget/__init__.py +28 -0
- walrasquant/exchange/bitget/connector.py +578 -0
- walrasquant/exchange/bitget/constants.py +392 -0
- walrasquant/exchange/bitget/ems.py +202 -0
- walrasquant/exchange/bitget/error.py +36 -0
- walrasquant/exchange/bitget/exchange.py +128 -0
- walrasquant/exchange/bitget/factory.py +135 -0
- walrasquant/exchange/bitget/oms.py +1619 -0
- walrasquant/exchange/bitget/rest_api.py +610 -0
- walrasquant/exchange/bitget/schema.py +885 -0
- walrasquant/exchange/bitget/websockets.py +753 -0
- walrasquant/exchange/bybit/__init__.py +32 -0
- walrasquant/exchange/bybit/connector.py +819 -0
- walrasquant/exchange/bybit/constants.py +479 -0
- walrasquant/exchange/bybit/ems.py +93 -0
- walrasquant/exchange/bybit/error.py +36 -0
- walrasquant/exchange/bybit/exchange.py +108 -0
- walrasquant/exchange/bybit/factory.py +128 -0
- walrasquant/exchange/bybit/oms.py +1195 -0
- walrasquant/exchange/bybit/rest_api.py +570 -0
- walrasquant/exchange/bybit/schema.py +867 -0
- walrasquant/exchange/bybit/websockets.py +307 -0
- walrasquant/exchange/hyperliquid/__init__.py +28 -0
- walrasquant/exchange/hyperliquid/connector.py +370 -0
- walrasquant/exchange/hyperliquid/constants.py +371 -0
- walrasquant/exchange/hyperliquid/ems.py +156 -0
- walrasquant/exchange/hyperliquid/error.py +48 -0
- walrasquant/exchange/hyperliquid/exchange.py +120 -0
- walrasquant/exchange/hyperliquid/factory.py +135 -0
- walrasquant/exchange/hyperliquid/oms.py +1081 -0
- walrasquant/exchange/hyperliquid/rest_api.py +348 -0
- walrasquant/exchange/hyperliquid/schema.py +583 -0
- walrasquant/exchange/hyperliquid/websockets.py +592 -0
- walrasquant/exchange/okx/__init__.py +25 -0
- walrasquant/exchange/okx/connector.py +931 -0
- walrasquant/exchange/okx/constants.py +518 -0
- walrasquant/exchange/okx/ems.py +144 -0
- walrasquant/exchange/okx/error.py +66 -0
- walrasquant/exchange/okx/exchange.py +102 -0
- walrasquant/exchange/okx/factory.py +138 -0
- walrasquant/exchange/okx/oms.py +1199 -0
- walrasquant/exchange/okx/rest_api.py +799 -0
- walrasquant/exchange/okx/schema.py +1449 -0
- walrasquant/exchange/okx/websockets.py +420 -0
- walrasquant/exchange/registry.py +201 -0
- walrasquant/execution/__init__.py +24 -0
- walrasquant/execution/algorithm.py +968 -0
- walrasquant/execution/algorithms/__init__.py +3 -0
- walrasquant/execution/algorithms/twap.py +392 -0
- walrasquant/execution/config.py +34 -0
- walrasquant/execution/constants.py +27 -0
- walrasquant/execution/schema.py +62 -0
- walrasquant/indicator.py +382 -0
- walrasquant/push.py +77 -0
- walrasquant/schema.py +755 -0
- walrasquant/strategy.py +1805 -0
- walrasquant/tools/__init__.py +0 -0
- walrasquant/tools/pm2_wrapper.py +1016 -0
- walrasquant/web/__init__.py +26 -0
- walrasquant/web/app.py +157 -0
- walrasquant/web/server.py +92 -0
- walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
- walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
- walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
- walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
walrasquant/base/sms.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
import nexuslog as logging
|
|
7
|
+
|
|
8
|
+
from walrasquant.schema import SubscriptionSubmit, UnsubscriptionSubmit, InstrumentId
|
|
9
|
+
from walrasquant.core.entity import TaskManager, DataReady
|
|
10
|
+
from walrasquant.core.nautilius_core import LiveClock
|
|
11
|
+
from walrasquant.constants import AccountType, DataType, KlineInterval, ExchangeType
|
|
12
|
+
from walrasquant.base.connector import PublicConnector, ExchangeManager
|
|
13
|
+
from walrasquant.error import SubscriptionError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SubscriptionManagementSystem:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
exchanges: dict[ExchangeType, ExchangeManager],
|
|
20
|
+
public_connectors: dict[AccountType, PublicConnector],
|
|
21
|
+
task_manager: TaskManager,
|
|
22
|
+
clock: LiveClock,
|
|
23
|
+
sms_maxsize: int = 100_000,
|
|
24
|
+
):
|
|
25
|
+
self._log = logging.getLogger(name=type(self).__name__)
|
|
26
|
+
|
|
27
|
+
self._exchanges = exchanges
|
|
28
|
+
self._public_connectors = public_connectors
|
|
29
|
+
self._task_manager = task_manager
|
|
30
|
+
self._clock = clock
|
|
31
|
+
|
|
32
|
+
# Two queues: one for subscribe, one for unsubscribe
|
|
33
|
+
self._subscribe_queue: asyncio.Queue[SubscriptionSubmit] = asyncio.Queue(
|
|
34
|
+
maxsize=sms_maxsize
|
|
35
|
+
)
|
|
36
|
+
self._unsubscribe_queue: asyncio.Queue[UnsubscriptionSubmit] = asyncio.Queue(
|
|
37
|
+
maxsize=sms_maxsize
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Track subscription readiness
|
|
41
|
+
self._subscriptions_ready: dict[DataType | str, DataReady] = {}
|
|
42
|
+
|
|
43
|
+
def _safe_put(self, queue: asyncio.Queue, item: Any) -> None:
|
|
44
|
+
# (Un)subscriptions may be requested off the event loop (e.g. threaded
|
|
45
|
+
# scheduled jobs). asyncio.Queue is not thread-safe, and touching it
|
|
46
|
+
# off-loop silently corrupts the loop state instead of raising. So decide
|
|
47
|
+
# the thread BEFORE touching the queue: on the loop, put inline; otherwise
|
|
48
|
+
# marshal the whole put onto the loop with call_soon_threadsafe.
|
|
49
|
+
try:
|
|
50
|
+
asyncio.get_running_loop() # raises only if this thread has no loop
|
|
51
|
+
on_loop = True
|
|
52
|
+
except RuntimeError:
|
|
53
|
+
on_loop = False
|
|
54
|
+
|
|
55
|
+
if on_loop:
|
|
56
|
+
self._do_put(queue, item)
|
|
57
|
+
else:
|
|
58
|
+
self._task_manager.loop.call_soon_threadsafe(self._do_put, queue, item)
|
|
59
|
+
|
|
60
|
+
def _do_put(self, queue: asyncio.Queue, item: Any) -> None:
|
|
61
|
+
# Always runs on the loop thread.
|
|
62
|
+
if queue.maxsize == 0 or queue.qsize() < queue.maxsize:
|
|
63
|
+
try:
|
|
64
|
+
queue.put_nowait(item)
|
|
65
|
+
return
|
|
66
|
+
except asyncio.QueueFull:
|
|
67
|
+
pass
|
|
68
|
+
task = asyncio.get_running_loop().create_task(queue.put(item))
|
|
69
|
+
task.add_done_callback(self._on_queue_put_done)
|
|
70
|
+
|
|
71
|
+
def _on_queue_put_done(self, task: asyncio.Task) -> None:
|
|
72
|
+
if task.cancelled():
|
|
73
|
+
return
|
|
74
|
+
exc = task.exception()
|
|
75
|
+
if exc:
|
|
76
|
+
self._log.error(f"Queue put failed: {exc!r}")
|
|
77
|
+
|
|
78
|
+
def _infer_account_type(self, symbol: str) -> AccountType:
|
|
79
|
+
"""Infer account type from symbol"""
|
|
80
|
+
instrument_id = InstrumentId.from_str(symbol)
|
|
81
|
+
exchange = self._exchanges.get(instrument_id.exchange)
|
|
82
|
+
if not exchange:
|
|
83
|
+
raise ValueError(f"Exchange {instrument_id.exchange} not found")
|
|
84
|
+
return exchange.instrument_id_to_account_type(instrument_id)
|
|
85
|
+
|
|
86
|
+
def _group_symbols_by_account_type(
|
|
87
|
+
self, symbols: list[str]
|
|
88
|
+
) -> dict[AccountType, list[str]]:
|
|
89
|
+
"""Group symbols by their account type"""
|
|
90
|
+
grouped = defaultdict(list)
|
|
91
|
+
for symbol in symbols:
|
|
92
|
+
account_type = self._infer_account_type(symbol)
|
|
93
|
+
grouped[account_type].append(symbol)
|
|
94
|
+
return dict(grouped)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def ready(self) -> bool:
|
|
98
|
+
"""Check if all subscriptions are ready"""
|
|
99
|
+
return all(
|
|
100
|
+
data_ready.ready for data_ready in self._subscriptions_ready.values()
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def input(self, sub_key: str | DataType, data: Any) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Input data to the corresponding DataReady tracker.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
sub_key: The subscription key (DataType, interval value, or symbol)
|
|
109
|
+
data: The data object to input
|
|
110
|
+
"""
|
|
111
|
+
self._subscriptions_ready[sub_key].input(data)
|
|
112
|
+
|
|
113
|
+
def subscribe(
|
|
114
|
+
self,
|
|
115
|
+
symbols: str | list[str],
|
|
116
|
+
data_type: DataType,
|
|
117
|
+
params: dict[str, Any] | None = None,
|
|
118
|
+
ready_timeout: int = 60,
|
|
119
|
+
ready: bool = True,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Subscribe to market data.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
symbols: Symbol or list of symbols to subscribe
|
|
126
|
+
data_type: Type of data to subscribe (BOOKL1, KLINE, etc.)
|
|
127
|
+
params: Additional parameters (interval for KLINE, level for BOOKL2, etc.)
|
|
128
|
+
ready_timeout: Timeout in seconds for data to be ready
|
|
129
|
+
ready: Whether to track readiness (True for event-driven strategies)
|
|
130
|
+
"""
|
|
131
|
+
if isinstance(symbols, str):
|
|
132
|
+
symbols = [symbols]
|
|
133
|
+
|
|
134
|
+
if params is None:
|
|
135
|
+
params = {}
|
|
136
|
+
|
|
137
|
+
subscription = SubscriptionSubmit(
|
|
138
|
+
symbols=symbols,
|
|
139
|
+
data_type=data_type,
|
|
140
|
+
params=params,
|
|
141
|
+
ready_timeout=ready_timeout,
|
|
142
|
+
ready=ready,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
self._safe_put(self._subscribe_queue, subscription)
|
|
146
|
+
self._log.debug(f"Subscription queued: {data_type} for {symbols}")
|
|
147
|
+
|
|
148
|
+
def unsubscribe(
|
|
149
|
+
self,
|
|
150
|
+
symbols: str | list[str],
|
|
151
|
+
data_type: DataType,
|
|
152
|
+
params: dict[str, Any] | None = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Unsubscribe from market data.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
symbols: Symbol or list of symbols to unsubscribe
|
|
159
|
+
data_type: Type of data to unsubscribe (BOOKL1, KLINE, etc.)
|
|
160
|
+
params: Additional parameters (interval for KLINE, level for BOOKL2, etc.)
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(symbols, str):
|
|
163
|
+
symbols = [symbols]
|
|
164
|
+
|
|
165
|
+
if params is None:
|
|
166
|
+
params = {}
|
|
167
|
+
|
|
168
|
+
unsubscription = UnsubscriptionSubmit(
|
|
169
|
+
symbols=symbols,
|
|
170
|
+
data_type=data_type,
|
|
171
|
+
params=params,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self._safe_put(self._unsubscribe_queue, unsubscription)
|
|
175
|
+
self._log.debug(f"Unsubscription queued: {data_type} for {symbols}")
|
|
176
|
+
|
|
177
|
+
def _subscribe_trade(
|
|
178
|
+
self, subscription: SubscriptionSubmit, account_type: AccountType
|
|
179
|
+
):
|
|
180
|
+
"""Subscribe to trade data"""
|
|
181
|
+
self._public_connectors[account_type].subscribe_trade(subscription.symbols)
|
|
182
|
+
|
|
183
|
+
# Register DataReady
|
|
184
|
+
if DataType.TRADE not in self._subscriptions_ready:
|
|
185
|
+
self._subscriptions_ready[DataType.TRADE] = DataReady(
|
|
186
|
+
subscription.symbols,
|
|
187
|
+
name="trade",
|
|
188
|
+
clock=self._clock,
|
|
189
|
+
timeout=subscription.ready_timeout,
|
|
190
|
+
permanently_ready=subscription.ready,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _subscribe_bookl1(
|
|
194
|
+
self, subscription: SubscriptionSubmit, account_type: AccountType
|
|
195
|
+
):
|
|
196
|
+
"""Subscribe to bookl1 data"""
|
|
197
|
+
self._public_connectors[account_type].subscribe_bookl1(subscription.symbols)
|
|
198
|
+
|
|
199
|
+
# Register DataReady
|
|
200
|
+
if DataType.BOOKL1 not in self._subscriptions_ready:
|
|
201
|
+
self._subscriptions_ready[DataType.BOOKL1] = DataReady(
|
|
202
|
+
subscription.symbols,
|
|
203
|
+
name="bookl1",
|
|
204
|
+
clock=self._clock,
|
|
205
|
+
timeout=subscription.ready_timeout,
|
|
206
|
+
permanently_ready=subscription.ready,
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
self._subscriptions_ready[DataType.BOOKL1].add_symbols(subscription.symbols)
|
|
210
|
+
|
|
211
|
+
def _subscribe_bookl2(
|
|
212
|
+
self, subscription: SubscriptionSubmit, account_type: AccountType
|
|
213
|
+
):
|
|
214
|
+
"""Subscribe to bookl2 data"""
|
|
215
|
+
level = subscription.params.get("level")
|
|
216
|
+
if level is None:
|
|
217
|
+
raise ValueError("level is required for BOOKL2 subscription")
|
|
218
|
+
self._public_connectors[account_type].subscribe_bookl2(
|
|
219
|
+
subscription.symbols, level
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Register DataReady
|
|
223
|
+
if DataType.BOOKL2 not in self._subscriptions_ready:
|
|
224
|
+
self._subscriptions_ready[DataType.BOOKL2] = DataReady(
|
|
225
|
+
subscription.symbols,
|
|
226
|
+
name="bookl2",
|
|
227
|
+
clock=self._clock,
|
|
228
|
+
timeout=subscription.ready_timeout,
|
|
229
|
+
permanently_ready=subscription.ready,
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
self._subscriptions_ready[DataType.BOOKL2].add_symbols(subscription.symbols)
|
|
233
|
+
|
|
234
|
+
def _subscribe_kline(
|
|
235
|
+
self, subscription: SubscriptionSubmit, account_type: AccountType
|
|
236
|
+
):
|
|
237
|
+
"""Subscribe to kline data"""
|
|
238
|
+
interval: KlineInterval = subscription.params.get("interval") # type: ignore
|
|
239
|
+
if interval is None:
|
|
240
|
+
raise ValueError("interval is required for KLINE subscription")
|
|
241
|
+
|
|
242
|
+
use_aggregator = subscription.params.get("use_aggregator", False)
|
|
243
|
+
build_with_no_updates = subscription.params.get("build_with_no_updates", True)
|
|
244
|
+
|
|
245
|
+
if use_aggregator:
|
|
246
|
+
for symbol in subscription.symbols:
|
|
247
|
+
self._public_connectors[account_type].subscribe_kline_aggregator(
|
|
248
|
+
symbol, interval, build_with_no_updates
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
self._public_connectors[account_type].subscribe_kline(
|
|
252
|
+
subscription.symbols, interval
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Register DataReady
|
|
256
|
+
if interval.value not in self._subscriptions_ready:
|
|
257
|
+
self._subscriptions_ready[interval.value] = DataReady(
|
|
258
|
+
subscription.symbols,
|
|
259
|
+
name=f"kline_{interval.value}",
|
|
260
|
+
clock=self._clock,
|
|
261
|
+
timeout=subscription.ready_timeout,
|
|
262
|
+
permanently_ready=subscription.ready,
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
self._subscriptions_ready[interval.value].add_symbols(subscription.symbols)
|
|
266
|
+
|
|
267
|
+
def _subscribe_volume_kline(
|
|
268
|
+
self, subscription: SubscriptionSubmit, account_type: AccountType
|
|
269
|
+
):
|
|
270
|
+
"""Subscribe to volume kline data"""
|
|
271
|
+
volume_threshold = subscription.params.get("volume_threshold")
|
|
272
|
+
if volume_threshold is None:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"`volume_threshold` is required for `VOLUME_KLINE` subscription"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
volume_type = subscription.params.get("volume_type", "DEFAULT")
|
|
278
|
+
|
|
279
|
+
for symbol in subscription.symbols:
|
|
280
|
+
self._public_connectors[account_type].subscribe_volume_kline_aggregator(
|
|
281
|
+
symbol, volume_threshold, volume_type
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Register DataReady for each symbol (volume kline uses symbol as key)
|
|
285
|
+
if symbol in self._subscriptions_ready:
|
|
286
|
+
raise ValueError(
|
|
287
|
+
f"Symbol {symbol} already subscribed to volume kline with a different threshold. Only one volume threshold per symbol is allowed."
|
|
288
|
+
)
|
|
289
|
+
self._subscriptions_ready[symbol] = DataReady(
|
|
290
|
+
[symbol],
|
|
291
|
+
name=f"volume_kline_{volume_threshold}",
|
|
292
|
+
clock=self._clock,
|
|
293
|
+
timeout=subscription.ready_timeout,
|
|
294
|
+
permanently_ready=subscription.ready,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _unsubscribe_volume_kline(
|
|
298
|
+
self, subscription: UnsubscriptionSubmit, account_type: AccountType
|
|
299
|
+
):
|
|
300
|
+
"""Unsubscribe from volume kline data"""
|
|
301
|
+
volume_threshold = subscription.params.get("volume_threshold")
|
|
302
|
+
if volume_threshold is None:
|
|
303
|
+
raise ValueError(
|
|
304
|
+
"`volume_threshold` is required for `VOLUME_KLINE` unsubscription"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
volume_type = subscription.params.get("volume_type", "DEFAULT")
|
|
308
|
+
|
|
309
|
+
for symbol in subscription.symbols:
|
|
310
|
+
self._public_connectors[account_type].unsubscribe_volume_kline_aggregator(
|
|
311
|
+
symbol, volume_threshold, volume_type
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _subscribe_funding_rate(
|
|
315
|
+
self, subscription: SubscriptionSubmit, account_type: AccountType
|
|
316
|
+
):
|
|
317
|
+
"""Subscribe to funding rate data"""
|
|
318
|
+
self._public_connectors[account_type].subscribe_funding_rate(
|
|
319
|
+
subscription.symbols
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Register DataReady
|
|
323
|
+
if DataType.FUNDING_RATE not in self._subscriptions_ready:
|
|
324
|
+
self._subscriptions_ready[DataType.FUNDING_RATE] = DataReady(
|
|
325
|
+
subscription.symbols,
|
|
326
|
+
name="funding_rate",
|
|
327
|
+
clock=self._clock,
|
|
328
|
+
timeout=subscription.ready_timeout,
|
|
329
|
+
permanently_ready=subscription.ready,
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
self._subscriptions_ready[DataType.FUNDING_RATE].add_symbols(
|
|
333
|
+
subscription.symbols
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def _subscribe_index_price(
|
|
337
|
+
self, subscription: SubscriptionSubmit, account_type: AccountType
|
|
338
|
+
):
|
|
339
|
+
"""Subscribe to index price data"""
|
|
340
|
+
self._public_connectors[account_type].subscribe_index_price(
|
|
341
|
+
subscription.symbols
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Register DataReady
|
|
345
|
+
if DataType.INDEX_PRICE not in self._subscriptions_ready:
|
|
346
|
+
self._subscriptions_ready[DataType.INDEX_PRICE] = DataReady(
|
|
347
|
+
subscription.symbols,
|
|
348
|
+
name="index_price",
|
|
349
|
+
clock=self._clock,
|
|
350
|
+
timeout=subscription.ready_timeout,
|
|
351
|
+
permanently_ready=subscription.ready,
|
|
352
|
+
)
|
|
353
|
+
else:
|
|
354
|
+
self._subscriptions_ready[DataType.INDEX_PRICE].add_symbols(
|
|
355
|
+
subscription.symbols
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _subscribe_mark_price(
|
|
359
|
+
self, subscription: SubscriptionSubmit, account_type: AccountType
|
|
360
|
+
):
|
|
361
|
+
"""Subscribe to mark price data"""
|
|
362
|
+
self._public_connectors[account_type].subscribe_mark_price(subscription.symbols)
|
|
363
|
+
|
|
364
|
+
# Register DataReady
|
|
365
|
+
if DataType.MARK_PRICE not in self._subscriptions_ready:
|
|
366
|
+
self._subscriptions_ready[DataType.MARK_PRICE] = DataReady(
|
|
367
|
+
subscription.symbols,
|
|
368
|
+
name="mark_price",
|
|
369
|
+
clock=self._clock,
|
|
370
|
+
timeout=subscription.ready_timeout,
|
|
371
|
+
permanently_ready=subscription.ready,
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
self._subscriptions_ready[DataType.MARK_PRICE].add_symbols(
|
|
375
|
+
subscription.symbols
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def _unsubscribe_trade(
|
|
379
|
+
self, unsubscription: UnsubscriptionSubmit, account_type: AccountType
|
|
380
|
+
):
|
|
381
|
+
"""Unsubscribe from trade data"""
|
|
382
|
+
self._public_connectors[account_type].unsubscribe_trade(unsubscription.symbols)
|
|
383
|
+
|
|
384
|
+
def _unsubscribe_bookl1(
|
|
385
|
+
self, unsubscription: UnsubscriptionSubmit, account_type: AccountType
|
|
386
|
+
):
|
|
387
|
+
"""Unsubscribe from bookl1 data"""
|
|
388
|
+
self._public_connectors[account_type].unsubscribe_bookl1(unsubscription.symbols)
|
|
389
|
+
|
|
390
|
+
def _unsubscribe_bookl2(
|
|
391
|
+
self, unsubscription: UnsubscriptionSubmit, account_type: AccountType
|
|
392
|
+
):
|
|
393
|
+
"""Unsubscribe from bookl2 data"""
|
|
394
|
+
level = unsubscription.params.get("level")
|
|
395
|
+
if level is None:
|
|
396
|
+
raise ValueError("level is required for BOOKL2 unsubscription")
|
|
397
|
+
|
|
398
|
+
self._public_connectors[account_type].unsubscribe_bookl2(
|
|
399
|
+
unsubscription.symbols, level
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def _unsubscribe_kline(
|
|
403
|
+
self, unsubscription: UnsubscriptionSubmit, account_type: AccountType
|
|
404
|
+
):
|
|
405
|
+
"""Unsubscribe from kline data"""
|
|
406
|
+
interval = unsubscription.params.get("interval")
|
|
407
|
+
if interval is None:
|
|
408
|
+
raise ValueError("interval is required for KLINE unsubscription")
|
|
409
|
+
|
|
410
|
+
use_aggregator = unsubscription.params.get("use_aggregator", False)
|
|
411
|
+
if use_aggregator:
|
|
412
|
+
for symbol in unsubscription.symbols:
|
|
413
|
+
self._public_connectors[account_type].unsubscribe_kline_aggregator(
|
|
414
|
+
symbol, interval
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
self._public_connectors[account_type].unsubscribe_kline(
|
|
418
|
+
unsubscription.symbols, interval
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def _unsubscribe_mark_price(
|
|
422
|
+
self, unsubscription: UnsubscriptionSubmit, account_type: AccountType
|
|
423
|
+
):
|
|
424
|
+
"""Unsubscribe from mark price data"""
|
|
425
|
+
self._public_connectors[account_type].unsubscribe_mark_price(
|
|
426
|
+
unsubscription.symbols
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def _unsubscribe_funding_rate(
|
|
430
|
+
self, unsubscription: UnsubscriptionSubmit, account_type: AccountType
|
|
431
|
+
):
|
|
432
|
+
"""Unsubscribe from funding rate data"""
|
|
433
|
+
self._public_connectors[account_type].unsubscribe_funding_rate(
|
|
434
|
+
unsubscription.symbols
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def _unsubscribe_index_price(
|
|
438
|
+
self, unsubscription: UnsubscriptionSubmit, account_type: AccountType
|
|
439
|
+
):
|
|
440
|
+
"""Unsubscribe from index price data"""
|
|
441
|
+
self._public_connectors[account_type].unsubscribe_index_price(
|
|
442
|
+
unsubscription.symbols
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
async def _handle_subscribe(self):
|
|
446
|
+
"""Handle subscription requests"""
|
|
447
|
+
self._log.debug("Starting subscription handler")
|
|
448
|
+
|
|
449
|
+
while True:
|
|
450
|
+
subscription = await self._subscribe_queue.get()
|
|
451
|
+
self._log.debug(f"[SUBSCRIPTION]: {subscription}")
|
|
452
|
+
|
|
453
|
+
grouped = self._group_symbols_by_account_type(subscription.symbols)
|
|
454
|
+
|
|
455
|
+
# Process each account type group
|
|
456
|
+
for account_type, symbols in grouped.items():
|
|
457
|
+
# Check if account_type exists in public_connectors
|
|
458
|
+
if account_type not in self._public_connectors:
|
|
459
|
+
raise SubscriptionError(
|
|
460
|
+
f"Please add `{account_type}` public connector to the `config.public_conn_config`."
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Create a new subscription for this account type
|
|
464
|
+
account_subscription = SubscriptionSubmit(
|
|
465
|
+
symbols=symbols,
|
|
466
|
+
data_type=subscription.data_type,
|
|
467
|
+
params=subscription.params,
|
|
468
|
+
ready=subscription.ready,
|
|
469
|
+
ready_timeout=subscription.ready_timeout,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
match subscription.data_type:
|
|
473
|
+
case DataType.TRADE:
|
|
474
|
+
self._subscribe_trade(account_subscription, account_type)
|
|
475
|
+
case DataType.BOOKL1:
|
|
476
|
+
self._subscribe_bookl1(account_subscription, account_type)
|
|
477
|
+
case DataType.BOOKL2:
|
|
478
|
+
self._subscribe_bookl2(account_subscription, account_type)
|
|
479
|
+
case DataType.KLINE:
|
|
480
|
+
self._subscribe_kline(account_subscription, account_type)
|
|
481
|
+
case DataType.VOLUME_KLINE:
|
|
482
|
+
self._subscribe_volume_kline(account_subscription, account_type)
|
|
483
|
+
case DataType.FUNDING_RATE:
|
|
484
|
+
self._subscribe_funding_rate(account_subscription, account_type)
|
|
485
|
+
case DataType.INDEX_PRICE:
|
|
486
|
+
self._subscribe_index_price(account_subscription, account_type)
|
|
487
|
+
case DataType.MARK_PRICE:
|
|
488
|
+
self._subscribe_mark_price(account_subscription, account_type)
|
|
489
|
+
self._subscribe_queue.task_done()
|
|
490
|
+
|
|
491
|
+
async def _handle_unsubscribe(self):
|
|
492
|
+
"""Handle unsubscription requests"""
|
|
493
|
+
self._log.debug("Starting unsubscription handler")
|
|
494
|
+
|
|
495
|
+
while True:
|
|
496
|
+
unsubscription = await self._unsubscribe_queue.get()
|
|
497
|
+
self._log.debug(f"[UNSUBSCRIPTION]: {unsubscription}")
|
|
498
|
+
|
|
499
|
+
grouped = self._group_symbols_by_account_type(unsubscription.symbols)
|
|
500
|
+
# Process each account type group
|
|
501
|
+
for account_type, symbols in grouped.items():
|
|
502
|
+
# Check if account_type exists in public_connectors
|
|
503
|
+
if account_type not in self._public_connectors:
|
|
504
|
+
raise SubscriptionError(
|
|
505
|
+
f"Please add `{account_type}` public connector to the `config.public_conn_config`."
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Create a new subscription for this account type
|
|
509
|
+
account_unsubscription = UnsubscriptionSubmit(
|
|
510
|
+
symbols=symbols,
|
|
511
|
+
data_type=unsubscription.data_type,
|
|
512
|
+
params=unsubscription.params,
|
|
513
|
+
)
|
|
514
|
+
match unsubscription.data_type:
|
|
515
|
+
case DataType.TRADE:
|
|
516
|
+
self._unsubscribe_trade(account_unsubscription, account_type)
|
|
517
|
+
case DataType.BOOKL1:
|
|
518
|
+
self._unsubscribe_bookl1(account_unsubscription, account_type)
|
|
519
|
+
case DataType.BOOKL2:
|
|
520
|
+
self._unsubscribe_bookl2(account_unsubscription, account_type)
|
|
521
|
+
case DataType.KLINE:
|
|
522
|
+
self._unsubscribe_kline(account_unsubscription, account_type)
|
|
523
|
+
case DataType.MARK_PRICE:
|
|
524
|
+
self._unsubscribe_mark_price(
|
|
525
|
+
account_unsubscription, account_type
|
|
526
|
+
)
|
|
527
|
+
case DataType.FUNDING_RATE:
|
|
528
|
+
self._unsubscribe_funding_rate(
|
|
529
|
+
account_unsubscription, account_type
|
|
530
|
+
)
|
|
531
|
+
case DataType.INDEX_PRICE:
|
|
532
|
+
self._unsubscribe_index_price(
|
|
533
|
+
account_unsubscription, account_type
|
|
534
|
+
)
|
|
535
|
+
case DataType.VOLUME_KLINE:
|
|
536
|
+
self._unsubscribe_volume_kline(
|
|
537
|
+
account_unsubscription, account_type
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
self._unsubscribe_queue.task_done()
|
|
541
|
+
|
|
542
|
+
async def start(self):
|
|
543
|
+
"""Start the subscription management system"""
|
|
544
|
+
self._task_manager.create_task(self._handle_subscribe())
|
|
545
|
+
self._task_manager.create_task(self._handle_unsubscribe())
|