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,819 @@
1
+ import msgspec
2
+ from typing import Dict, List, cast
3
+ from collections import defaultdict
4
+ from walrasquant.base import PublicConnector, PrivateConnector
5
+ from walrasquant.config import OrderQueryConfig
6
+ from walrasquant.core.nautilius_core import MessageBus, LiveClock
7
+ from walrasquant.core.entity import TaskManager
8
+ from walrasquant.core.registry import OrderRegistry
9
+ from walrasquant.core.cache import AsyncCache
10
+ from walrasquant.schema import (
11
+ BookL1,
12
+ Trade,
13
+ Kline,
14
+ BookL2,
15
+ BookOrderData,
16
+ FundingRate,
17
+ IndexPrice,
18
+ MarkPrice,
19
+ KlineList,
20
+ Ticker,
21
+ BaseMarket,
22
+ )
23
+ from walrasquant.constants import (
24
+ KlineInterval,
25
+ BookLevel,
26
+ )
27
+ from walrasquant.exchange.bybit.schema import (
28
+ BybitKlineResponse,
29
+ BybitKlineResponseArray,
30
+ BybitWsMessageGeneral,
31
+ BybitWsOrderbookDepthMsg,
32
+ BybitOrderBook,
33
+ BybitWsTradeMsg,
34
+ BybitWsTickerMsg,
35
+ BybitWsKlineMsg,
36
+ BybitTicker,
37
+ BybitIndexKlineResponse,
38
+ BybitIndexKlineResponseArray,
39
+ )
40
+ from walrasquant.exchange.bybit.rest_api import BybitApiClient
41
+ from walrasquant.exchange.bybit.websockets import BybitWSClient
42
+ from walrasquant.exchange.bybit.oms import BybitOrderManagementSystem
43
+ from walrasquant.exchange.bybit.constants import (
44
+ BybitAccountType,
45
+ BybitEnumParser,
46
+ )
47
+ from walrasquant.exchange.bybit.exchange import BybitExchangeManager
48
+
49
+
50
+ class BybitPublicConnector(PublicConnector):
51
+ _api_client: BybitApiClient
52
+ _ws_client: BybitWSClient
53
+ _account_type: BybitAccountType
54
+
55
+ def __init__(
56
+ self,
57
+ account_type: BybitAccountType,
58
+ exchange: BybitExchangeManager,
59
+ msgbus: MessageBus,
60
+ clock: LiveClock,
61
+ task_manager: TaskManager,
62
+ custom_url: str | None = None,
63
+ enable_rate_limit: bool = True,
64
+ max_subscriptions_per_client: int | None = None,
65
+ max_clients: int | None = None,
66
+ ):
67
+ if account_type in {BybitAccountType.UNIFIED, BybitAccountType.UNIFIED_TESTNET}:
68
+ raise ValueError(
69
+ "Please not using `BybitAccountType.UNIFIED` or `BybitAccountType.UNIFIED_TESTNET` in `PublicConnector`"
70
+ )
71
+
72
+ super().__init__(
73
+ account_type=account_type,
74
+ market=exchange.market,
75
+ market_id=exchange.market_id,
76
+ exchange_id=exchange.exchange_id,
77
+ ws_client=BybitWSClient(
78
+ account_type=account_type,
79
+ handler=self._ws_msg_handler,
80
+ clock=clock,
81
+ task_manager=task_manager,
82
+ custom_url=custom_url,
83
+ max_subscriptions_per_client=max_subscriptions_per_client,
84
+ max_clients=max_clients,
85
+ ),
86
+ clock=clock,
87
+ msgbus=msgbus,
88
+ api_client=BybitApiClient(
89
+ clock=clock,
90
+ testnet=account_type.is_testnet,
91
+ enable_rate_limit=enable_rate_limit,
92
+ ),
93
+ task_manager=task_manager,
94
+ )
95
+ self._ws_msg_trade_decoder = msgspec.json.Decoder(BybitWsTradeMsg)
96
+ self._ws_msg_orderbook_decoder = msgspec.json.Decoder(BybitWsOrderbookDepthMsg)
97
+ self._ws_msg_general_decoder = msgspec.json.Decoder(BybitWsMessageGeneral)
98
+ self._ws_msg_kline_decoder = msgspec.json.Decoder(BybitWsKlineMsg)
99
+ self._ws_msg_ticker_decoder = msgspec.json.Decoder(BybitWsTickerMsg)
100
+ self._bookl1_orderbook = defaultdict(BybitOrderBook)
101
+ self._bookl2_orderbook = defaultdict(BybitOrderBook)
102
+ self._ticker: Dict[str, BybitTicker] = defaultdict(BybitTicker)
103
+
104
+ @property
105
+ def market_type(self):
106
+ if self._account_type.is_spot:
107
+ return "_spot"
108
+ elif self._account_type.is_linear:
109
+ return "_linear"
110
+ elif self._account_type.is_inverse:
111
+ return "_inverse"
112
+ else:
113
+ raise ValueError(f"Unsupported BybitAccountType.{self._account_type.value}")
114
+
115
+ def _get_category(self, market: BaseMarket):
116
+ if market.spot:
117
+ return "spot"
118
+ elif market.linear:
119
+ return "linear"
120
+ elif market.inverse:
121
+ return "inverse"
122
+ else:
123
+ raise ValueError(f"Unsupported market type: {market.type}")
124
+
125
+ def _ws_msg_handler(self, raw: bytes):
126
+ try:
127
+ ws_msg: BybitWsMessageGeneral = self._ws_msg_general_decoder.decode(raw)
128
+ # if ws_msg.ret_msg == "pong":
129
+ # self._ws_client._transport.notify_user_specific_pong_received()
130
+ # self._log.debug(f"Pong received {str(ws_msg)}")
131
+ # return
132
+ if ws_msg.success is False:
133
+ self._log.error(f"WebSocket error: {ws_msg}")
134
+ return
135
+
136
+ if "orderbook.1" in ws_msg.topic:
137
+ self._handle_orderbook(raw)
138
+ elif ws_msg.topic.startswith("orderbook."):
139
+ self._handle_orderbook_depth(raw)
140
+ elif "publicTrade" in ws_msg.topic:
141
+ self._handle_trade(raw)
142
+ elif "kline" in ws_msg.topic:
143
+ self._handle_kline(raw)
144
+ elif "tickers" in ws_msg.topic:
145
+ self._handle_ticker(raw)
146
+ except msgspec.DecodeError as e:
147
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
148
+
149
+ def _handle_ticker(self, raw: bytes):
150
+ msg: BybitWsTickerMsg = self._ws_msg_ticker_decoder.decode(raw)
151
+ id = msg.data.symbol + self.market_type
152
+ symbol = self._market_id[id]
153
+
154
+ ticker = self._ticker[symbol]
155
+ ticker.parse_ticker(msg)
156
+ funding_rate = ticker.fundingRate
157
+ next_funding_time = ticker.nextFundingTime
158
+ index_price = ticker.indexPrice
159
+ mark_price = ticker.markPrice
160
+ if (
161
+ funding_rate is None
162
+ or next_funding_time is None
163
+ or index_price is None
164
+ or mark_price is None
165
+ ):
166
+ raise ValueError(f"Incomplete ticker payload for {symbol}")
167
+
168
+ funding_rate_msg = FundingRate(
169
+ exchange=self._exchange_id,
170
+ symbol=symbol,
171
+ rate=float(funding_rate),
172
+ timestamp=msg.ts,
173
+ next_funding_time=int(next_funding_time),
174
+ )
175
+
176
+ index_price_msg = IndexPrice(
177
+ exchange=self._exchange_id,
178
+ symbol=symbol,
179
+ price=float(index_price),
180
+ timestamp=msg.ts,
181
+ )
182
+
183
+ mark_price_msg = MarkPrice(
184
+ exchange=self._exchange_id,
185
+ symbol=symbol,
186
+ price=float(mark_price),
187
+ timestamp=msg.ts,
188
+ )
189
+
190
+ self._msgbus.publish(topic="funding_rate", msg=funding_rate_msg)
191
+ self._msgbus.publish(topic="index_price", msg=index_price_msg)
192
+ self._msgbus.publish(topic="mark_price", msg=mark_price_msg)
193
+
194
+ def _handle_kline(self, raw: bytes):
195
+ msg: BybitWsKlineMsg = self._ws_msg_kline_decoder.decode(raw)
196
+ id = msg.topic.split(".")[-1] + self.market_type
197
+ symbol = self._market_id[id]
198
+ for d in msg.data:
199
+ interval = BybitEnumParser.parse_kline_interval(d.interval)
200
+ kline = Kline(
201
+ exchange=self._exchange_id,
202
+ symbol=symbol,
203
+ interval=interval,
204
+ open=float(d.open),
205
+ high=float(d.high),
206
+ low=float(d.low),
207
+ close=float(d.close),
208
+ volume=float(d.volume),
209
+ start=d.start,
210
+ confirm=d.confirm,
211
+ timestamp=msg.ts,
212
+ )
213
+ self._msgbus.publish(topic="kline", msg=kline)
214
+
215
+ def _handle_trade(self, raw: bytes):
216
+ msg: BybitWsTradeMsg = self._ws_msg_trade_decoder.decode(raw)
217
+ for d in msg.data:
218
+ id = d.s + self.market_type
219
+ symbol = self._market_id[id]
220
+ trade = Trade(
221
+ exchange=self._exchange_id,
222
+ symbol=symbol,
223
+ price=float(d.p),
224
+ size=float(d.v),
225
+ timestamp=msg.ts,
226
+ side=BybitEnumParser.parse_order_side(d.S),
227
+ )
228
+ self._msgbus.publish(topic="trade", msg=trade)
229
+
230
+ def _handle_orderbook(self, raw: bytes):
231
+ msg: BybitWsOrderbookDepthMsg = self._ws_msg_orderbook_decoder.decode(raw)
232
+ id = msg.data.s + self.market_type
233
+ symbol = self._market_id[id]
234
+ res = self._bookl1_orderbook[symbol].parse_orderbook_depth(msg, levels=1)
235
+
236
+ bid, bid_size = (
237
+ (res["bids"][0].price, res["bids"][0].size) if res["bids"] else (0, 0)
238
+ )
239
+ ask, ask_size = (
240
+ (res["asks"][0].price, res["asks"][0].size) if res["asks"] else (0, 0)
241
+ )
242
+
243
+ bookl1 = BookL1(
244
+ exchange=self._exchange_id,
245
+ symbol=symbol,
246
+ timestamp=msg.ts,
247
+ bid=bid,
248
+ bid_size=bid_size,
249
+ ask=ask,
250
+ ask_size=ask_size,
251
+ )
252
+ self._msgbus.publish(topic="bookl1", msg=bookl1)
253
+
254
+ def _handle_orderbook_depth(self, raw: bytes):
255
+ msg: BybitWsOrderbookDepthMsg = self._ws_msg_orderbook_decoder.decode(raw)
256
+ id = msg.data.s + self.market_type
257
+ symbol = self._market_id[id]
258
+ depth = int(msg.topic.split(".")[1])
259
+ res = self._bookl2_orderbook[symbol].parse_orderbook_depth(msg, levels=depth)
260
+
261
+ bids = res["bids"] if res["bids"] else [BookOrderData(price=0, size=0)]
262
+ asks = res["asks"] if res["asks"] else [BookOrderData(price=0, size=0)]
263
+
264
+ bookl2 = BookL2(
265
+ exchange=self._exchange_id,
266
+ symbol=symbol,
267
+ timestamp=msg.ts,
268
+ bids=bids,
269
+ asks=asks,
270
+ )
271
+ self._msgbus.publish(topic="bookl2", msg=bookl2)
272
+
273
+ async def request_ticker(
274
+ self,
275
+ symbol: str,
276
+ ) -> Ticker:
277
+ """Request 24hr ticker data"""
278
+ market = self._market.get(symbol)
279
+ if not market:
280
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
281
+ category = self._get_category(market)
282
+ id = market.id
283
+ ticker_response = await self._api_client.get_v5_market_tickers(
284
+ category=category, symbol=id
285
+ )
286
+ for ticker in ticker_response.result.list:
287
+ return Ticker(
288
+ exchange=self._exchange_id,
289
+ symbol=symbol,
290
+ last_price=float(ticker.lastPrice),
291
+ timestamp=ticker_response.time,
292
+ volume=float(ticker.volume24h),
293
+ volumeCcy=float(ticker.turnover24h),
294
+ )
295
+ raise ValueError(f"No ticker data found for symbol {symbol}")
296
+
297
+ async def request_all_tickers(
298
+ self,
299
+ ) -> Dict[str, Ticker]:
300
+ """Request 24hr ticker data for multiple symbols"""
301
+ if self._account_type.is_spot:
302
+ category = "spot"
303
+ elif self._account_type.is_linear:
304
+ category = "linear"
305
+ elif self._account_type.is_inverse:
306
+ category = "inverse"
307
+ ticker_response = await self._api_client.get_v5_market_tickers(
308
+ category=category,
309
+ )
310
+ tickers = {}
311
+ for ticker in ticker_response.result.list:
312
+ id = ticker.symbol + self.market_type
313
+ symbol = self._market_id.get(id)
314
+ if not symbol:
315
+ continue
316
+ tickers[symbol] = Ticker(
317
+ exchange=self._exchange_id,
318
+ symbol=symbol,
319
+ last_price=float(ticker.lastPrice),
320
+ timestamp=ticker_response.time,
321
+ volume=float(ticker.volume24h),
322
+ volumeCcy=float(ticker.turnover24h),
323
+ )
324
+ return tickers
325
+
326
+ async def request_index_klines(
327
+ self,
328
+ symbol: str,
329
+ interval: KlineInterval,
330
+ limit: int | None = None,
331
+ start_time: int | None = None,
332
+ end_time: int | None = None,
333
+ ) -> KlineList:
334
+ market = self._market.get(symbol)
335
+ if not market:
336
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
337
+ if market.spot:
338
+ raise ValueError("Spot market is not supported for index klines")
339
+ category = self._get_category(market)
340
+ id = market.id
341
+ bybit_interval = BybitEnumParser.to_bybit_kline_interval(interval)
342
+ all_klines: list[Kline] = []
343
+ seen_timestamps: set[int] = set()
344
+ prev_start_time: int | None = None
345
+
346
+ while True:
347
+ # Check for infinite loop condition
348
+ if prev_start_time is not None and prev_start_time == start_time:
349
+ break
350
+ prev_start_time = start_time
351
+
352
+ klines_response: BybitIndexKlineResponse = (
353
+ await self._api_client.get_v5_market_index_price_kline(
354
+ category=category,
355
+ symbol=id,
356
+ interval=bybit_interval.value,
357
+ limit=1000,
358
+ start=start_time,
359
+ end=end_time,
360
+ )
361
+ )
362
+
363
+ # Sort klines by start time and filter out duplicates
364
+ response_klines = sorted(
365
+ klines_response.result.list, key=lambda k: int(k.startTime)
366
+ )
367
+ klines: list[Kline] = [
368
+ self._handle_index_candlesticks(
369
+ symbol=symbol,
370
+ interval=interval,
371
+ kline=kline,
372
+ timestamp=klines_response.time,
373
+ )
374
+ for kline in response_klines
375
+ if int(kline.startTime) not in seen_timestamps
376
+ ]
377
+
378
+ all_klines.extend(klines)
379
+ seen_timestamps.update(int(kline.startTime) for kline in response_klines)
380
+
381
+ # If no new klines were found, break
382
+ if not klines:
383
+ break
384
+
385
+ # Update the start_time to fetch the next set of bars
386
+ start_time = int(response_klines[-1].startTime) + 1
387
+
388
+ # No more bars to fetch if we've reached the end time
389
+ if end_time is not None and start_time >= end_time:
390
+ break
391
+
392
+ # If limit is specified, return the last 'limit' number of klines
393
+ if limit is not None and len(all_klines) > limit:
394
+ all_klines = all_klines[-limit:]
395
+
396
+ kline_list = KlineList(
397
+ all_klines,
398
+ fields=[
399
+ "timestamp",
400
+ "symbol",
401
+ "open",
402
+ "high",
403
+ "low",
404
+ "close",
405
+ "confirm",
406
+ ],
407
+ )
408
+ return kline_list
409
+
410
+ async def request_klines(
411
+ self,
412
+ symbol: str,
413
+ interval: KlineInterval,
414
+ limit: int | None = None,
415
+ start_time: int | None = None,
416
+ end_time: int | None = None,
417
+ ) -> KlineList:
418
+ market = self._market.get(symbol)
419
+ if not market:
420
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
421
+ category = self._get_category(market)
422
+ id = market.id
423
+ bybit_interval = BybitEnumParser.to_bybit_kline_interval(interval)
424
+ all_klines: list[Kline] = []
425
+ seen_timestamps: set[int] = set()
426
+ prev_start_time: int | None = None
427
+
428
+ while True:
429
+ # Check for infinite loop condition
430
+ if prev_start_time is not None and prev_start_time == start_time:
431
+ break
432
+ prev_start_time = start_time
433
+
434
+ klines_response: BybitKlineResponse = (
435
+ await self._api_client.get_v5_market_kline(
436
+ category=category,
437
+ symbol=id,
438
+ interval=bybit_interval.value,
439
+ limit=1000,
440
+ start=start_time,
441
+ end=end_time,
442
+ )
443
+ )
444
+
445
+ # Sort klines by start time and filter out duplicates
446
+ response_klines = sorted(
447
+ klines_response.result.list, key=lambda k: int(k.startTime)
448
+ )
449
+ klines: list[Kline] = [
450
+ self._handle_candlesticks(
451
+ symbol=symbol,
452
+ interval=interval,
453
+ kline=kline,
454
+ timestamp=klines_response.time,
455
+ )
456
+ for kline in response_klines
457
+ if int(kline.startTime) not in seen_timestamps
458
+ ]
459
+
460
+ all_klines.extend(klines)
461
+ seen_timestamps.update(int(kline.startTime) for kline in response_klines)
462
+
463
+ # If no new klines were found, break
464
+ if not klines:
465
+ break
466
+
467
+ # Update the start_time to fetch the next set of bars
468
+ start_time = int(response_klines[-1].startTime) + 1
469
+
470
+ # No more bars to fetch if we've reached the end time
471
+ if end_time is not None and start_time >= end_time:
472
+ break
473
+
474
+ # If limit is specified, return the last 'limit' number of klines
475
+ if limit is not None and len(all_klines) > limit:
476
+ all_klines = all_klines[-limit:]
477
+
478
+ kline_list = KlineList(
479
+ all_klines,
480
+ fields=[
481
+ "timestamp",
482
+ "symbol",
483
+ "open",
484
+ "high",
485
+ "low",
486
+ "close",
487
+ "volume",
488
+ "turnover",
489
+ "confirm",
490
+ ],
491
+ )
492
+ return kline_list
493
+
494
+ def subscribe_funding_rate(self, symbol: str | List[str]):
495
+ symbols = []
496
+ if isinstance(symbol, str):
497
+ symbol = [symbol]
498
+
499
+ for s in symbol:
500
+ market = self._market.get(s)
501
+ if not market:
502
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
503
+ symbols.append(market.id)
504
+
505
+ self._ws_client.subscribe_ticker(symbols)
506
+
507
+ def unsubscribe_funding_rate(self, symbol: str | List[str]):
508
+ symbols = []
509
+ if isinstance(symbol, str):
510
+ symbol = [symbol]
511
+
512
+ for s in symbol:
513
+ market = self._market.get(s)
514
+ if not market:
515
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
516
+ symbols.append(market.id)
517
+
518
+ self._ws_client.unsubscribe_ticker(symbols)
519
+
520
+ def subscribe_index_price(self, symbol: str | List[str]):
521
+ symbols = []
522
+ if isinstance(symbol, str):
523
+ symbol = [symbol]
524
+
525
+ for s in symbol:
526
+ market = self._market.get(s)
527
+ if not market:
528
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
529
+ symbols.append(market.id)
530
+
531
+ self._ws_client.subscribe_ticker(symbols)
532
+
533
+ def unsubscribe_index_price(self, symbol: str | List[str]):
534
+ symbols = []
535
+ if isinstance(symbol, str):
536
+ symbol = [symbol]
537
+
538
+ for s in symbol:
539
+ market = self._market.get(s)
540
+ if not market:
541
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
542
+ symbols.append(market.id)
543
+
544
+ self._ws_client.unsubscribe_ticker(symbols)
545
+
546
+ def subscribe_mark_price(self, symbol: str | List[str]):
547
+ symbols = []
548
+ if isinstance(symbol, str):
549
+ symbol = [symbol]
550
+
551
+ for s in symbol:
552
+ market = self._market.get(s)
553
+ if not market:
554
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
555
+ symbols.append(market.id)
556
+
557
+ self._ws_client.subscribe_ticker(symbols)
558
+
559
+ def unsubscribe_mark_price(self, symbol: str | List[str]):
560
+ symbols = []
561
+ if isinstance(symbol, str):
562
+ symbol = [symbol]
563
+
564
+ for s in symbol:
565
+ market = self._market.get(s)
566
+ if not market:
567
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
568
+ symbols.append(market.id)
569
+
570
+ self._ws_client.unsubscribe_ticker(symbols)
571
+
572
+ def subscribe_bookl1(self, symbol: str | List[str]):
573
+ symbols = []
574
+ if isinstance(symbol, str):
575
+ symbol = [symbol]
576
+
577
+ for s in symbol:
578
+ market = self._market.get(s)
579
+ if not market:
580
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
581
+ symbols.append(market.id)
582
+
583
+ self._ws_client.subscribe_order_book(symbols, depth=1)
584
+
585
+ def unsubscribe_bookl1(self, symbol: str | List[str]):
586
+ symbols = []
587
+ if isinstance(symbol, str):
588
+ symbol = [symbol]
589
+
590
+ for s in symbol:
591
+ market = self._market.get(s)
592
+ if not market:
593
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
594
+ symbols.append(market.id)
595
+
596
+ self._ws_client.unsubscribe_order_book(symbols, depth=1)
597
+
598
+ def subscribe_trade(self, symbol: str | List[str]):
599
+ symbols = []
600
+ if isinstance(symbol, str):
601
+ symbol = [symbol]
602
+
603
+ for s in symbol:
604
+ market = self._market.get(s)
605
+ if not market:
606
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
607
+ symbols.append(market.id)
608
+
609
+ self._ws_client.subscribe_trade(symbols)
610
+
611
+ def unsubscribe_trade(self, symbol: str | List[str]):
612
+ symbols = []
613
+ if isinstance(symbol, str):
614
+ symbol = [symbol]
615
+
616
+ for s in symbol:
617
+ market = self._market.get(s)
618
+ if not market:
619
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
620
+ symbols.append(market.id)
621
+
622
+ self._ws_client.unsubscribe_trade(symbols)
623
+
624
+ def subscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
625
+ symbols = []
626
+ if isinstance(symbol, str):
627
+ symbol = [symbol]
628
+
629
+ for s in symbol:
630
+ market = self._market.get(s)
631
+ if not market:
632
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
633
+ symbols.append(market.id)
634
+
635
+ bybit_interval = BybitEnumParser.to_bybit_kline_interval(interval)
636
+ self._ws_client.subscribe_kline(symbols, bybit_interval)
637
+
638
+ def unsubscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
639
+ symbols = []
640
+ if isinstance(symbol, str):
641
+ symbol = [symbol]
642
+
643
+ for s in symbol:
644
+ market = self._market.get(s)
645
+ if not market:
646
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
647
+ symbols.append(market.id)
648
+
649
+ bybit_interval = BybitEnumParser.to_bybit_kline_interval(interval)
650
+ self._ws_client.unsubscribe_kline(symbols, bybit_interval)
651
+
652
+ def subscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
653
+ supported = {BookLevel.L50, BookLevel.L200, BookLevel.L1000}
654
+ if level not in supported:
655
+ raise ValueError(
656
+ f"Unsupported book level for Bybit: {level}. Use L50, L200, or L1000."
657
+ )
658
+
659
+ symbols = []
660
+ if isinstance(symbol, str):
661
+ symbol = [symbol]
662
+
663
+ for s in symbol:
664
+ market = self._market.get(s)
665
+ if not market:
666
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
667
+ symbols.append(market.id)
668
+
669
+ self._ws_client.subscribe_order_book(symbols, depth=int(level.value))
670
+
671
+ def unsubscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
672
+ supported = {BookLevel.L50, BookLevel.L200, BookLevel.L1000}
673
+ if level not in supported:
674
+ raise ValueError(
675
+ f"Unsupported book level for Bybit: {level}. Use L50, L200, or L1000."
676
+ )
677
+
678
+ symbols = []
679
+ if isinstance(symbol, str):
680
+ symbol = [symbol]
681
+
682
+ for s in symbol:
683
+ market = self._market.get(s)
684
+ if not market:
685
+ raise ValueError(f"Symbol {s} formated wrongly, or not supported")
686
+ symbols.append(market.id)
687
+
688
+ self._ws_client.unsubscribe_order_book(symbols, depth=int(level.value))
689
+
690
+ def _handle_index_candlesticks(
691
+ self,
692
+ symbol: str,
693
+ interval: KlineInterval,
694
+ kline: BybitIndexKlineResponseArray,
695
+ timestamp: int,
696
+ ) -> Kline:
697
+ local_timestamp = self._clock.timestamp_ms()
698
+ confirm = (
699
+ True
700
+ if local_timestamp >= int(kline.startTime) + interval.seconds * 1000 - 1
701
+ else False
702
+ )
703
+ return Kline(
704
+ exchange=self._exchange_id,
705
+ symbol=symbol,
706
+ interval=interval,
707
+ open=float(kline.openPrice),
708
+ high=float(kline.highPrice),
709
+ low=float(kline.lowPrice),
710
+ close=float(kline.closePrice),
711
+ start=int(kline.startTime),
712
+ timestamp=timestamp,
713
+ confirm=confirm,
714
+ )
715
+
716
+ def _handle_candlesticks(
717
+ self,
718
+ symbol: str,
719
+ interval: KlineInterval,
720
+ kline: BybitKlineResponseArray,
721
+ timestamp: int,
722
+ ) -> Kline:
723
+ local_timestamp = self._clock.timestamp_ms()
724
+ confirm = (
725
+ True
726
+ if local_timestamp >= int(kline.startTime) + interval.seconds * 1000 - 1
727
+ else False
728
+ )
729
+ return Kline(
730
+ exchange=self._exchange_id,
731
+ symbol=symbol,
732
+ interval=interval,
733
+ open=float(kline.openPrice),
734
+ high=float(kline.highPrice),
735
+ low=float(kline.lowPrice),
736
+ close=float(kline.closePrice),
737
+ volume=float(kline.volume),
738
+ start=int(kline.startTime),
739
+ turnover=float(kline.turnover),
740
+ timestamp=timestamp,
741
+ confirm=confirm,
742
+ )
743
+
744
+
745
+ class BybitPrivateConnector(PrivateConnector):
746
+ _account_type: BybitAccountType
747
+ _market: Dict[str, BaseMarket]
748
+ _market_id: Dict[str, str]
749
+ _api_client: BybitApiClient
750
+ _oms: BybitOrderManagementSystem
751
+
752
+ def __init__(
753
+ self,
754
+ account_type: BybitAccountType,
755
+ exchange: BybitExchangeManager,
756
+ cache: AsyncCache,
757
+ registry: OrderRegistry,
758
+ clock: LiveClock,
759
+ msgbus: MessageBus,
760
+ task_manager: TaskManager,
761
+ order_query_config: OrderQueryConfig,
762
+ enable_rate_limit: bool = True,
763
+ max_subscriptions_per_client: int | None = None,
764
+ max_clients: int | None = None,
765
+ **kwargs,
766
+ ):
767
+ if not exchange.api_key or not exchange.secret:
768
+ raise ValueError("API key and secret are required for private endpoints")
769
+
770
+ if account_type not in {
771
+ BybitAccountType.UNIFIED,
772
+ BybitAccountType.UNIFIED_TESTNET,
773
+ }:
774
+ raise ValueError(
775
+ "Please using `BybitAccountType.UNIFIED` or `BybitAccountType.UNIFIED_TESTNET` in `PrivateConnector`"
776
+ )
777
+
778
+ api_client = BybitApiClient(
779
+ clock=clock,
780
+ api_key=exchange.api_key,
781
+ secret=exchange.secret,
782
+ testnet=account_type.is_testnet,
783
+ enable_rate_limit=enable_rate_limit,
784
+ **kwargs,
785
+ )
786
+
787
+ oms = BybitOrderManagementSystem(
788
+ account_type=account_type,
789
+ api_key=exchange.api_key,
790
+ secret=exchange.secret,
791
+ market=cast(Dict[str, BaseMarket], exchange.market),
792
+ market_id=exchange.market_id,
793
+ registry=registry,
794
+ cache=cache,
795
+ api_client=api_client,
796
+ exchange_id=exchange.exchange_id,
797
+ clock=clock,
798
+ msgbus=msgbus,
799
+ task_manager=task_manager,
800
+ enable_rate_limit=enable_rate_limit,
801
+ max_subscriptions_per_client=max_subscriptions_per_client,
802
+ max_clients=max_clients,
803
+ order_query_config=order_query_config,
804
+ )
805
+
806
+ super().__init__(
807
+ account_type=account_type,
808
+ market=cast(Dict[str, BaseMarket], exchange.market),
809
+ api_client=api_client,
810
+ task_manager=task_manager,
811
+ oms=oms,
812
+ )
813
+
814
+ async def connect(self):
815
+ self._oms._ws_client.subscribe_order()
816
+ self._oms._ws_client.subscribe_position()
817
+ self._oms._ws_client.subscribe_wallet()
818
+ await self._oms._ws_client.connect()
819
+ await self._oms._ws_api_client.connect()