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,570 @@
1
+ import hmac
2
+ import hashlib
3
+ import msgspec
4
+ from typing import Any, Dict, List, Optional
5
+ from urllib.parse import urljoin, urlencode
6
+ import aiohttp
7
+ import asyncio
8
+ from yarl import URL
9
+ from decimal import Decimal
10
+ from walrasquant.base import ApiClient, RetryManager
11
+ from walrasquant.exchange.bybit.constants import (
12
+ BybitBaseUrl,
13
+ BybitRateLimiter,
14
+ )
15
+
16
+ from walrasquant.exchange.bybit.error import BybitError
17
+ from walrasquant.core.nautilius_core import hmac_signature, LiveClock
18
+ from walrasquant.exchange.bybit.schema import (
19
+ BybitResponse,
20
+ BybitOrderResponse,
21
+ BybitPositionResponse,
22
+ BybitOrderHistoryResponse,
23
+ BybitOpenOrdersResponse,
24
+ BybitWalletBalanceResponse,
25
+ BybitKlineResponse,
26
+ BybitIndexKlineResponse,
27
+ BybitBatchOrderResponse,
28
+ BybitBatchCancelOrderResponse,
29
+ BybitTickersResponse,
30
+ )
31
+
32
+
33
+ class BybitApiClient(ApiClient):
34
+ _limiter: BybitRateLimiter
35
+
36
+ def __init__(
37
+ self,
38
+ clock: LiveClock,
39
+ api_key: Optional[str] = None,
40
+ secret: Optional[str] = None,
41
+ timeout: int = 10,
42
+ testnet: bool = False,
43
+ enable_rate_limit: bool = True,
44
+ max_retries: int = 0,
45
+ delay_initial_ms: int = 100,
46
+ delay_max_ms: int = 800,
47
+ backoff_factor: int = 2,
48
+ ):
49
+ """
50
+ ### Testnet:
51
+ `https://api-testnet.bybit.com`
52
+
53
+ ### Mainnet:
54
+ (both endpoints are available):
55
+ `https://api.bybit.com`
56
+ `https://api.bytick.com`
57
+
58
+ ### Important:
59
+ Netherland users: use `https://api.bybit.nl` for mainnet
60
+ Hong Kong users: use `https://api.byhkbit.com` for mainnet
61
+ Turkey users: use `https://api.bybit-tr.com` for mainnet
62
+ Kazakhstan users: use `https://api.bybit.kz` for mainnet
63
+ """
64
+
65
+ super().__init__(
66
+ clock=clock,
67
+ api_key=api_key,
68
+ secret=secret,
69
+ timeout=timeout,
70
+ rate_limiter=BybitRateLimiter(enable_rate_limit),
71
+ retry_manager=RetryManager(
72
+ max_retries=max_retries,
73
+ delay_initial_ms=delay_initial_ms,
74
+ delay_max_ms=delay_max_ms,
75
+ backoff_factor=backoff_factor,
76
+ exc_types=(BybitError,),
77
+ retry_check=lambda e: (
78
+ isinstance(e, BybitError)
79
+ and e.code
80
+ in [
81
+ 429,
82
+ 10000,
83
+ 10016,
84
+ 3400214,
85
+ 170007,
86
+ 177002,
87
+ 500001,
88
+ ]
89
+ ), # please refer to https://bybit-exchange.github.io/docs/v5/error
90
+ ),
91
+ )
92
+ self._recv_window = 5000
93
+
94
+ if testnet:
95
+ self._base_url = BybitBaseUrl.TESTNET.base_url
96
+ else:
97
+ self._base_url = BybitBaseUrl.MAINNET_1.base_url
98
+
99
+ self._headers = {
100
+ "Content-Type": "application/json",
101
+ "User-Agent": "TradingBot/1.0",
102
+ }
103
+
104
+ if api_key:
105
+ self._headers["X-BAPI-API-KEY"] = api_key
106
+
107
+ self._msg_decoder = msgspec.json.Decoder()
108
+ self._msg_encoder = msgspec.json.Encoder()
109
+ self._response_decoder = msgspec.json.Decoder(BybitResponse)
110
+ self._order_response_decoder = msgspec.json.Decoder(BybitOrderResponse)
111
+ self._position_response_decoder = msgspec.json.Decoder(BybitPositionResponse)
112
+ self._order_history_response_decoder = msgspec.json.Decoder(
113
+ BybitOrderHistoryResponse
114
+ )
115
+ self._open_orders_response_decoder = msgspec.json.Decoder(
116
+ BybitOpenOrdersResponse
117
+ )
118
+ self._wallet_balance_response_decoder = msgspec.json.Decoder(
119
+ BybitWalletBalanceResponse
120
+ )
121
+ self._kline_response_decoder = msgspec.json.Decoder(BybitKlineResponse)
122
+ self._index_kline_response_decoder = msgspec.json.Decoder(
123
+ BybitIndexKlineResponse
124
+ )
125
+ self._batch_order_response_decoder = msgspec.json.Decoder(
126
+ BybitBatchOrderResponse
127
+ )
128
+ self._batch_cancel_order_response_decoder = msgspec.json.Decoder(
129
+ BybitBatchCancelOrderResponse
130
+ )
131
+ self._tickers_response_decoder = msgspec.json.Decoder(BybitTickersResponse)
132
+
133
+ def _generate_signature(self, payload: str) -> List[str]:
134
+ api_key = self._api_key or ""
135
+ secret = self._secret or ""
136
+ timestamp = str(self._clock.timestamp_ms())
137
+
138
+ param = str(timestamp) + api_key + str(self._recv_window) + payload
139
+ hash = hmac.new(bytes(secret, "utf-8"), param.encode("utf-8"), hashlib.sha256)
140
+ signature = hash.hexdigest()
141
+ return [signature, timestamp]
142
+
143
+ def _generate_signature_v2(self, payload: str) -> List[str]:
144
+ api_key = self._api_key or ""
145
+ secret = self._secret or ""
146
+ timestamp = str(self._clock.timestamp_ms())
147
+ param = f"{timestamp}{api_key}{self._recv_window}{payload}"
148
+ signature = hmac_signature(secret, param) # return hex digest string
149
+ return [signature, timestamp]
150
+
151
+ async def _fetch(
152
+ self,
153
+ method: str,
154
+ base_url: Optional[str],
155
+ endpoint: str,
156
+ payload: Optional[Dict[str, Any]] = None,
157
+ signed: bool = False,
158
+ ):
159
+ assert self._retry_manager is not None
160
+ await self._limiter.ip_limit()
161
+ result = await self._retry_manager.run(
162
+ name=f"{method} {endpoint}",
163
+ func=self._fetch_async,
164
+ method=method,
165
+ base_url=base_url,
166
+ endpoint=endpoint,
167
+ payload=payload,
168
+ signed=signed,
169
+ )
170
+ assert result is not None
171
+ return result
172
+
173
+ async def _fetch_async(
174
+ self,
175
+ method: str,
176
+ base_url: Optional[str],
177
+ endpoint: str,
178
+ payload: Optional[Dict[str, Any]] = None,
179
+ signed: bool = False,
180
+ ):
181
+ assert base_url is not None
182
+ await self._init_session()
183
+
184
+ url = urljoin(base_url, endpoint)
185
+ payload = payload or {}
186
+
187
+ payload_str = (
188
+ urlencode(payload)
189
+ if method == "GET"
190
+ else self._msg_encoder.encode(payload).decode("utf-8")
191
+ )
192
+
193
+ headers = self._headers
194
+ if signed:
195
+ signature, timestamp = self._generate_signature_v2(payload_str)
196
+ headers = {
197
+ **headers,
198
+ "X-BAPI-TIMESTAMP": timestamp,
199
+ "X-BAPI-SIGN": signature,
200
+ "X-BAPI-RECV-WINDOW": str(self._recv_window),
201
+ }
202
+
203
+ if method == "GET":
204
+ url += f"?{payload_str}"
205
+ payload_str = None
206
+
207
+ assert self._session is not None
208
+ try:
209
+ self._log.debug(f"Request: {url} {payload_str}")
210
+ async with self._session.request(
211
+ method=method,
212
+ url=url,
213
+ headers=headers,
214
+ data=payload_str,
215
+ ) as response:
216
+ raw = await response.read()
217
+ if response.status >= 400:
218
+ error_data = self._msg_decoder.decode(raw) if raw else {}
219
+ ret_msg = (
220
+ error_data.get("retMsg", "Unknown error")
221
+ if isinstance(error_data, dict)
222
+ else str(error_data)
223
+ )
224
+ raise BybitError(
225
+ code=response.status,
226
+ message=ret_msg,
227
+ )
228
+ bybit_response: BybitResponse = self._response_decoder.decode(raw)
229
+ if bybit_response.retCode == 0:
230
+ return raw
231
+ else:
232
+ raise BybitError(
233
+ code=bybit_response.retCode,
234
+ message=bybit_response.retMsg,
235
+ )
236
+ except asyncio.TimeoutError as e:
237
+ self._log.error(f"Timeout {method} Url: {url} - {e}")
238
+ raise
239
+ except aiohttp.ClientError as e:
240
+ self._log.error(f"Request Error {method} Url: {url} - {e}")
241
+ raise
242
+ except Exception as e:
243
+ self._log.error(f"Error {method} Url: {url} - {e}")
244
+ raise
245
+
246
+ async def post_v5_order_create(
247
+ self,
248
+ category: str,
249
+ symbol: str,
250
+ side: str,
251
+ order_type: str,
252
+ qty: Decimal,
253
+ **kwargs,
254
+ ) -> BybitOrderResponse:
255
+ """
256
+ https://bybit-exchange.github.io/docs/v5/order/create-order
257
+ """
258
+ endpoint = "/v5/order/create"
259
+ payload = {
260
+ "category": category,
261
+ "symbol": symbol,
262
+ "side": side,
263
+ "orderType": order_type,
264
+ "qty": str(qty),
265
+ **kwargs,
266
+ }
267
+ await self._limiter.order_limit(category, endpoint, cost=1)
268
+ raw = await self._fetch("POST", self._base_url, endpoint, payload, signed=True)
269
+ return self._order_response_decoder.decode(raw)
270
+
271
+ async def post_v5_order_cancel(
272
+ self, category: str, symbol: str, **kwargs
273
+ ) -> BybitOrderResponse:
274
+ """
275
+ https://bybit-exchange.github.io/docs/v5/order/cancel-order
276
+ """
277
+ endpoint = "/v5/order/cancel"
278
+ payload = {
279
+ "category": category,
280
+ "symbol": symbol,
281
+ **kwargs,
282
+ }
283
+ # await self._limiter.order_limit(category, endpoint, cost=1)
284
+ raw = await self._fetch("POST", self._base_url, endpoint, payload, signed=True)
285
+ return self._order_response_decoder.decode(raw)
286
+
287
+ async def get_v5_position_list(
288
+ self, category: str, **kwargs
289
+ ) -> BybitPositionResponse:
290
+ endpoint = "/v5/position/list"
291
+ payload = {
292
+ "category": category,
293
+ **kwargs,
294
+ }
295
+ await self._limiter.query_limit(endpoint)
296
+ payload = {k: v for k, v in payload.items() if v is not None}
297
+ raw = await self._fetch_async(
298
+ "GET", self._base_url, endpoint, payload, signed=True
299
+ )
300
+ return self._position_response_decoder.decode(raw)
301
+
302
+ async def get_v5_order_realtime(self, category: str, **kwargs):
303
+ """
304
+ https://bybit-exchange.github.io/docs/v5/order/open-order
305
+ """
306
+ endpoint = "/v5/order/realtime"
307
+ payload = {
308
+ "category": category,
309
+ **kwargs,
310
+ }
311
+ raw = await self._fetch("GET", self._base_url, endpoint, payload, signed=True)
312
+ return self._open_orders_response_decoder.decode(raw)
313
+
314
+ async def get_v5_order_history(self, category: str, **kwargs):
315
+ """
316
+ https://bybit-exchange.github.io/docs/v5/order/order-list
317
+ """
318
+ endpoint = "/v5/order/history"
319
+ payload = {
320
+ "category": category,
321
+ **kwargs,
322
+ }
323
+ raw = await self._fetch("GET", self._base_url, endpoint, payload, signed=True)
324
+ return self._order_history_response_decoder.decode(raw)
325
+
326
+ async def get_v5_account_wallet_balance(
327
+ self, account_type: str, **kwargs
328
+ ) -> BybitWalletBalanceResponse:
329
+ endpoint = "/v5/account/wallet-balance"
330
+ payload = {
331
+ "accountType": account_type,
332
+ **kwargs,
333
+ }
334
+ await self._limiter.query_limit(endpoint)
335
+ raw = await self._fetch_async(
336
+ "GET", self._base_url, endpoint, payload, signed=True
337
+ )
338
+ return self._wallet_balance_response_decoder.decode(raw)
339
+
340
+ async def post_v5_order_amend(
341
+ self,
342
+ category: str,
343
+ symbol: str,
344
+ orderId: str | None = None,
345
+ orderLinkId: str | None = None,
346
+ orderIv: str | None = None,
347
+ triggerPrice: str | None = None,
348
+ qty: str | None = None,
349
+ price: str | None = None,
350
+ tpslMode: str | None = None,
351
+ takeProfit: str | None = None,
352
+ stopLoss: str | None = None,
353
+ tpTriggerBy: str | None = None,
354
+ slTriggerBy: str | None = None,
355
+ triggerBy: str | None = None,
356
+ tpLimitPrice: str | None = None,
357
+ slLimitPrice: str | None = None,
358
+ ):
359
+ """
360
+ https://bybit-exchange.github.io/docs/v5/order/amend-order
361
+ """
362
+ endpoint = "/v5/order/amend"
363
+ payload = {
364
+ "category": category,
365
+ "symbol": symbol,
366
+ "orderId": orderId,
367
+ "orderLinkId": orderLinkId,
368
+ "orderIv": orderIv,
369
+ "triggerPrice": triggerPrice,
370
+ "qty": qty,
371
+ "price": price,
372
+ "tpslMode": tpslMode,
373
+ "takeProfit": takeProfit,
374
+ "stopLoss": stopLoss,
375
+ "tpTriggerBy": tpTriggerBy,
376
+ "slTriggerBy": slTriggerBy,
377
+ "triggerBy": triggerBy,
378
+ "tpLimitPrice": tpLimitPrice,
379
+ "slLimitPrice": slLimitPrice,
380
+ }
381
+ await self._limiter.order_limit(category, endpoint, cost=1)
382
+ payload = {k: v for k, v in payload.items() if v is not None}
383
+ raw = await self._fetch("POST", self._base_url, endpoint, payload, signed=True)
384
+ return self._order_response_decoder.decode(raw)
385
+
386
+ async def post_v5_order_cancel_all(
387
+ self,
388
+ category: str,
389
+ symbol: str | None = None,
390
+ baseCoin: str | None = None,
391
+ settleCoin: str | None = None,
392
+ orderFilter: str | None = None,
393
+ stopOrderType: str | None = None,
394
+ ):
395
+ endpoint = "/v5/order/cancel-all"
396
+ payload = {
397
+ "category": category,
398
+ "symbol": symbol,
399
+ "baseCoin": baseCoin,
400
+ "settleCoin": settleCoin,
401
+ "orderFilter": orderFilter,
402
+ "stopOrderType": stopOrderType,
403
+ }
404
+ # await self._limiter.order_limit(category, endpoint, cost=1)
405
+ payload = {k: v for k, v in payload.items() if v is not None}
406
+ raw = await self._fetch("POST", self._base_url, endpoint, payload, signed=True)
407
+ return self._msg_decoder.decode(raw)
408
+
409
+ async def get_v5_market_kline(
410
+ self,
411
+ category: str,
412
+ symbol: str,
413
+ interval: str,
414
+ start: int | None = None,
415
+ end: int | None = None,
416
+ limit: int | None = None,
417
+ ):
418
+ endpoint = "/v5/market/kline"
419
+ payload = {
420
+ "category": category,
421
+ "symbol": symbol,
422
+ "interval": interval,
423
+ "start": start,
424
+ "end": end,
425
+ "limit": limit,
426
+ }
427
+ await self._limiter.public_limit(endpoint)
428
+ payload = {k: v for k, v in payload.items() if v is not None}
429
+ raw = await self._fetch_async(
430
+ "GET", self._base_url, endpoint, payload, signed=False
431
+ )
432
+ return self._kline_response_decoder.decode(raw)
433
+
434
+ async def get_v5_market_index_price_kline(
435
+ self,
436
+ category: str,
437
+ symbol: str,
438
+ interval: str,
439
+ start: int | None = None,
440
+ end: int | None = None,
441
+ limit: int | None = None,
442
+ ):
443
+ endpoint = "/v5/market/index-price-kline"
444
+ payload = {
445
+ "category": category,
446
+ "symbol": symbol,
447
+ "interval": interval,
448
+ "start": start,
449
+ "end": end,
450
+ "limit": limit,
451
+ }
452
+ await self._limiter.public_limit(endpoint)
453
+ payload = {k: v for k, v in payload.items() if v is not None}
454
+ raw = await self._fetch_async(
455
+ "GET", self._base_url, endpoint, payload, signed=False
456
+ )
457
+ return self._index_kline_response_decoder.decode(raw)
458
+
459
+ async def post_v5_order_create_batch(
460
+ self,
461
+ category: str,
462
+ request: List[Dict[str, Any]],
463
+ ) -> BybitBatchOrderResponse:
464
+ """
465
+ Place multiple orders in a single request
466
+ https://bybit-exchange.github.io/docs/v5/order/batch-place
467
+
468
+ Maximum orders per request:
469
+ - Option: 20 orders
470
+ - Inverse: 20 orders
471
+ - Linear: 20 orders
472
+ - Spot: 10 orders
473
+ """
474
+ endpoint = "/v5/order/create-batch"
475
+ payload = {
476
+ "category": category,
477
+ "request": request,
478
+ }
479
+
480
+ await self._limiter.order_limit(category, endpoint, cost=len(request))
481
+
482
+ raw = await self._fetch("POST", self._base_url, endpoint, payload, signed=True)
483
+ return self._batch_order_response_decoder.decode(raw)
484
+
485
+ async def post_v5_order_cancel_batch(
486
+ self,
487
+ category: str,
488
+ request: List[Dict[str, Any]],
489
+ ) -> BybitBatchCancelOrderResponse:
490
+ """
491
+ Cancel multiple orders in a single request
492
+ https://bybit-exchange.github.io/docs/v5/order/batch-cancel
493
+
494
+ Maximum orders per request:
495
+ - Linear / Inverse: 20 orders
496
+ - Spot: 10 orders
497
+ """
498
+ endpoint = "/v5/order/cancel-batch"
499
+ payload = {
500
+ "category": category,
501
+ "request": request,
502
+ }
503
+ # await self._limiter.order_limit(category, endpoint, cost=len(request))
504
+ raw = await self._fetch("POST", self._base_url, endpoint, payload, signed=True)
505
+ return self._batch_cancel_order_response_decoder.decode(raw)
506
+
507
+ async def get_v5_market_tickers(
508
+ self,
509
+ category: str,
510
+ symbol: str | None = None,
511
+ base_coin: str | None = None,
512
+ exp_date: str | None = None,
513
+ ) -> BybitTickersResponse:
514
+ """
515
+ GET /v5/market/tickers
516
+
517
+ Query for the latest price snapshot, best bid/ask price, and trading volume
518
+ in the last 24 hours.
519
+
520
+ Covers: Spot / USDT contract / USDC contract / Inverse contract / Option
521
+
522
+ Args:
523
+ category: Product type (spot, linear, inverse, option)
524
+ symbol: Symbol name (optional), like BTCUSDT, uppercase only
525
+ base_coin: Base coin (optional), uppercase only. Apply to option only
526
+ exp_date: Expiry date (optional), e.g., 25DEC22. Apply to option only
527
+
528
+ Returns:
529
+ BybitTickersResponse: Response containing ticker data
530
+ """
531
+ endpoint = "/v5/market/tickers"
532
+ payload = {
533
+ "category": category,
534
+ "symbol": symbol,
535
+ "baseCoin": base_coin,
536
+ "expDate": exp_date,
537
+ }
538
+ payload = {k: v for k, v in payload.items() if v is not None}
539
+
540
+ await self._limiter.public_limit(endpoint)
541
+ raw = await self._fetch_async(
542
+ "GET", self._base_url, endpoint, payload, signed=False
543
+ )
544
+ return self._tickers_response_decoder.decode(raw)
545
+
546
+ async def get_v5_position_limit_info(
547
+ self,
548
+ symbol: str,
549
+ ) -> Dict[str, Any]:
550
+ """
551
+ GET /v5/position/limit-info
552
+
553
+ Retrieve position limit information for a specific symbol and category.
554
+
555
+ Args:
556
+ category: Product type (linear, inverse)
557
+ symbol: Symbol name, like BTCUSDT, uppercase only
558
+ Returns:
559
+ Dict[str, Any]: Response containing position limit information
560
+ """
561
+ endpoint = "/v5/position/limit-info"
562
+ payload = {
563
+ "symbol": symbol,
564
+ }
565
+
566
+ await self._limiter.query_limit(endpoint)
567
+ raw = await self._fetch_async(
568
+ "GET", self._base_url, endpoint, payload, signed=True
569
+ )
570
+ return self._msg_decoder.decode(raw)