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.
Files changed (105) hide show
  1. walrasquant/__init__.py +7 -0
  2. walrasquant/aggregation.py +449 -0
  3. walrasquant/backends/__init__.py +5 -0
  4. walrasquant/backends/db.py +109 -0
  5. walrasquant/backends/db_memory.py +61 -0
  6. walrasquant/backends/db_postgresql.py +321 -0
  7. walrasquant/backends/db_sqlite.py +310 -0
  8. walrasquant/base/__init__.py +24 -0
  9. walrasquant/base/api_client.py +46 -0
  10. walrasquant/base/connector.py +863 -0
  11. walrasquant/base/ems.py +794 -0
  12. walrasquant/base/exchange.py +213 -0
  13. walrasquant/base/oms.py +428 -0
  14. walrasquant/base/retry.py +220 -0
  15. walrasquant/base/sms.py +545 -0
  16. walrasquant/base/ws_client.py +408 -0
  17. walrasquant/config.py +284 -0
  18. walrasquant/constants.py +413 -0
  19. walrasquant/core/__init__.py +0 -0
  20. walrasquant/core/cache.py +688 -0
  21. walrasquant/core/clock.py +59 -0
  22. walrasquant/core/connection.py +41 -0
  23. walrasquant/core/entity.py +504 -0
  24. walrasquant/core/nautilius_core.py +103 -0
  25. walrasquant/core/registry.py +41 -0
  26. walrasquant/engine.py +745 -0
  27. walrasquant/error.py +34 -0
  28. walrasquant/exchange/__init__.py +13 -0
  29. walrasquant/exchange/base_factory.py +172 -0
  30. walrasquant/exchange/binance/__init__.py +30 -0
  31. walrasquant/exchange/binance/connector.py +1093 -0
  32. walrasquant/exchange/binance/constants.py +934 -0
  33. walrasquant/exchange/binance/ems.py +140 -0
  34. walrasquant/exchange/binance/error.py +48 -0
  35. walrasquant/exchange/binance/exchange.py +144 -0
  36. walrasquant/exchange/binance/factory.py +115 -0
  37. walrasquant/exchange/binance/oms.py +1807 -0
  38. walrasquant/exchange/binance/rest_api.py +1653 -0
  39. walrasquant/exchange/binance/schema.py +1063 -0
  40. walrasquant/exchange/binance/websockets.py +389 -0
  41. walrasquant/exchange/bitget/__init__.py +28 -0
  42. walrasquant/exchange/bitget/connector.py +578 -0
  43. walrasquant/exchange/bitget/constants.py +392 -0
  44. walrasquant/exchange/bitget/ems.py +202 -0
  45. walrasquant/exchange/bitget/error.py +36 -0
  46. walrasquant/exchange/bitget/exchange.py +128 -0
  47. walrasquant/exchange/bitget/factory.py +135 -0
  48. walrasquant/exchange/bitget/oms.py +1619 -0
  49. walrasquant/exchange/bitget/rest_api.py +610 -0
  50. walrasquant/exchange/bitget/schema.py +885 -0
  51. walrasquant/exchange/bitget/websockets.py +753 -0
  52. walrasquant/exchange/bybit/__init__.py +32 -0
  53. walrasquant/exchange/bybit/connector.py +819 -0
  54. walrasquant/exchange/bybit/constants.py +479 -0
  55. walrasquant/exchange/bybit/ems.py +93 -0
  56. walrasquant/exchange/bybit/error.py +36 -0
  57. walrasquant/exchange/bybit/exchange.py +108 -0
  58. walrasquant/exchange/bybit/factory.py +128 -0
  59. walrasquant/exchange/bybit/oms.py +1195 -0
  60. walrasquant/exchange/bybit/rest_api.py +570 -0
  61. walrasquant/exchange/bybit/schema.py +867 -0
  62. walrasquant/exchange/bybit/websockets.py +307 -0
  63. walrasquant/exchange/hyperliquid/__init__.py +28 -0
  64. walrasquant/exchange/hyperliquid/connector.py +370 -0
  65. walrasquant/exchange/hyperliquid/constants.py +371 -0
  66. walrasquant/exchange/hyperliquid/ems.py +156 -0
  67. walrasquant/exchange/hyperliquid/error.py +48 -0
  68. walrasquant/exchange/hyperliquid/exchange.py +120 -0
  69. walrasquant/exchange/hyperliquid/factory.py +135 -0
  70. walrasquant/exchange/hyperliquid/oms.py +1081 -0
  71. walrasquant/exchange/hyperliquid/rest_api.py +348 -0
  72. walrasquant/exchange/hyperliquid/schema.py +583 -0
  73. walrasquant/exchange/hyperliquid/websockets.py +592 -0
  74. walrasquant/exchange/okx/__init__.py +25 -0
  75. walrasquant/exchange/okx/connector.py +931 -0
  76. walrasquant/exchange/okx/constants.py +518 -0
  77. walrasquant/exchange/okx/ems.py +144 -0
  78. walrasquant/exchange/okx/error.py +66 -0
  79. walrasquant/exchange/okx/exchange.py +102 -0
  80. walrasquant/exchange/okx/factory.py +138 -0
  81. walrasquant/exchange/okx/oms.py +1199 -0
  82. walrasquant/exchange/okx/rest_api.py +799 -0
  83. walrasquant/exchange/okx/schema.py +1449 -0
  84. walrasquant/exchange/okx/websockets.py +420 -0
  85. walrasquant/exchange/registry.py +201 -0
  86. walrasquant/execution/__init__.py +24 -0
  87. walrasquant/execution/algorithm.py +968 -0
  88. walrasquant/execution/algorithms/__init__.py +3 -0
  89. walrasquant/execution/algorithms/twap.py +392 -0
  90. walrasquant/execution/config.py +34 -0
  91. walrasquant/execution/constants.py +27 -0
  92. walrasquant/execution/schema.py +62 -0
  93. walrasquant/indicator.py +382 -0
  94. walrasquant/push.py +77 -0
  95. walrasquant/schema.py +755 -0
  96. walrasquant/strategy.py +1805 -0
  97. walrasquant/tools/__init__.py +0 -0
  98. walrasquant/tools/pm2_wrapper.py +1016 -0
  99. walrasquant/web/__init__.py +26 -0
  100. walrasquant/web/app.py +157 -0
  101. walrasquant/web/server.py +92 -0
  102. walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
  103. walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
  104. walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
  105. walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
@@ -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())