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,1195 @@
1
+ import msgspec
2
+ from typing import Dict, List
3
+ from decimal import Decimal
4
+
5
+ from walrasquant.error import PositionModeError
6
+ from walrasquant.exchange.bybit.error import BybitRateLimitError, BybitError
7
+
8
+ from walrasquant.core.nautilius_core import LiveClock, MessageBus
9
+ from walrasquant.base import OrderManagementSystem
10
+ from walrasquant.core.entity import TaskManager
11
+ from walrasquant.core.registry import OrderRegistry
12
+ from walrasquant.core.cache import AsyncCache
13
+ from walrasquant.schema import (
14
+ Order,
15
+ Position,
16
+ BatchOrderSubmit,
17
+ BaseMarket,
18
+ CancelOrderSubmit,
19
+ )
20
+ from walrasquant.constants import (
21
+ OrderSide,
22
+ OrderStatus,
23
+ ExchangeType,
24
+ OrderType,
25
+ TimeInForce,
26
+ PositionSide,
27
+ TriggerType,
28
+ )
29
+ from walrasquant.config import OrderQueryConfig
30
+ from walrasquant.exchange.bybit.schema import (
31
+ BybitOrder,
32
+ BybitWsMessageGeneral,
33
+ BybitWsApiGeneralMsg,
34
+ BybitWsOrderMsg,
35
+ BybitWsPositionMsg,
36
+ BybitWsAccountWalletMsg,
37
+ BybitWalletBalanceResponse,
38
+ BybitPositionStruct,
39
+ BybitWsApiOrderMsg,
40
+ )
41
+ from walrasquant.exchange.bybit.rest_api import BybitApiClient
42
+ from walrasquant.exchange.bybit.websockets import BybitWSClient, BybitWSApiClient
43
+ from walrasquant.exchange.bybit.constants import (
44
+ BybitAccountType,
45
+ BybitEnumParser,
46
+ BybitProductType,
47
+ BybitOrderType,
48
+ BybitTimeInForce,
49
+ BybitPositionIdx,
50
+ )
51
+
52
+
53
+ class BybitOrderManagementSystem(OrderManagementSystem):
54
+ _ws_client: BybitWSClient
55
+ _account_type: BybitAccountType
56
+ _market: Dict[str, BaseMarket]
57
+ _market_id: Dict[str, str]
58
+ _api_client: BybitApiClient
59
+
60
+ def __init__(
61
+ self,
62
+ account_type: BybitAccountType,
63
+ api_key: str,
64
+ secret: str,
65
+ market: Dict[str, BaseMarket],
66
+ market_id: Dict[str, str],
67
+ registry: OrderRegistry,
68
+ cache: AsyncCache,
69
+ api_client: BybitApiClient,
70
+ exchange_id: ExchangeType,
71
+ clock: LiveClock,
72
+ msgbus: MessageBus,
73
+ task_manager: TaskManager,
74
+ enable_rate_limit: bool,
75
+ order_query_config: OrderQueryConfig,
76
+ max_subscriptions_per_client: int | None = None,
77
+ max_clients: int | None = None,
78
+ ):
79
+ super().__init__(
80
+ account_type=account_type,
81
+ market=market,
82
+ market_id=market_id,
83
+ registry=registry,
84
+ cache=cache,
85
+ api_client=api_client,
86
+ ws_client=BybitWSClient(
87
+ account_type=account_type,
88
+ handler=self._ws_msg_handler,
89
+ task_manager=task_manager,
90
+ clock=clock,
91
+ api_key=api_key,
92
+ secret=secret,
93
+ max_subscriptions_per_client=max_subscriptions_per_client,
94
+ max_clients=max_clients,
95
+ ),
96
+ exchange_id=exchange_id,
97
+ clock=clock,
98
+ msgbus=msgbus,
99
+ task_manager=task_manager,
100
+ order_query_config=order_query_config,
101
+ )
102
+
103
+ self._ws_api_client = BybitWSApiClient(
104
+ account_type=account_type,
105
+ api_key=api_key,
106
+ secret=secret,
107
+ handler=self._ws_api_msg_handler,
108
+ task_manager=task_manager,
109
+ clock=clock,
110
+ enable_rate_limit=enable_rate_limit,
111
+ )
112
+
113
+ self._ws_api_msg_general_decoder = msgspec.json.Decoder(BybitWsApiGeneralMsg)
114
+ self._ws_api_msg_order_decoder = msgspec.json.Decoder(BybitWsApiOrderMsg)
115
+ self._ws_msg_general_decoder = msgspec.json.Decoder(BybitWsMessageGeneral)
116
+ self._ws_msg_order_update_decoder = msgspec.json.Decoder(BybitWsOrderMsg)
117
+ self._ws_msg_position_decoder = msgspec.json.Decoder(BybitWsPositionMsg)
118
+ self._ws_msg_wallet_decoder = msgspec.json.Decoder(BybitWsAccountWalletMsg)
119
+
120
+ def _parse_order_create(self, raw: bytes):
121
+ msg = self._ws_api_msg_order_decoder.decode(raw)
122
+ oid = msg.oid
123
+
124
+ tmp_order = self._registry.get_tmp_order(oid)
125
+ if not tmp_order:
126
+ return
127
+ symbol = tmp_order.symbol
128
+ amount = tmp_order.amount
129
+ type = tmp_order.type
130
+ price = tmp_order.price
131
+ side = tmp_order.side
132
+ time_in_force = tmp_order.time_in_force
133
+ reduce_only = tmp_order.reduce_only
134
+ ts = self._clock.timestamp_ms()
135
+ if msg.is_success:
136
+ data = msg.data
137
+ if data is None or data.orderId is None:
138
+ raise ValueError("Missing Bybit order create response data")
139
+ ordId = data.orderId
140
+ self._log.debug(f"[{symbol}] new order success: oid: {oid} eid: {ordId}")
141
+ order = Order(
142
+ exchange=self._exchange_id,
143
+ symbol=symbol,
144
+ oid=oid,
145
+ eid=ordId,
146
+ side=side,
147
+ status=OrderStatus.PENDING,
148
+ amount=amount,
149
+ timestamp=ts,
150
+ type=type,
151
+ price=price,
152
+ time_in_force=time_in_force,
153
+ reduce_only=reduce_only,
154
+ )
155
+ self.order_status_update(order) # INITIALIZED -> PENDING
156
+ else:
157
+ self._log.error(f"[{symbol}] new order failed: oid: {oid} {msg.error_msg}")
158
+ order = Order(
159
+ exchange=self._exchange_id,
160
+ symbol=symbol,
161
+ oid=oid,
162
+ status=OrderStatus.FAILED,
163
+ amount=amount,
164
+ timestamp=ts,
165
+ side=side,
166
+ type=type,
167
+ price=price,
168
+ time_in_force=time_in_force,
169
+ reduce_only=reduce_only,
170
+ reason=msg.error_msg,
171
+ )
172
+ self.order_status_update(order)
173
+
174
+ def _parse_order_cancel(self, raw: bytes):
175
+ msg = self._ws_api_msg_order_decoder.decode(raw)
176
+ oid = msg.oid
177
+ tmp_order = self._registry.get_tmp_order(oid)
178
+ if not tmp_order:
179
+ return
180
+ symbol = tmp_order.symbol
181
+ amount = tmp_order.amount
182
+ side = tmp_order.side
183
+ type = tmp_order.type
184
+ price = tmp_order.price
185
+ time_in_force = tmp_order.time_in_force
186
+ reduce_only = tmp_order.reduce_only
187
+ ts = self._clock.timestamp_ms()
188
+ if msg.is_success:
189
+ data = msg.data
190
+ if data is None or data.orderId is None:
191
+ raise ValueError("Missing Bybit order cancel response data")
192
+ ordId = data.orderId
193
+ self._log.debug(
194
+ f"[{symbol}] canceling order success: oid: {oid} id: {ordId}"
195
+ )
196
+ order = Order(
197
+ exchange=self._exchange_id,
198
+ symbol=symbol,
199
+ oid=oid,
200
+ eid=ordId,
201
+ side=side,
202
+ status=OrderStatus.CANCELING,
203
+ amount=amount,
204
+ type=type,
205
+ price=price,
206
+ time_in_force=time_in_force,
207
+ timestamp=ts,
208
+ reduce_only=reduce_only,
209
+ )
210
+ self.order_status_update(order)
211
+ else:
212
+ self._log.error(
213
+ f"[{symbol}] canceling order failed: oid: {oid} {msg.error_msg}"
214
+ )
215
+ order = Order(
216
+ exchange=self._exchange_id,
217
+ symbol=symbol,
218
+ oid=oid,
219
+ status=OrderStatus.CANCEL_FAILED,
220
+ amount=amount,
221
+ side=side,
222
+ type=type,
223
+ price=price,
224
+ time_in_force=time_in_force,
225
+ timestamp=ts,
226
+ reduce_only=reduce_only,
227
+ reason=msg.error_msg,
228
+ )
229
+ self.order_status_update(order)
230
+
231
+ def _ws_api_msg_handler(self, raw: bytes):
232
+ try:
233
+ ws_msg = self._ws_api_msg_general_decoder.decode(raw)
234
+ # if ws_msg.is_pong:
235
+ # self._ws_api_client._transport.notify_user_specific_pong_received()
236
+ # self._log.debug(f"Pong received {str(ws_msg)}")
237
+ # return
238
+ if ws_msg.is_order_create:
239
+ self._parse_order_create(raw)
240
+ elif ws_msg.is_order_cancel:
241
+ self._parse_order_cancel(raw)
242
+ except msgspec.DecodeError as e:
243
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
244
+
245
+ def _ws_msg_handler(self, raw: bytes):
246
+ try:
247
+ ws_msg = self._ws_msg_general_decoder.decode(raw)
248
+ # if ws_msg.op == "pong":
249
+ # self._ws_client._transport.notify_user_specific_pong_received()
250
+ # self._log.debug(f"Pong received {str(ws_msg)}")
251
+ # return
252
+ if ws_msg.success is False:
253
+ self._log.error(f"WebSocket error: {ws_msg}")
254
+ return
255
+ if "order" in ws_msg.topic:
256
+ self._parse_order_update(raw)
257
+ elif "position" in ws_msg.topic:
258
+ self._parse_position_update(raw)
259
+ elif "wallet" == ws_msg.topic:
260
+ self._parse_wallet_update(raw)
261
+ except msgspec.DecodeError as e:
262
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
263
+
264
+ def _get_category(self, market: BaseMarket):
265
+ if market.spot:
266
+ return "spot"
267
+ elif market.linear:
268
+ return "linear"
269
+ elif market.inverse:
270
+ return "inverse"
271
+ else:
272
+ raise ValueError(f"Unsupported market type: {market.type}")
273
+
274
+ async def cancel_order_ws(self, oid: str, symbol: str, **kwargs):
275
+ market = self._market.get(symbol)
276
+ if not market:
277
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
278
+ id = market.id
279
+ category = self._get_category(market)
280
+ params = {
281
+ "category": category,
282
+ "symbol": id,
283
+ "orderLinkId": oid,
284
+ **kwargs,
285
+ }
286
+
287
+ await self._ws_api_client.cancel_order(id=oid, **params)
288
+
289
+ async def cancel_order(self, oid: str, symbol: str, **kwargs):
290
+ market = self._market.get(symbol)
291
+ if not market:
292
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
293
+ id = market.id
294
+ category = self._get_category(market)
295
+ params = {
296
+ "category": category,
297
+ "symbol": id,
298
+ "orderLinkId": oid,
299
+ **kwargs,
300
+ }
301
+ try:
302
+ res = await self._api_client.post_v5_order_cancel(**params)
303
+ order = Order(
304
+ oid=oid,
305
+ eid=res.result.orderId,
306
+ exchange=self._exchange_id,
307
+ timestamp=res.time,
308
+ symbol=symbol,
309
+ status=OrderStatus.CANCELING,
310
+ )
311
+ except BybitRateLimitError as e:
312
+ error_msg = f"rate_limit (retry_after={e.retry_after:.1f}s): {str(e)}"
313
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
314
+ order = Order(
315
+ oid=oid,
316
+ exchange=self._exchange_id,
317
+ timestamp=self._clock.timestamp_ms(),
318
+ symbol=symbol,
319
+ status=OrderStatus.CANCEL_FAILED,
320
+ reason=error_msg,
321
+ )
322
+ except BybitError as e:
323
+ error_msg = str(e)
324
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
325
+ order = Order(
326
+ oid=oid,
327
+ exchange=self._exchange_id,
328
+ timestamp=self._clock.timestamp_ms(),
329
+ symbol=symbol,
330
+ status=OrderStatus.CANCEL_FAILED,
331
+ reason=error_msg,
332
+ )
333
+ except Exception as e:
334
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
335
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
336
+ order = Order(
337
+ oid=oid,
338
+ exchange=self._exchange_id,
339
+ timestamp=self._clock.timestamp_ms(),
340
+ symbol=symbol,
341
+ status=OrderStatus.CANCEL_FAILED,
342
+ reason=error_msg,
343
+ )
344
+ self.order_status_update(order)
345
+
346
+ async def _init_account_balance(self):
347
+ res: BybitWalletBalanceResponse = (
348
+ await self._api_client.get_v5_account_wallet_balance(account_type="UNIFIED")
349
+ )
350
+ for result in res.result.list:
351
+ self._cache._apply_balance(self._account_type, result.parse_to_balances())
352
+
353
+ async def _get_all_positions_list(
354
+ self, category: BybitProductType, settle_coin: str | None = None
355
+ ) -> list[BybitPositionStruct]:
356
+ all_positions = []
357
+ next_page_cursor = ""
358
+
359
+ while True:
360
+ res = await self._api_client.get_v5_position_list(
361
+ category=category.value,
362
+ settleCoin=settle_coin,
363
+ limit=200,
364
+ cursor=next_page_cursor,
365
+ )
366
+
367
+ all_positions.extend(res.result.list)
368
+
369
+ # If there's no next page cursor, we've reached the end
370
+ if not res.result.nextPageCursor:
371
+ break
372
+
373
+ next_page_cursor = res.result.nextPageCursor
374
+
375
+ return all_positions
376
+
377
+ async def _init_position(self):
378
+ res_linear_usdt = await self._get_all_positions_list(
379
+ BybitProductType.LINEAR, settle_coin="USDT"
380
+ )
381
+ res_linear_usdc = await self._get_all_positions_list(
382
+ BybitProductType.LINEAR, settle_coin="USDC"
383
+ )
384
+ res_inverse = await self._get_all_positions_list(BybitProductType.INVERSE)
385
+
386
+ self._apply_cache_position(res_linear_usdt, BybitProductType.LINEAR)
387
+ self._apply_cache_position(res_linear_usdc, BybitProductType.LINEAR)
388
+ self._apply_cache_position(res_inverse, BybitProductType.INVERSE)
389
+
390
+ self._cache.get_all_positions()
391
+
392
+ async def _position_mode_check(self):
393
+ # NOTE: no need to implement this for bybit, we do position mode check in _get_all_positions_list
394
+ pass
395
+
396
+ def _apply_cache_position(
397
+ self, positions: list[BybitPositionStruct], category: BybitProductType
398
+ ):
399
+ for result in positions:
400
+ side = result.side.parse_to_position_side()
401
+ if side == PositionSide.FLAT:
402
+ signed_amount = Decimal(0)
403
+ side = None
404
+ elif side == PositionSide.LONG:
405
+ signed_amount = Decimal(result.size)
406
+ elif side == PositionSide.SHORT:
407
+ signed_amount = -Decimal(result.size)
408
+
409
+ if category.is_inverse:
410
+ market_id = result.symbol + "_inverse"
411
+ elif category.is_linear:
412
+ market_id = result.symbol + "_linear"
413
+ else:
414
+ raise ValueError(f"Unsupported position category: {category}")
415
+
416
+ symbol = self._market_id[market_id]
417
+
418
+ if not result.positionIdx.is_one_way_mode():
419
+ raise PositionModeError(
420
+ f"Please Set Position Mode to `One-Way Mode` in Bybit App for {symbol}"
421
+ )
422
+
423
+ position = Position(
424
+ symbol=symbol,
425
+ exchange=self._exchange_id,
426
+ side=side,
427
+ signed_amount=signed_amount,
428
+ entry_price=float(result.avgPrice),
429
+ unrealized_pnl=float(result.unrealisedPnl),
430
+ realized_pnl=float(result.cumRealisedPnl),
431
+ )
432
+ self._cache._apply_position(position)
433
+
434
+ async def create_tp_sl_order(
435
+ self,
436
+ oid: str,
437
+ symbol: str,
438
+ side: OrderSide,
439
+ type: OrderType,
440
+ amount: Decimal,
441
+ price: Decimal | None = None,
442
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
443
+ tp_order_type: OrderType | None = None,
444
+ tp_trigger_price: Decimal | None = None,
445
+ tp_price: Decimal | None = None,
446
+ tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
447
+ sl_order_type: OrderType | None = None,
448
+ sl_trigger_price: Decimal | None = None,
449
+ sl_price: Decimal | None = None,
450
+ sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
451
+ **kwargs,
452
+ ) -> Order:
453
+ """Create a take profit and stop loss order"""
454
+ market = self._market.get(symbol)
455
+ if not market:
456
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
457
+ id = market.id
458
+
459
+ category = self._get_category(market)
460
+
461
+ tif = time_in_force or TimeInForce.GTC
462
+ tif = time_in_force or TimeInForce.GTC
463
+ params: dict[str, object] = {
464
+ "category": category,
465
+ "symbol": id,
466
+ "side": BybitEnumParser.to_bybit_order_side(side).value,
467
+ "qty": amount,
468
+ "orderLinkId": oid,
469
+ }
470
+
471
+ if type.is_limit:
472
+ if not price:
473
+ raise ValueError("Price is required for limit order")
474
+ params["price"] = str(price)
475
+ params["order_type"] = BybitOrderType.LIMIT.value
476
+ params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(tif).value
477
+ elif type.is_post_only:
478
+ if not price:
479
+ raise ValueError("Price is required for post-only order")
480
+ params["order_type"] = BybitOrderType.LIMIT.value
481
+ params["price"] = str(price)
482
+ params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
483
+ elif type == OrderType.MARKET:
484
+ params["order_type"] = BybitOrderType.MARKET.value
485
+
486
+ if market.spot:
487
+ params["marketUnit"] = "baseCoin"
488
+
489
+ if tp_order_type:
490
+ params["takeProfit"] = str(tp_trigger_price)
491
+ params["triggerBy"] = BybitEnumParser.to_bybit_trigger_type(
492
+ tp_trigger_type or TriggerType.LAST_PRICE
493
+ ).value
494
+ if tp_order_type.is_limit:
495
+ if not tp_price:
496
+ raise ValueError("Price is required for limit take profit order")
497
+ params["tpOrderType"] = BybitOrderType.LIMIT.value
498
+ params["tpLimitPrice"] = str(tp_price)
499
+
500
+ if sl_order_type:
501
+ params["stopLoss"] = str(sl_trigger_price)
502
+ params["triggerBy"] = BybitEnumParser.to_bybit_trigger_type(
503
+ sl_trigger_type or TriggerType.LAST_PRICE
504
+ ).value
505
+ if sl_order_type.is_limit:
506
+ if not sl_price:
507
+ raise ValueError("Price is required for limit stop loss order")
508
+ params["slOrderType"] = BybitOrderType.LIMIT.value
509
+ params["slLimitPrice"] = str(sl_price)
510
+
511
+ if not market.spot:
512
+ tpslMode = kwargs.pop("tpslMode", "Partial")
513
+ params["tpslMode"] = tpslMode
514
+
515
+ params.update(kwargs)
516
+
517
+ try:
518
+ res = await self._api_client.post_v5_order_create(
519
+ category=category,
520
+ symbol=id,
521
+ side=BybitEnumParser.to_bybit_order_side(side).value,
522
+ order_type=str(params["order_type"]),
523
+ qty=amount,
524
+ **{
525
+ k: v
526
+ for k, v in params.items()
527
+ if k not in {"category", "symbol", "side", "order_type", "qty"}
528
+ },
529
+ )
530
+
531
+ order = Order(
532
+ exchange=self._exchange_id,
533
+ eid=res.result.orderId,
534
+ oid=oid,
535
+ timestamp=int(res.time),
536
+ symbol=symbol,
537
+ type=type,
538
+ side=side,
539
+ amount=amount,
540
+ price=float(price) if price else None,
541
+ time_in_force=time_in_force,
542
+ status=OrderStatus.PENDING,
543
+ filled=Decimal(0),
544
+ remaining=amount,
545
+ reduce_only=False,
546
+ )
547
+ return order
548
+ except Exception as e:
549
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
550
+ self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
551
+ order = Order(
552
+ exchange=self._exchange_id,
553
+ timestamp=self._clock.timestamp_ms(),
554
+ symbol=symbol,
555
+ oid=oid,
556
+ type=type,
557
+ side=side,
558
+ amount=amount,
559
+ price=float(price) if price else None,
560
+ time_in_force=time_in_force,
561
+ status=OrderStatus.FAILED,
562
+ filled=Decimal(0),
563
+ remaining=amount,
564
+ reason=error_msg,
565
+ )
566
+ return order
567
+
568
+ async def create_batch_orders(self, orders: List[BatchOrderSubmit]):
569
+ if not orders:
570
+ raise ValueError("No orders provided for batch submission")
571
+
572
+ # Get category from first order
573
+ first_market = self._market.get(orders[0].symbol)
574
+ if not first_market:
575
+ raise ValueError(f"Symbol {orders[0].symbol} not found in market")
576
+ category = self._get_category(first_market)
577
+
578
+ batch_orders = []
579
+ for order in orders:
580
+ market = self._market.get(order.symbol)
581
+ if not market:
582
+ raise ValueError(f"Symbol {order.symbol} not found in market")
583
+ id = market.id
584
+
585
+ params: dict[str, object] = {
586
+ "symbol": id,
587
+ "side": BybitEnumParser.to_bybit_order_side(order.side).value,
588
+ "qty": str(order.amount),
589
+ "orderLinkId": order.oid,
590
+ }
591
+ if order.type.is_limit:
592
+ if not order.price:
593
+ raise ValueError("Price is required for limit order")
594
+ params["orderType"] = BybitOrderType.LIMIT.value
595
+ params["price"] = str(order.price)
596
+ params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(
597
+ order.time_in_force
598
+ ).value
599
+ elif order.type.is_post_only:
600
+ if not order.price:
601
+ raise ValueError("Price is required for limit order")
602
+ params["orderType"] = BybitOrderType.LIMIT.value
603
+ params["price"] = str(order.price)
604
+ params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
605
+ elif order.type == OrderType.MARKET:
606
+ params["orderType"] = BybitOrderType.MARKET.value
607
+
608
+ if order.reduce_only:
609
+ params["reduceOnly"] = True
610
+ if market.spot:
611
+ params["marketUnit"] = "baseCoin"
612
+ params.update(order.kwargs)
613
+ batch_orders.append(params)
614
+
615
+ try:
616
+ res = await self._api_client.post_v5_order_create_batch(
617
+ category=category, request=batch_orders
618
+ )
619
+ for order, res_order, res_ext in zip(
620
+ orders, res.result.list, res.retExtInfo.list
621
+ ):
622
+ if res_ext.code == 0:
623
+ res_batch_order = Order(
624
+ exchange=self._exchange_id,
625
+ oid=order.oid,
626
+ eid=res_order.orderId,
627
+ timestamp=int(res_order.createAt),
628
+ symbol=order.symbol,
629
+ type=order.type,
630
+ side=order.side,
631
+ amount=order.amount,
632
+ price=float(order.price) if order.price else None,
633
+ time_in_force=order.time_in_force,
634
+ status=OrderStatus.PENDING,
635
+ filled=Decimal(0),
636
+ remaining=order.amount,
637
+ reduce_only=order.reduce_only,
638
+ )
639
+ else:
640
+ res_batch_order = Order(
641
+ exchange=self._exchange_id,
642
+ timestamp=self._clock.timestamp_ms(),
643
+ symbol=order.symbol,
644
+ type=order.type,
645
+ oid=order.oid,
646
+ side=order.side,
647
+ amount=order.amount,
648
+ price=float(order.price) if order.price else None,
649
+ time_in_force=order.time_in_force,
650
+ status=OrderStatus.FAILED,
651
+ filled=Decimal(0),
652
+ remaining=order.amount,
653
+ reduce_only=order.reduce_only,
654
+ reason=f"code={res_ext.code}, msg={res_ext.msg}",
655
+ )
656
+ self._log.error(
657
+ f"Failed to place order for {order.symbol}: {res_ext.msg} code: {res_ext.code} {order.oid}"
658
+ )
659
+ self.order_status_update(res_batch_order)
660
+ except Exception as e:
661
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
662
+ self._log.error(f"Error creating batch orders: {error_msg}")
663
+ for order in orders:
664
+ res_batch_order = Order(
665
+ exchange=self._exchange_id,
666
+ timestamp=self._clock.timestamp_ms(),
667
+ symbol=order.symbol,
668
+ oid=order.oid,
669
+ type=order.type,
670
+ side=order.side,
671
+ amount=order.amount,
672
+ price=float(order.price) if order.price else None,
673
+ time_in_force=order.time_in_force,
674
+ status=OrderStatus.FAILED,
675
+ filled=Decimal(0),
676
+ remaining=order.amount,
677
+ reduce_only=order.reduce_only,
678
+ reason=error_msg,
679
+ )
680
+ self.order_status_update(res_batch_order)
681
+
682
+ async def cancel_batch_orders(self, orders: List[CancelOrderSubmit]):
683
+ from collections import defaultdict
684
+
685
+ groups = defaultdict(list)
686
+ for order in orders:
687
+ market = self._market.get(order.symbol)
688
+ if not market:
689
+ raise ValueError(f"Symbol {order.symbol} not found")
690
+ category = self._get_category(market)
691
+ groups[category].append((market, order))
692
+
693
+ for category, items in groups.items():
694
+ chunk_size = 10 if category == "spot" else 20
695
+ for i in range(0, len(items), chunk_size):
696
+ chunk = items[i : i + chunk_size]
697
+ request = [{"symbol": m.id, "orderLinkId": o.oid} for m, o in chunk]
698
+ try:
699
+ res = await self._api_client.post_v5_order_cancel_batch(
700
+ category=category, request=request
701
+ )
702
+ for (_, submit), res_order, res_ext in zip(
703
+ chunk, res.result.list, res.retExtInfo.list
704
+ ):
705
+ if res_ext.code == 0:
706
+ order = Order(
707
+ exchange=self._exchange_id,
708
+ symbol=submit.symbol,
709
+ oid=res_order.orderLinkId,
710
+ eid=res_order.orderId,
711
+ timestamp=res.time,
712
+ status=OrderStatus.CANCELING,
713
+ )
714
+ else:
715
+ order = Order(
716
+ exchange=self._exchange_id,
717
+ symbol=submit.symbol,
718
+ oid=submit.oid,
719
+ timestamp=self._clock.timestamp_ms(),
720
+ status=OrderStatus.CANCEL_FAILED,
721
+ reason=f"code={res_ext.code}, msg={res_ext.msg}",
722
+ )
723
+ self._log.error(
724
+ f"Batch cancel failed for {submit.symbol} oid={submit.oid}: {res_ext.msg}"
725
+ )
726
+ self.order_status_update(order)
727
+ except Exception as e:
728
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
729
+ self._log.error(f"Error canceling batch orders: {error_msg}")
730
+ for _, submit in chunk:
731
+ order = Order(
732
+ exchange=self._exchange_id,
733
+ symbol=submit.symbol,
734
+ oid=submit.oid,
735
+ timestamp=self._clock.timestamp_ms(),
736
+ status=OrderStatus.CANCEL_FAILED,
737
+ reason=error_msg,
738
+ )
739
+ self.order_status_update(order)
740
+
741
+ async def create_order(
742
+ self,
743
+ oid: str,
744
+ symbol: str,
745
+ side: OrderSide,
746
+ type: OrderType,
747
+ amount: Decimal,
748
+ price: Decimal | None = None,
749
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
750
+ reduce_only: bool = False,
751
+ **kwargs,
752
+ ):
753
+ market = self._market.get(symbol)
754
+ if not market:
755
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
756
+ id = market.id
757
+
758
+ category = self._get_category(market)
759
+ tif = time_in_force or TimeInForce.GTC
760
+
761
+ params: dict[str, object] = {
762
+ "category": category,
763
+ "symbol": id,
764
+ "side": BybitEnumParser.to_bybit_order_side(side).value,
765
+ "qty": amount,
766
+ "orderLinkId": oid,
767
+ }
768
+
769
+ if type.is_limit:
770
+ if not price:
771
+ raise ValueError("Price is required for limit order")
772
+ params["price"] = str(price)
773
+ params["order_type"] = BybitOrderType.LIMIT.value
774
+ params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(tif).value
775
+ elif type.is_post_only:
776
+ if not price:
777
+ raise ValueError("Price is required for post-only order")
778
+ params["order_type"] = BybitOrderType.LIMIT.value
779
+ params["price"] = str(price)
780
+ params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
781
+ elif type == OrderType.MARKET:
782
+ params["order_type"] = BybitOrderType.MARKET.value
783
+
784
+ # if position_side:
785
+ # params["positionIdx"] = BybitEnumParser.to_bybit_position_side(
786
+ # position_side
787
+ # ).value
788
+ if reduce_only:
789
+ params["reduceOnly"] = True
790
+ if market.spot:
791
+ params["marketUnit"] = "baseCoin"
792
+
793
+ params.update(kwargs)
794
+
795
+ try:
796
+ res = await self._api_client.post_v5_order_create(
797
+ category=category,
798
+ symbol=id,
799
+ side=BybitEnumParser.to_bybit_order_side(side).value,
800
+ order_type=str(params["order_type"]),
801
+ qty=amount,
802
+ **{
803
+ k: v
804
+ for k, v in params.items()
805
+ if k not in {"category", "symbol", "side", "order_type", "qty"}
806
+ },
807
+ )
808
+ order = Order(
809
+ exchange=self._exchange_id,
810
+ eid=res.result.orderId,
811
+ oid=oid,
812
+ timestamp=int(res.time),
813
+ symbol=symbol,
814
+ type=type,
815
+ side=side,
816
+ amount=amount,
817
+ price=float(price) if price else None,
818
+ time_in_force=time_in_force,
819
+ # position_side=position_side,
820
+ status=OrderStatus.PENDING,
821
+ filled=Decimal(0),
822
+ remaining=amount,
823
+ reduce_only=reduce_only,
824
+ )
825
+ except Exception as e:
826
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
827
+ self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
828
+ order = Order(
829
+ exchange=self._exchange_id,
830
+ oid=oid,
831
+ timestamp=self._clock.timestamp_ms(),
832
+ symbol=symbol,
833
+ type=type,
834
+ side=side,
835
+ amount=amount,
836
+ price=float(price) if price else None,
837
+ time_in_force=time_in_force,
838
+ # position_side=position_side,
839
+ status=OrderStatus.FAILED,
840
+ filled=Decimal(0),
841
+ remaining=amount,
842
+ reduce_only=reduce_only,
843
+ reason=error_msg,
844
+ )
845
+ self.order_status_update(order)
846
+
847
+ async def create_order_ws(
848
+ self,
849
+ oid: str,
850
+ symbol: str,
851
+ side: OrderSide,
852
+ type: OrderType,
853
+ amount: Decimal,
854
+ price: Decimal | None = None,
855
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
856
+ reduce_only: bool = False,
857
+ **kwargs,
858
+ ) -> Order:
859
+ self._registry.register_tmp_order(
860
+ order=Order(
861
+ oid=oid,
862
+ exchange=self._exchange_id,
863
+ symbol=symbol,
864
+ status=OrderStatus.INITIALIZED,
865
+ amount=amount,
866
+ type=type,
867
+ side=side,
868
+ price=float(price) if price else None,
869
+ time_in_force=time_in_force,
870
+ reduce_only=reduce_only,
871
+ timestamp=self._clock.timestamp_ms(),
872
+ )
873
+ )
874
+ market = self._market.get(symbol)
875
+ if not market:
876
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
877
+ id = market.id
878
+
879
+ category = self._get_category(market)
880
+
881
+ tif = time_in_force or TimeInForce.GTC
882
+ params: dict[str, object] = {
883
+ "category": category,
884
+ "symbol": id,
885
+ "side": BybitEnumParser.to_bybit_order_side(side).value,
886
+ "qty": str(amount),
887
+ "orderLinkId": oid,
888
+ }
889
+
890
+ if type.is_limit:
891
+ if not price:
892
+ raise ValueError("Price is required for limit order")
893
+ params["price"] = str(price)
894
+ params["orderType"] = BybitOrderType.LIMIT.value
895
+ params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(tif).value
896
+ elif type.is_post_only:
897
+ if not price:
898
+ raise ValueError("Price is required for post-only order")
899
+ params["orderType"] = BybitOrderType.LIMIT.value
900
+ params["price"] = str(price)
901
+ params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
902
+ elif type == OrderType.MARKET:
903
+ params["orderType"] = BybitOrderType.MARKET.value
904
+
905
+ # if position_side:
906
+ # params["positionIdx"] = BybitEnumParser.to_bybit_position_side(
907
+ # position_side
908
+ # ).value
909
+ if reduce_only:
910
+ params["reduceOnly"] = True
911
+ if market.spot:
912
+ params["marketUnit"] = "baseCoin"
913
+
914
+ params.update(kwargs)
915
+
916
+ try:
917
+ await self._ws_api_client.create_order(
918
+ id=oid,
919
+ category=category,
920
+ symbol=id,
921
+ side=BybitEnumParser.to_bybit_order_side(side).value,
922
+ orderType=str(params["orderType"]),
923
+ qty=str(amount),
924
+ **{
925
+ k: v
926
+ for k, v in params.items()
927
+ if k not in {"category", "symbol", "side", "orderType", "qty"}
928
+ },
929
+ )
930
+ except BybitRateLimitError as e:
931
+ order = self._rate_limit_failed_order(
932
+ oid=oid,
933
+ symbol=symbol,
934
+ side=side,
935
+ type=type,
936
+ amount=amount,
937
+ price=price,
938
+ time_in_force=time_in_force,
939
+ reduce_only=reduce_only,
940
+ exc=e,
941
+ )
942
+ self.order_status_update(order)
943
+ return order
944
+ tmp_order = self._registry.get_tmp_order(oid)
945
+ if tmp_order is None:
946
+ raise ValueError(f"Temporary order {oid} was not registered")
947
+ return tmp_order
948
+
949
+ def _parse_rest_order(
950
+ self, data: BybitOrder, symbol: str, fallback_oid: str
951
+ ) -> Order:
952
+ time_in_force = BybitEnumParser.parse_time_in_force(data.timeInForce)
953
+ order_type = BybitEnumParser.parse_order_type(data.orderType, time_in_force)
954
+
955
+ position_idx = (
956
+ data.positionIdx
957
+ if isinstance(data.positionIdx, BybitPositionIdx)
958
+ else BybitPositionIdx(data.positionIdx)
959
+ )
960
+
961
+ return Order(
962
+ exchange=self._exchange_id,
963
+ symbol=symbol,
964
+ status=BybitEnumParser.parse_order_status(data.orderStatus),
965
+ eid=data.orderId,
966
+ oid=data.orderLinkId or fallback_oid,
967
+ timestamp=int(data.updatedTime),
968
+ type=order_type,
969
+ side=BybitEnumParser.parse_order_side(data.side),
970
+ time_in_force=time_in_force,
971
+ price=float(data.price),
972
+ average=float(data.avgPrice) if data.avgPrice else None,
973
+ amount=Decimal(data.qty),
974
+ filled=Decimal(data.cumExecQty),
975
+ remaining=Decimal(data.qty) - Decimal(data.cumExecQty),
976
+ fee=Decimal(data.cumExecFee or "0"),
977
+ fee_currency=getattr(data, "feeCurrency", None),
978
+ cum_cost=Decimal(data.cumExecValue or "0"),
979
+ reduce_only=data.reduceOnly,
980
+ position_side=BybitEnumParser.parse_position_side(position_idx),
981
+ )
982
+
983
+ async def query_order(self, oid: str, symbol: str) -> Order | None:
984
+ market = self._market.get(symbol)
985
+ if not market:
986
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
987
+
988
+ category = self._get_category(market)
989
+ params = {
990
+ "symbol": market.id,
991
+ "orderLinkId": oid,
992
+ }
993
+
994
+ try:
995
+ realtime = await self._api_client.get_v5_order_realtime(
996
+ category=category,
997
+ **params,
998
+ )
999
+ if realtime.retCode != 0:
1000
+ self._log.error(
1001
+ f"Error querying realtime order: {realtime.retMsg} params: {params}"
1002
+ )
1003
+ return None
1004
+ if realtime.result.list:
1005
+ return self._parse_rest_order(
1006
+ realtime.result.list[0],
1007
+ symbol,
1008
+ fallback_oid=oid,
1009
+ )
1010
+
1011
+ history = await self._api_client.get_v5_order_history(
1012
+ category=category,
1013
+ **params,
1014
+ )
1015
+ if history.retCode != 0:
1016
+ self._log.error(
1017
+ f"Error querying historical order: {history.retMsg} params: {params}"
1018
+ )
1019
+ return None
1020
+ if history.result.list:
1021
+ return self._parse_rest_order(
1022
+ history.result.list[0],
1023
+ symbol,
1024
+ fallback_oid=oid,
1025
+ )
1026
+ return None
1027
+ except Exception as e:
1028
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1029
+ self._log.error(f"Error querying order: {error_msg} params: {params}")
1030
+ return None
1031
+
1032
+ async def cancel_all_orders(self, symbol: str) -> bool:
1033
+ try:
1034
+ market = self._market.get(symbol)
1035
+ if not market:
1036
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1037
+ symbol = market.id
1038
+
1039
+ category = self._get_category(market)
1040
+
1041
+ params = {
1042
+ "category": category,
1043
+ "symbol": symbol,
1044
+ }
1045
+
1046
+ await self._api_client.post_v5_order_cancel_all(**params)
1047
+ return True
1048
+ except Exception as e:
1049
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1050
+ self._log.error(
1051
+ f"Error canceling all orders: {error_msg} params: {str(params)}"
1052
+ )
1053
+ return False
1054
+
1055
+ async def modify_order(
1056
+ self,
1057
+ oid: str,
1058
+ symbol: str,
1059
+ side: OrderSide | None = None,
1060
+ price: Decimal | None = None,
1061
+ amount: Decimal | None = None,
1062
+ **kwargs,
1063
+ ):
1064
+ # NOTE: side is not supported for modify order
1065
+ market = self._market.get(symbol)
1066
+ if not market:
1067
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1068
+ id = market.id
1069
+
1070
+ category = self._get_category(market)
1071
+ amend_kwargs: dict[str, str | None] = {
1072
+ "orderLinkId": oid,
1073
+ "price": str(price) if price else None,
1074
+ "qty": str(amount) if amount else None,
1075
+ **kwargs,
1076
+ }
1077
+
1078
+ try:
1079
+ res = await self._api_client.post_v5_order_amend(
1080
+ category=category,
1081
+ symbol=id,
1082
+ **amend_kwargs,
1083
+ )
1084
+ order = Order(
1085
+ exchange=self._exchange_id,
1086
+ eid=res.result.orderId,
1087
+ oid=oid,
1088
+ timestamp=int(res.time),
1089
+ symbol=symbol,
1090
+ status=OrderStatus.PENDING,
1091
+ filled=Decimal(0),
1092
+ price=float(price) if price else None,
1093
+ remaining=amount,
1094
+ )
1095
+ return order
1096
+ except Exception as e:
1097
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1098
+ self._log.error(
1099
+ f"Error modifying order: {error_msg} params: {str(amend_kwargs)}"
1100
+ )
1101
+ order = Order(
1102
+ exchange=self._exchange_id,
1103
+ timestamp=self._clock.timestamp_ms(),
1104
+ symbol=symbol,
1105
+ oid=oid,
1106
+ status=OrderStatus.FAILED,
1107
+ filled=Decimal(0),
1108
+ remaining=amount,
1109
+ price=float(price) if price else None,
1110
+ reason=error_msg,
1111
+ )
1112
+ return order
1113
+
1114
+ def _parse_order_update(self, raw: bytes):
1115
+ order_msg = self._ws_msg_order_update_decoder.decode(raw)
1116
+ self._log.debug(f"Order update: {str(order_msg)}")
1117
+ for data in order_msg.data:
1118
+ category = data.category
1119
+ if category.is_spot:
1120
+ market_id = data.symbol + "_spot"
1121
+ elif category.is_linear:
1122
+ market_id = data.symbol + "_linear"
1123
+ elif category.is_inverse:
1124
+ market_id = data.symbol + "_inverse"
1125
+ else:
1126
+ raise ValueError(f"Unsupported order category: {category}")
1127
+ symbol = self._market_id[market_id]
1128
+ time_in_force = BybitEnumParser.parse_time_in_force(data.timeInForce)
1129
+
1130
+ order = Order(
1131
+ exchange=self._exchange_id,
1132
+ symbol=symbol,
1133
+ status=BybitEnumParser.parse_order_status(data.orderStatus),
1134
+ eid=data.orderId,
1135
+ oid=data.orderLinkId,
1136
+ timestamp=int(data.updatedTime),
1137
+ type=BybitEnumParser.parse_order_type(data.orderType, time_in_force),
1138
+ side=BybitEnumParser.parse_order_side(data.side),
1139
+ time_in_force=time_in_force,
1140
+ price=float(data.price),
1141
+ average=float(data.avgPrice) if data.avgPrice else None,
1142
+ amount=Decimal(data.qty),
1143
+ filled=Decimal(data.cumExecQty),
1144
+ remaining=Decimal(data.qty) - Decimal(data.cumExecQty),
1145
+ fee=Decimal(data.cumExecFee),
1146
+ fee_currency=data.feeCurrency,
1147
+ cum_cost=Decimal(data.cumExecValue),
1148
+ reduce_only=data.reduceOnly,
1149
+ position_side=BybitEnumParser.parse_position_side(data.positionIdx),
1150
+ )
1151
+
1152
+ self.order_status_update(order)
1153
+
1154
+ def _parse_position_update(self, raw: bytes):
1155
+ position_msg = self._ws_msg_position_decoder.decode(raw)
1156
+ self._log.debug(f"Position update: {str(position_msg)}")
1157
+
1158
+ for data in position_msg.data:
1159
+ category = data.category
1160
+ if category.is_linear: # only linear/inverse/ position is supported
1161
+ market_id = data.symbol + "_linear"
1162
+ elif category.is_inverse:
1163
+ market_id = data.symbol + "_inverse"
1164
+ else:
1165
+ raise ValueError(f"Unsupported position category: {category}")
1166
+ symbol = self._market_id[market_id]
1167
+
1168
+ side = data.side.parse_to_position_side()
1169
+ if side == PositionSide.LONG:
1170
+ signed_amount = Decimal(data.size)
1171
+ elif side == PositionSide.SHORT:
1172
+ signed_amount = -Decimal(data.size)
1173
+ else:
1174
+ side = None
1175
+ signed_amount = Decimal(0)
1176
+
1177
+ position = Position(
1178
+ symbol=symbol,
1179
+ exchange=self._exchange_id,
1180
+ side=side,
1181
+ signed_amount=signed_amount,
1182
+ entry_price=float(data.entryPrice),
1183
+ unrealized_pnl=float(data.unrealisedPnl),
1184
+ realized_pnl=float(data.cumRealisedPnl),
1185
+ )
1186
+
1187
+ self._cache._apply_position(position)
1188
+
1189
+ def _parse_wallet_update(self, raw: bytes):
1190
+ wallet_msg = self._ws_msg_wallet_decoder.decode(raw)
1191
+ self._log.debug(f"Wallet update: {str(wallet_msg)}")
1192
+
1193
+ for data in wallet_msg.data:
1194
+ balances = data.parse_to_balances()
1195
+ self._cache._apply_balance(self._account_type, balances)