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,931 @@
1
+ import msgspec
2
+ from typing import Dict, List # noqa: F401
3
+
4
+
5
+ from walrasquant.config import OrderQueryConfig
6
+ from walrasquant.exchange.okx import OkxAccountType
7
+ from walrasquant.exchange.okx.websockets import OkxWSClient
8
+ from walrasquant.exchange.okx.exchange import OkxExchangeManager
9
+ from walrasquant.exchange.okx.schema import OkxWsGeneralMsg
10
+ from walrasquant.exchange.okx.oms import OkxOrderManagementSystem
11
+ from walrasquant.schema import (
12
+ Trade,
13
+ BookL1,
14
+ Kline,
15
+ BookL2,
16
+ IndexPrice,
17
+ FundingRate,
18
+ MarkPrice,
19
+ KlineList,
20
+ Ticker,
21
+ )
22
+ from walrasquant.exchange.okx.schema import (
23
+ OkxMarket,
24
+ OkxWsBboTbtMsg,
25
+ OkxWsCandleMsg,
26
+ OkxWsTradeMsg,
27
+ OkxWsIndexTickerMsg,
28
+ OkxWsFundingRateMsg,
29
+ OkxWsMarkPriceMsg,
30
+ OkxCandlesticksResponse,
31
+ OkxCandlesticksResponseData,
32
+ OkxWsBook5Msg,
33
+ OkxTickersResponse,
34
+ OkxIndexCandlesticksResponseData,
35
+ OkxWsBooksMsg,
36
+ OkxOrderBook,
37
+ )
38
+ from walrasquant.constants import (
39
+ KlineInterval,
40
+ BookLevel,
41
+ )
42
+ from walrasquant.base import PublicConnector, PrivateConnector
43
+ from walrasquant.core.nautilius_core import MessageBus, LiveClock
44
+ from walrasquant.core.cache import AsyncCache
45
+ from walrasquant.core.entity import TaskManager
46
+ from walrasquant.core.registry import OrderRegistry
47
+ from walrasquant.exchange.okx.rest_api import OkxApiClient
48
+ from walrasquant.exchange.okx.constants import (
49
+ OkxEnumParser,
50
+ OkxKlineInterval,
51
+ )
52
+
53
+
54
+ class OkxPublicConnector(PublicConnector):
55
+ _ws_client: OkxWSClient
56
+ _api_client: OkxApiClient
57
+ _account_type: OkxAccountType
58
+
59
+ def __init__(
60
+ self,
61
+ account_type: OkxAccountType,
62
+ exchange: OkxExchangeManager,
63
+ msgbus: MessageBus,
64
+ clock: LiveClock,
65
+ task_manager: TaskManager,
66
+ custom_url: str | None = None,
67
+ enable_rate_limit: bool = True,
68
+ max_subscriptions_per_client: int | None = None,
69
+ max_clients: int | None = None,
70
+ ):
71
+ super().__init__(
72
+ account_type=account_type,
73
+ market=exchange.market,
74
+ market_id=exchange.market_id,
75
+ exchange_id=exchange.exchange_id,
76
+ ws_client=OkxWSClient(
77
+ account_type=account_type,
78
+ handler=self._ws_msg_handler,
79
+ task_manager=task_manager,
80
+ custom_url=custom_url,
81
+ clock=clock,
82
+ max_subscriptions_per_client=max_subscriptions_per_client,
83
+ max_clients=max_clients,
84
+ ),
85
+ msgbus=msgbus,
86
+ clock=clock,
87
+ api_client=OkxApiClient(
88
+ clock=clock,
89
+ testnet=account_type.is_testnet,
90
+ enable_rate_limit=enable_rate_limit,
91
+ ),
92
+ task_manager=task_manager,
93
+ )
94
+ self._business_ws_client = OkxWSClient(
95
+ account_type=account_type,
96
+ handler=self._business_ws_msg_handler,
97
+ task_manager=task_manager,
98
+ business_url=True,
99
+ custom_url=custom_url,
100
+ clock=clock,
101
+ max_subscriptions_per_client=max_subscriptions_per_client,
102
+ max_clients=max_clients,
103
+ )
104
+ self._ws_msg_general_decoder = msgspec.json.Decoder(OkxWsGeneralMsg)
105
+ self._ws_msg_bbo_tbt_decoder = msgspec.json.Decoder(OkxWsBboTbtMsg)
106
+ self._ws_msg_book5_decoder = msgspec.json.Decoder(OkxWsBook5Msg)
107
+ self._ws_msg_books_decoder = msgspec.json.Decoder(OkxWsBooksMsg)
108
+ self._ws_msg_candle_decoder = msgspec.json.Decoder(OkxWsCandleMsg)
109
+ self._ws_msg_trade_decoder = msgspec.json.Decoder(OkxWsTradeMsg)
110
+ self._ws_msg_index_ticker_decoder = msgspec.json.Decoder(OkxWsIndexTickerMsg)
111
+ self._ws_msg_mark_price_decoder = msgspec.json.Decoder(OkxWsMarkPriceMsg)
112
+ self._ws_msg_funding_rate_decoder = msgspec.json.Decoder(OkxWsFundingRateMsg)
113
+ self._okx_orderbook: Dict[str, OkxOrderBook] = {}
114
+
115
+ def _require_channel(self, ws_msg: OkxWsGeneralMsg) -> str:
116
+ arg = ws_msg.arg
117
+ channel = arg.channel if arg is not None else None
118
+ if channel is None:
119
+ raise ValueError("Missing channel in OKX websocket message")
120
+ return channel
121
+
122
+ def _require_arg_channel(self, channel: str | None) -> str:
123
+ if channel is None:
124
+ raise ValueError("Missing channel in OKX websocket message")
125
+ return channel
126
+
127
+ def _require_inst_id(self, inst_id: str | None) -> str:
128
+ if inst_id is None:
129
+ raise ValueError("Missing instrument id in OKX websocket message")
130
+ return inst_id
131
+
132
+ async def request_ticker(
133
+ self,
134
+ symbol: str,
135
+ ) -> Ticker:
136
+ """Request 24hr ticker data"""
137
+ market = self._market.get(symbol)
138
+ if not market:
139
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
140
+
141
+ ticker_response: OkxTickersResponse = (
142
+ await self._api_client.get_api_v5_market_ticker(inst_id=market.id)
143
+ )
144
+ for item in ticker_response.data:
145
+ ticker = Ticker(
146
+ exchange=self._exchange_id,
147
+ symbol=symbol,
148
+ last_price=float(item.last) if item.last else 0.0,
149
+ timestamp=int(item.ts),
150
+ volume=float(item.vol24h) if item.vol24h else 0.0,
151
+ volumeCcy=float(item.volCcy24h) if item.volCcy24h else 0.0,
152
+ )
153
+ return ticker
154
+ raise RuntimeError(f"Ticker data for symbol {symbol} not found")
155
+
156
+ async def request_all_tickers(
157
+ self,
158
+ ) -> Dict[str, Ticker]:
159
+ """Request 24hr ticker data for multiple symbols"""
160
+ spot_tickers_response: OkxTickersResponse = (
161
+ await self._api_client.get_api_v5_market_tickers(inst_type="SPOT")
162
+ )
163
+ swap_tickers_response: OkxTickersResponse = (
164
+ await self._api_client.get_api_v5_market_tickers(inst_type="SWAP")
165
+ )
166
+ future_tickers_response: OkxTickersResponse = (
167
+ await self._api_client.get_api_v5_market_tickers(inst_type="FUTURES")
168
+ )
169
+
170
+ tickers = {}
171
+ for item in (
172
+ spot_tickers_response.data
173
+ + swap_tickers_response.data
174
+ + future_tickers_response.data
175
+ ):
176
+ symbol = self._market_id.get(item.instId)
177
+ if not symbol:
178
+ continue
179
+ tickers[symbol] = Ticker(
180
+ exchange=self._exchange_id,
181
+ symbol=symbol,
182
+ last_price=float(item.last) if item.last else 0.0,
183
+ timestamp=int(item.ts),
184
+ volume=float(item.vol24h) if item.vol24h else 0.0,
185
+ volumeCcy=float(item.volCcy24h) if item.volCcy24h else 0.0,
186
+ )
187
+
188
+ return tickers
189
+
190
+ async def request_index_klines(
191
+ self,
192
+ symbol: str,
193
+ interval: KlineInterval,
194
+ limit: int | None = None,
195
+ start_time: int | None = None,
196
+ end_time: int | None = None,
197
+ ) -> KlineList:
198
+ market = self._market.get(symbol)
199
+ if not market:
200
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
201
+
202
+ okx_interval = OkxEnumParser.to_okx_kline_interval(interval)
203
+ all_klines: list[Kline] = []
204
+ seen_timestamps: set[int] = set()
205
+
206
+ # First request to get the most recent data using before parameter
207
+ klines_response = (
208
+ await self._api_client.get_api_v5_market_history_index_candles(
209
+ instId=self._market[symbol].id,
210
+ bar=okx_interval.value,
211
+ limit=100, # Maximum allowed by the API is 100
212
+ before="0", # Get the latest klines
213
+ )
214
+ )
215
+
216
+ response_klines = sorted(klines_response.data, key=lambda x: int(x.ts))
217
+ klines = [
218
+ self._handle_index_candlesticks(
219
+ symbol=symbol, interval=interval, kline=kline
220
+ )
221
+ for kline in response_klines
222
+ if int(kline.ts) not in seen_timestamps
223
+ ]
224
+ all_klines.extend(klines)
225
+ seen_timestamps.update(int(kline.ts) for kline in response_klines)
226
+
227
+ # Continue fetching older data using after parameter if needed
228
+ if (
229
+ start_time is not None
230
+ and all_klines
231
+ and int(all_klines[0].timestamp) > start_time
232
+ ):
233
+ while True:
234
+ # Use the oldest timestamp we have as the 'after' parameter
235
+ oldest_timestamp = (
236
+ min(int(kline.ts) for kline in response_klines)
237
+ if response_klines
238
+ else None
239
+ )
240
+
241
+ if not oldest_timestamp or (
242
+ start_time is not None and oldest_timestamp <= start_time
243
+ ):
244
+ break
245
+
246
+ klines_response = (
247
+ await self._api_client.get_api_v5_market_history_index_candles(
248
+ instId=self._market[symbol].id,
249
+ bar=okx_interval.value,
250
+ limit=100,
251
+ after=str(oldest_timestamp), # Get klines before this timestamp
252
+ )
253
+ )
254
+
255
+ response_klines = sorted(klines_response.data, key=lambda x: int(x.ts))
256
+ if not response_klines:
257
+ break
258
+
259
+ # Process klines and filter out duplicates
260
+ new_klines = [
261
+ self._handle_index_candlesticks(
262
+ symbol=symbol, interval=interval, kline=kline
263
+ )
264
+ for kline in response_klines
265
+ if int(kline.ts) not in seen_timestamps
266
+ ]
267
+
268
+ if not new_klines:
269
+ break
270
+
271
+ all_klines = (
272
+ new_klines + all_klines
273
+ ) # Prepend new klines as they are older
274
+ seen_timestamps.update(int(kline.ts) for kline in response_klines)
275
+
276
+ # Apply limit if specified
277
+ if limit is not None and len(all_klines) > limit:
278
+ all_klines = all_klines[-limit:] # Take the most recent klines
279
+
280
+ if end_time:
281
+ all_klines = [kline for kline in all_klines if kline.timestamp < end_time]
282
+
283
+ kline_list = KlineList(
284
+ all_klines,
285
+ fields=[
286
+ "timestamp",
287
+ "symbol",
288
+ "open",
289
+ "high",
290
+ "low",
291
+ "close",
292
+ "confirm",
293
+ ],
294
+ )
295
+ return kline_list
296
+
297
+ async def request_klines(
298
+ self,
299
+ symbol: str,
300
+ interval: KlineInterval,
301
+ limit: int | None = None,
302
+ start_time: int | None = None,
303
+ end_time: int | None = None,
304
+ ) -> KlineList:
305
+ market = self._market.get(symbol)
306
+ if not market:
307
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
308
+
309
+ okx_interval = OkxEnumParser.to_okx_kline_interval(interval)
310
+ all_klines: list[Kline] = []
311
+ seen_timestamps: set[int] = set()
312
+
313
+ # First request to get the most recent data using before parameter
314
+ klines_response: OkxCandlesticksResponse = (
315
+ await self._api_client.get_api_v5_market_candles(
316
+ instId=self._market[symbol].id,
317
+ bar=okx_interval.value,
318
+ limit=100, # Maximum allowed by the API is 100
319
+ before="0", # Get the latest klines
320
+ )
321
+ )
322
+
323
+ response_klines = sorted(klines_response.data, key=lambda x: int(x.ts))
324
+ klines = [
325
+ self._handle_candlesticks(symbol=symbol, interval=interval, kline=kline)
326
+ for kline in response_klines
327
+ if int(kline.ts) not in seen_timestamps
328
+ ]
329
+ all_klines.extend(klines)
330
+ seen_timestamps.update(int(kline.ts) for kline in response_klines)
331
+
332
+ # Continue fetching older data using after parameter if needed
333
+ if (
334
+ start_time is not None
335
+ and all_klines
336
+ and int(all_klines[0].timestamp) > start_time
337
+ ):
338
+ while True:
339
+ # Use the oldest timestamp we have as the 'after' parameter
340
+ oldest_timestamp = (
341
+ min(int(kline.ts) for kline in response_klines)
342
+ if response_klines
343
+ else None
344
+ )
345
+
346
+ if not oldest_timestamp or (
347
+ start_time is not None and oldest_timestamp <= start_time
348
+ ):
349
+ break
350
+
351
+ klines_response = (
352
+ await self._api_client.get_api_v5_market_history_candles(
353
+ instId=self._market[symbol].id,
354
+ bar=okx_interval.value,
355
+ limit="100",
356
+ after=str(oldest_timestamp), # Get klines before this timestamp
357
+ )
358
+ )
359
+
360
+ response_klines = sorted(klines_response.data, key=lambda x: int(x.ts))
361
+ if not response_klines:
362
+ break
363
+
364
+ # Process klines and filter out duplicates
365
+ new_klines = [
366
+ self._handle_candlesticks(
367
+ symbol=symbol, interval=interval, kline=kline
368
+ )
369
+ for kline in response_klines
370
+ if int(kline.ts) not in seen_timestamps
371
+ ]
372
+
373
+ if not new_klines:
374
+ break
375
+
376
+ all_klines = (
377
+ new_klines + all_klines
378
+ ) # Prepend new klines as they are older
379
+ seen_timestamps.update(int(kline.ts) for kline in response_klines)
380
+
381
+ # Apply limit if specified
382
+ if limit is not None and len(all_klines) > limit:
383
+ all_klines = all_klines[-limit:] # Take the most recent klines
384
+
385
+ if end_time:
386
+ all_klines = [kline for kline in all_klines if kline.timestamp < end_time]
387
+
388
+ kline_list = KlineList(
389
+ all_klines,
390
+ fields=[
391
+ "timestamp",
392
+ "symbol",
393
+ "open",
394
+ "high",
395
+ "low",
396
+ "close",
397
+ "volume",
398
+ "quote_volume",
399
+ "confirm",
400
+ ],
401
+ )
402
+ return kline_list
403
+
404
+ def subscribe_trade(self, symbol: str | List[str]):
405
+ symbols = []
406
+ if isinstance(symbol, str):
407
+ symbol = [symbol]
408
+
409
+ for s in symbol:
410
+ market = self._market.get(s)
411
+ if not market:
412
+ raise ValueError(f"Symbol {s} not found in market")
413
+ symbols.append(market.id)
414
+
415
+ self._ws_client.subscribe_trade(symbols)
416
+
417
+ def subscribe_bookl1(self, symbol: str | List[str]):
418
+ symbols = []
419
+ if isinstance(symbol, str):
420
+ symbol = [symbol]
421
+
422
+ for s in symbol:
423
+ market = self._market.get(s)
424
+ if not market:
425
+ raise ValueError(f"Symbol {s} not found in market")
426
+ symbols.append(market.id)
427
+
428
+ self._ws_client.subscribe_order_book(symbols, channel="bbo-tbt")
429
+
430
+ def subscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
431
+ if isinstance(symbol, str):
432
+ symbol = [symbol]
433
+
434
+ if level == BookLevel.L5:
435
+ symbols = []
436
+ for s in symbol:
437
+ market = self._market.get(s)
438
+ if not market:
439
+ raise ValueError(f"Symbol {s} not found in market")
440
+ symbols.append(market.id)
441
+ self._ws_client.subscribe_order_book(symbols, channel="books5")
442
+ elif level == BookLevel.L400:
443
+ symbols = []
444
+ for s in symbol:
445
+ market = self._market.get(s)
446
+ if not market:
447
+ raise ValueError(f"Symbol {s} not found in market")
448
+ symbols.append(market.id)
449
+ self._okx_orderbook[s] = OkxOrderBook()
450
+ self._ws_client.subscribe_order_book(symbols, channel="books")
451
+ else:
452
+ raise ValueError(
453
+ f"OKX only supports BookLevel.L5 or BookLevel.L400, got {level}"
454
+ )
455
+
456
+ def subscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
457
+ symbols = []
458
+ if isinstance(symbol, str):
459
+ symbol = [symbol]
460
+
461
+ for s in symbol:
462
+ market = self._market.get(s)
463
+ if not market:
464
+ raise ValueError(f"Symbol {s} not found in market")
465
+ symbols.append(market.id)
466
+
467
+ okx_interval = OkxEnumParser.to_okx_kline_interval(interval)
468
+ self._business_ws_client.subscribe_candlesticks(symbols, okx_interval)
469
+
470
+ def subscribe_funding_rate(self, symbol: str | List[str]):
471
+ symbols = []
472
+ if isinstance(symbol, str):
473
+ symbol = [symbol]
474
+
475
+ for s in symbol:
476
+ market = self._market.get(s)
477
+ if not market:
478
+ raise ValueError(f"Symbol {s} not found in market")
479
+ symbols.append(market.id)
480
+
481
+ self._ws_client.subscribe_funding_rate(symbols)
482
+
483
+ def subscribe_index_price(self, symbol: str | List[str]):
484
+ symbols = []
485
+ if isinstance(symbol, str):
486
+ symbol = [symbol]
487
+
488
+ for s in symbol:
489
+ market = self._market.get(s)
490
+ if not market:
491
+ raise ValueError(f"Symbol {s} not found in market")
492
+ symbols.append(market.id)
493
+
494
+ self._ws_client.subscribe_index_price(symbols)
495
+
496
+ def subscribe_mark_price(self, symbol: str | List[str]):
497
+ symbols = []
498
+ if isinstance(symbol, str):
499
+ symbol = [symbol]
500
+
501
+ for s in symbol:
502
+ market = self._market.get(s)
503
+ if not market:
504
+ raise ValueError(f"Symbol {s} not found in market")
505
+ symbols.append(market.id)
506
+
507
+ self._ws_client.subscribe_mark_price(symbols)
508
+
509
+ def unsubscribe_trade(self, symbol: str | List[str]):
510
+ symbols = []
511
+ if isinstance(symbol, str):
512
+ symbol = [symbol]
513
+
514
+ for s in symbol:
515
+ market = self._market.get(s)
516
+ if not market:
517
+ raise ValueError(f"Symbol {s} not found in market")
518
+ symbols.append(market.id)
519
+
520
+ self._ws_client.unsubscribe_trade(symbols)
521
+
522
+ def unsubscribe_bookl1(self, symbol: str | List[str]):
523
+ symbols = []
524
+ if isinstance(symbol, str):
525
+ symbol = [symbol]
526
+
527
+ for s in symbol:
528
+ market = self._market.get(s)
529
+ if not market:
530
+ raise ValueError(f"Symbol {s} not found in market")
531
+ symbols.append(market.id)
532
+
533
+ self._ws_client.unsubscribe_order_book(symbols, channel="bbo-tbt")
534
+
535
+ def unsubscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
536
+ if isinstance(symbol, str):
537
+ symbol = [symbol]
538
+
539
+ if level == BookLevel.L5:
540
+ symbols = []
541
+ for s in symbol:
542
+ market = self._market.get(s)
543
+ if not market:
544
+ raise ValueError(f"Symbol {s} not found in market")
545
+ symbols.append(market.id)
546
+ self._ws_client.unsubscribe_order_book(symbols, channel="books5")
547
+ elif level == BookLevel.L400:
548
+ symbols = []
549
+ for s in symbol:
550
+ market = self._market.get(s)
551
+ if not market:
552
+ raise ValueError(f"Symbol {s} not found in market")
553
+ symbols.append(market.id)
554
+ self._okx_orderbook.pop(s, None)
555
+ self._ws_client.unsubscribe_order_book(symbols, channel="books")
556
+ else:
557
+ raise ValueError(
558
+ f"OKX only supports BookLevel.L5 or BookLevel.L400, got {level}"
559
+ )
560
+
561
+ def unsubscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
562
+ symbols = []
563
+ if isinstance(symbol, str):
564
+ symbol = [symbol]
565
+
566
+ for s in symbol:
567
+ market = self._market.get(s)
568
+ if not market:
569
+ raise ValueError(f"Symbol {s} not found in market")
570
+ symbols.append(market.id)
571
+
572
+ okx_interval = OkxEnumParser.to_okx_kline_interval(interval)
573
+ self._business_ws_client.unsubscribe_candlesticks(symbols, okx_interval)
574
+
575
+ def unsubscribe_funding_rate(self, symbol: str | List[str]):
576
+ symbols = []
577
+ if isinstance(symbol, str):
578
+ symbol = [symbol]
579
+
580
+ for s in symbol:
581
+ market = self._market.get(s)
582
+ if not market:
583
+ raise ValueError(f"Symbol {s} not found in market")
584
+ symbols.append(market.id)
585
+
586
+ self._ws_client.unsubscribe_funding_rate(symbols)
587
+
588
+ def unsubscribe_index_price(self, symbol: str | List[str]):
589
+ symbols = []
590
+ if isinstance(symbol, str):
591
+ symbol = [symbol]
592
+
593
+ for s in symbol:
594
+ market = self._market.get(s)
595
+ if not market:
596
+ raise ValueError(f"Symbol {s} not found in market")
597
+ symbols.append(market.id)
598
+
599
+ self._ws_client.unsubscribe_index_price(symbols)
600
+
601
+ def unsubscribe_mark_price(self, symbol: str | List[str]):
602
+ symbols = []
603
+ if isinstance(symbol, str):
604
+ symbol = [symbol]
605
+
606
+ for s in symbol:
607
+ market = self._market.get(s)
608
+ if not market:
609
+ raise ValueError(f"Symbol {s} not found in market")
610
+ symbols.append(market.id)
611
+
612
+ self._ws_client.unsubscribe_mark_price(symbols)
613
+
614
+ def _business_ws_msg_handler(self, raw: bytes):
615
+ # if raw == b"pong":
616
+ # self._business_ws_client._transport.notify_user_specific_pong_received()
617
+ # self._log.debug(f"Pong received:{str(raw)}")
618
+ # return
619
+ try:
620
+ ws_msg: OkxWsGeneralMsg = self._ws_msg_general_decoder.decode(raw)
621
+ if ws_msg.is_event_msg:
622
+ self._handle_event_msg(ws_msg)
623
+ else:
624
+ channel = self._require_channel(ws_msg)
625
+ if channel.startswith("candle"):
626
+ self._handle_kline(raw)
627
+ except msgspec.DecodeError:
628
+ self._log.error(f"Error decoding message: {str(raw)}")
629
+
630
+ def _ws_msg_handler(self, raw: bytes):
631
+ # if raw == b"pong":
632
+ # self._ws_client._transport.notify_user_specific_pong_received()
633
+ # self._log.debug(f"Pong received:{raw.decode()}")
634
+ # return
635
+ try:
636
+ ws_msg: OkxWsGeneralMsg = self._ws_msg_general_decoder.decode(raw)
637
+ if ws_msg.is_event_msg:
638
+ self._handle_event_msg(ws_msg)
639
+ else:
640
+ channel = self._require_channel(ws_msg)
641
+ if channel == "bbo-tbt":
642
+ self._handle_bbo_tbt(raw)
643
+ elif channel == "trades":
644
+ self._handle_trade(raw)
645
+ elif channel.startswith("candle"):
646
+ self._handle_kline(raw)
647
+ elif channel == "books5":
648
+ self._handle_book5(raw)
649
+ elif channel == "books":
650
+ self._handle_books(raw)
651
+ elif channel == "index-ticker":
652
+ self._handle_index_ticker(raw)
653
+ elif channel == "mark-price":
654
+ self._handle_mark_price(raw)
655
+ elif channel == "funding-rate":
656
+ self._handle_funding_rate(raw)
657
+ except msgspec.DecodeError as e:
658
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
659
+
660
+ def _handle_index_ticker(self, raw: bytes):
661
+ msg: OkxWsIndexTickerMsg = self._ws_msg_index_ticker_decoder.decode(raw)
662
+
663
+ id = self._require_inst_id(msg.arg.instId)
664
+ symbol = self._market_id[id]
665
+
666
+ for d in msg.data:
667
+ index_price = IndexPrice(
668
+ exchange=self._exchange_id,
669
+ symbol=symbol,
670
+ price=float(d.idxPx),
671
+ timestamp=int(d.ts),
672
+ )
673
+ self._msgbus.publish(topic="index_price", msg=index_price)
674
+
675
+ def _handle_mark_price(self, raw: bytes):
676
+ msg: OkxWsMarkPriceMsg = self._ws_msg_mark_price_decoder.decode(raw)
677
+
678
+ id = self._require_inst_id(msg.arg.instId)
679
+ symbol = self._market_id[id]
680
+
681
+ for d in msg.data:
682
+ mark_price = MarkPrice(
683
+ exchange=self._exchange_id,
684
+ symbol=symbol,
685
+ price=float(d.markPx),
686
+ timestamp=int(d.ts),
687
+ )
688
+ self._msgbus.publish(topic="mark_price", msg=mark_price)
689
+
690
+ def _handle_funding_rate(self, raw: bytes):
691
+ msg: OkxWsFundingRateMsg = self._ws_msg_funding_rate_decoder.decode(raw)
692
+
693
+ id = self._require_inst_id(msg.arg.instId)
694
+ symbol = self._market_id[id]
695
+
696
+ for d in msg.data:
697
+ funding_rate = FundingRate(
698
+ exchange=self._exchange_id,
699
+ symbol=symbol,
700
+ rate=float(d.fundingRate),
701
+ timestamp=int(d.ts),
702
+ next_funding_time=int(d.fundingTime),
703
+ )
704
+ self._msgbus.publish(topic="funding_rate", msg=funding_rate)
705
+
706
+ def _handle_book5(self, raw: bytes):
707
+ msg: OkxWsBook5Msg = self._ws_msg_book5_decoder.decode(raw)
708
+
709
+ id = self._require_inst_id(msg.arg.instId)
710
+ symbol = self._market_id[id]
711
+
712
+ for d in msg.data:
713
+ asks = [d.parse_to_book_order_data() for d in d.asks]
714
+ bids = [d.parse_to_book_order_data() for d in d.bids]
715
+ bookl2 = BookL2(
716
+ exchange=self._exchange_id,
717
+ symbol=symbol,
718
+ asks=asks,
719
+ bids=bids,
720
+ timestamp=int(d.ts),
721
+ )
722
+ self._msgbus.publish(topic="bookl2", msg=bookl2)
723
+
724
+ def _handle_books(self, raw: bytes):
725
+ msg: OkxWsBooksMsg = self._ws_msg_books_decoder.decode(raw)
726
+
727
+ id = self._require_inst_id(msg.arg.instId)
728
+ symbol = self._market_id[id]
729
+ ob = self._okx_orderbook[symbol]
730
+
731
+ for d in msg.data:
732
+ if msg.action == "snapshot":
733
+ ob.apply_snapshot(d)
734
+ else:
735
+ ob.apply_update(d)
736
+ result = ob.get_orderbook()
737
+ bookl2 = BookL2(
738
+ exchange=self._exchange_id,
739
+ symbol=symbol,
740
+ bids=result["bids"],
741
+ asks=result["asks"],
742
+ timestamp=int(d.ts),
743
+ )
744
+ self._msgbus.publish(topic="bookl2", msg=bookl2)
745
+
746
+ def _handle_event_msg(self, ws_msg: OkxWsGeneralMsg):
747
+ if ws_msg.event == "error":
748
+ self._log.error(f"Error code: {ws_msg.code}, message: {ws_msg.msg}")
749
+ elif ws_msg.event == "login":
750
+ self._log.debug("Login success")
751
+ elif ws_msg.event == "subscribe":
752
+ self._log.debug(f"Subscribed to {self._require_channel(ws_msg)}")
753
+
754
+ def _handle_kline(self, raw: bytes):
755
+ msg: OkxWsCandleMsg = self._ws_msg_candle_decoder.decode(raw)
756
+
757
+ id = self._require_inst_id(msg.arg.instId)
758
+ symbol = self._market_id[id]
759
+ okx_interval = OkxKlineInterval(self._require_arg_channel(msg.arg.channel))
760
+ interval = OkxEnumParser.parse_kline_interval(okx_interval)
761
+
762
+ for d in msg.data:
763
+ kline = Kline(
764
+ exchange=self._exchange_id,
765
+ symbol=symbol,
766
+ interval=interval,
767
+ open=float(d[1]),
768
+ high=float(d[2]),
769
+ low=float(d[3]),
770
+ close=float(d[4]),
771
+ volume=float(d[5]),
772
+ start=int(d[0]),
773
+ timestamp=self._clock.timestamp_ms(),
774
+ confirm=False if d[8] == "0" else True,
775
+ )
776
+ self._msgbus.publish(topic="kline", msg=kline)
777
+
778
+ def _handle_trade(self, raw: bytes):
779
+ msg: OkxWsTradeMsg = self._ws_msg_trade_decoder.decode(raw)
780
+ id = self._require_inst_id(msg.arg.instId)
781
+ symbol = self._market_id[id]
782
+ for d in msg.data:
783
+ trade = Trade(
784
+ exchange=self._exchange_id,
785
+ symbol=symbol,
786
+ price=float(d.px),
787
+ size=float(d.sz),
788
+ timestamp=int(d.ts),
789
+ side=OkxEnumParser.parse_order_side(d.side),
790
+ )
791
+ self._msgbus.publish(topic="trade", msg=trade)
792
+
793
+ def _handle_bbo_tbt(self, raw: bytes):
794
+ msg: OkxWsBboTbtMsg = self._ws_msg_bbo_tbt_decoder.decode(raw)
795
+
796
+ id = self._require_inst_id(msg.arg.instId)
797
+ symbol = self._market_id[id]
798
+
799
+ for d in msg.data:
800
+ if not d.bids or not d.asks:
801
+ continue
802
+
803
+ bookl1 = BookL1(
804
+ exchange=self._exchange_id,
805
+ symbol=symbol,
806
+ bid=float(d.bids[0][0]),
807
+ ask=float(d.asks[0][0]),
808
+ bid_size=float(d.bids[0][1]),
809
+ ask_size=float(d.asks[0][1]),
810
+ timestamp=int(d.ts),
811
+ )
812
+ self._msgbus.publish(topic="bookl1", msg=bookl1)
813
+
814
+ def _handle_index_candlesticks(
815
+ self,
816
+ symbol: str,
817
+ interval: KlineInterval,
818
+ kline: OkxIndexCandlesticksResponseData,
819
+ ) -> Kline:
820
+ return Kline(
821
+ exchange=self._exchange_id,
822
+ symbol=symbol,
823
+ interval=interval,
824
+ open=float(kline.o),
825
+ high=float(kline.h),
826
+ low=float(kline.l),
827
+ close=float(kline.c),
828
+ start=int(kline.ts),
829
+ timestamp=self._clock.timestamp_ms(),
830
+ confirm=False if int(kline.confirm) == 0 else True,
831
+ )
832
+
833
+ def _handle_candlesticks(
834
+ self, symbol: str, interval: KlineInterval, kline: OkxCandlesticksResponseData
835
+ ) -> Kline:
836
+ return Kline(
837
+ exchange=self._exchange_id,
838
+ symbol=symbol,
839
+ interval=interval,
840
+ open=float(kline.o),
841
+ high=float(kline.h),
842
+ low=float(kline.l),
843
+ close=float(kline.c),
844
+ volume=float(kline.vol),
845
+ quote_volume=float(kline.volCcyQuote),
846
+ start=int(kline.ts),
847
+ timestamp=self._clock.timestamp_ms(),
848
+ confirm=False if int(kline.confirm) == 0 else True,
849
+ )
850
+
851
+ async def connect(self):
852
+ await self._ws_client.connect()
853
+ await self._business_ws_client.connect()
854
+
855
+ async def wait_ready(self):
856
+ """Wait for the initial WebSocket connection to be established"""
857
+ await self._ws_client.wait_ready()
858
+ await self._business_ws_client.wait_ready()
859
+
860
+
861
+ class OkxPrivateConnector(PrivateConnector):
862
+ _api_client: OkxApiClient
863
+ _account_type: OkxAccountType
864
+ _market: Dict[str, OkxMarket]
865
+ _market_id: Dict[str, str]
866
+ _oms: OkxOrderManagementSystem
867
+
868
+ def __init__(
869
+ self,
870
+ account_type: OkxAccountType,
871
+ exchange: OkxExchangeManager,
872
+ cache: AsyncCache,
873
+ registry: OrderRegistry,
874
+ clock: LiveClock,
875
+ msgbus: MessageBus,
876
+ task_manager: TaskManager,
877
+ order_query_config: OrderQueryConfig,
878
+ enable_rate_limit: bool = True,
879
+ max_subscriptions_per_client: int | None = None,
880
+ max_clients: int | None = None,
881
+ **kwargs,
882
+ ):
883
+ if not exchange.api_key or not exchange.secret or not exchange.passphrase:
884
+ raise ValueError(
885
+ "API key, secret, and passphrase are required for private endpoints"
886
+ )
887
+
888
+ api_client = OkxApiClient(
889
+ clock=clock,
890
+ api_key=exchange.api_key,
891
+ secret=exchange.secret,
892
+ passphrase=exchange.passphrase,
893
+ testnet=account_type.is_testnet,
894
+ enable_rate_limit=enable_rate_limit,
895
+ **kwargs,
896
+ )
897
+
898
+ oms = OkxOrderManagementSystem(
899
+ account_type=account_type,
900
+ api_key=exchange.api_key,
901
+ secret=exchange.secret,
902
+ passphrase=exchange.passphrase,
903
+ market=exchange.market,
904
+ market_id=exchange.market_id,
905
+ registry=registry,
906
+ cache=cache,
907
+ api_client=api_client,
908
+ exchange_id=exchange.exchange_id,
909
+ clock=clock,
910
+ msgbus=msgbus,
911
+ task_manager=task_manager,
912
+ enable_rate_limit=enable_rate_limit,
913
+ max_subscriptions_per_client=max_subscriptions_per_client,
914
+ max_clients=max_clients,
915
+ order_query_config=order_query_config,
916
+ )
917
+
918
+ super().__init__(
919
+ account_type=account_type,
920
+ market=exchange.market,
921
+ api_client=api_client,
922
+ task_manager=task_manager,
923
+ oms=oms,
924
+ )
925
+
926
+ async def connect(self):
927
+ self._oms._ws_client.subscribe_orders()
928
+ self._oms._ws_client.subscribe_positions()
929
+ self._oms._ws_client.subscribe_account()
930
+ await self._oms._ws_client.connect()
931
+ await self._oms._ws_api_client.connect()