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,1619 @@
1
+ import msgspec
2
+ from typing import Any, Dict, List, cast
3
+ from decimal import Decimal
4
+ from typing import Literal
5
+ from decimal import ROUND_HALF_UP, ROUND_CEILING, ROUND_FLOOR
6
+ from walrasquant.error import PositionModeError
7
+ from walrasquant.config import OrderQueryConfig
8
+ from walrasquant.exchange.bitget.error import BitgetRateLimitError, BitgetError
9
+ from walrasquant.core.nautilius_core import LiveClock, MessageBus
10
+ from walrasquant.core.cache import AsyncCache
11
+ from walrasquant.base import OrderManagementSystem
12
+ from walrasquant.core.entity import TaskManager
13
+ from walrasquant.core.registry import OrderRegistry
14
+ from walrasquant.schema import (
15
+ Order,
16
+ Position,
17
+ BatchOrderSubmit,
18
+ BaseMarket,
19
+ )
20
+ from walrasquant.constants import (
21
+ ExchangeType,
22
+ OrderSide,
23
+ OrderStatus,
24
+ OrderType,
25
+ TimeInForce,
26
+ TriggerType,
27
+ )
28
+ from walrasquant.exchange.bitget.schema import (
29
+ BitgetMarket,
30
+ BitgetWsGeneralMsg,
31
+ BitgetWsUtaGeneralMsg,
32
+ BitgetWsArgMsg,
33
+ BitgetOrderWsMsg,
34
+ BitgetPositionWsMsg,
35
+ BitgetUtaOrderWsMsg,
36
+ BitgetUtaPositionWsMsg,
37
+ BitgetSpotAccountWsMsg,
38
+ BitgetFuturesAccountWsMsg,
39
+ BitgetUtaAccountWsMsg,
40
+ BitgetWsApiGeneralMsg,
41
+ BitgetWsApiArgMsg,
42
+ BitgetWsApiUtaGeneralMsg,
43
+ )
44
+ from walrasquant.exchange.bitget.rest_api import BitgetApiClient
45
+ from walrasquant.exchange.bitget.websockets import BitgetWSClient, BitgetWSApiClient
46
+ from walrasquant.exchange.bitget.constants import (
47
+ BitgetAccountType,
48
+ BitgetInstType,
49
+ BitgetUtaInstType,
50
+ BitgetEnumParser,
51
+ BitgetOrderSide,
52
+ BitgetOrderStatus,
53
+ BitgetOrderType,
54
+ BitgetPositionSide,
55
+ BitgetTimeInForce,
56
+ )
57
+
58
+
59
+ class BitgetOrderManagementSystem(OrderManagementSystem):
60
+ _inst_type_map = {
61
+ BitgetInstType.SPOT: "spot",
62
+ BitgetInstType.USDC_FUTURES: "linear",
63
+ BitgetInstType.USDT_FUTURES: "linear",
64
+ BitgetInstType.COIN_FUTURES: "inverse",
65
+ }
66
+ _uta_inst_type_map = {
67
+ BitgetUtaInstType.SPOT: "spot",
68
+ BitgetUtaInstType.USDC_FUTURES: "linear",
69
+ BitgetUtaInstType.USDT_FUTURES: "linear",
70
+ BitgetUtaInstType.COIN_FUTURES: "inverse",
71
+ }
72
+ _ws_client: BitgetWSClient
73
+ _account_type: BitgetAccountType
74
+ _market: Dict[str, BitgetMarket]
75
+ _market_id: Dict[str, str]
76
+ _api_client: BitgetApiClient
77
+
78
+ def __init__(
79
+ self,
80
+ account_type: BitgetAccountType,
81
+ api_key: str,
82
+ secret: str,
83
+ passphrase: str,
84
+ market: Dict[str, BitgetMarket],
85
+ market_id: Dict[str, str],
86
+ registry: OrderRegistry,
87
+ cache: AsyncCache,
88
+ api_client: BitgetApiClient,
89
+ exchange_id: ExchangeType,
90
+ clock: LiveClock,
91
+ msgbus: MessageBus,
92
+ task_manager: TaskManager,
93
+ max_slippage: float,
94
+ enable_rate_limit: bool,
95
+ order_query_config: OrderQueryConfig,
96
+ max_subscriptions_per_client: int | None = None,
97
+ max_clients: int | None = None,
98
+ ):
99
+ super().__init__(
100
+ account_type=account_type,
101
+ market=cast(dict[str, BaseMarket], market),
102
+ market_id=market_id,
103
+ registry=registry,
104
+ cache=cache,
105
+ api_client=api_client,
106
+ ws_client=BitgetWSClient(
107
+ account_type=account_type,
108
+ handler=self._ws_uta_msg_handler
109
+ if account_type.is_uta
110
+ else self._ws_msg_handler,
111
+ clock=clock,
112
+ task_manager=task_manager,
113
+ api_key=api_key,
114
+ secret=secret,
115
+ passphrase=passphrase,
116
+ max_subscriptions_per_client=max_subscriptions_per_client,
117
+ max_clients=max_clients,
118
+ ),
119
+ exchange_id=exchange_id,
120
+ clock=clock,
121
+ msgbus=msgbus,
122
+ task_manager=task_manager,
123
+ order_query_config=order_query_config,
124
+ )
125
+
126
+ self._ws_api_client = BitgetWSApiClient(
127
+ account_type=account_type,
128
+ api_key=api_key,
129
+ secret=secret,
130
+ passphrase=passphrase,
131
+ handler=self._ws_uta_api_msg_handler
132
+ if account_type.is_uta
133
+ else self._ws_api_msg_handler,
134
+ task_manager=task_manager,
135
+ clock=clock,
136
+ enable_rate_limit=enable_rate_limit,
137
+ )
138
+
139
+ self._max_slippage = max_slippage
140
+ self._ws_msg_general_decoder = msgspec.json.Decoder(BitgetWsGeneralMsg)
141
+ self._ws_msg_uta_general_decoder = msgspec.json.Decoder(BitgetWsUtaGeneralMsg)
142
+ self._ws_msg_orders_decoder = msgspec.json.Decoder(BitgetOrderWsMsg)
143
+ self._ws_msg_uta_orders_decoder = msgspec.json.Decoder(BitgetUtaOrderWsMsg)
144
+ self._ws_msg_positions_decoder = msgspec.json.Decoder(BitgetPositionWsMsg)
145
+ self._ws_msg_uta_positions_decoder = msgspec.json.Decoder(
146
+ BitgetUtaPositionWsMsg
147
+ )
148
+
149
+ self._ws_msg_spot_account_decoder = msgspec.json.Decoder(BitgetSpotAccountWsMsg)
150
+ self._ws_msg_futures_account_decoder = msgspec.json.Decoder(
151
+ BitgetFuturesAccountWsMsg
152
+ )
153
+ self._ws_msg_uta_account_decoder = msgspec.json.Decoder(BitgetUtaAccountWsMsg)
154
+
155
+ self._ws_api_general_decoder = msgspec.json.Decoder(BitgetWsApiGeneralMsg)
156
+ self._ws_api_uta_general_decoder = msgspec.json.Decoder(
157
+ BitgetWsApiUtaGeneralMsg
158
+ )
159
+
160
+ def _supports_query_market(self, market: BitgetMarket) -> bool:
161
+ if self._account_type.is_uta:
162
+ return market.spot or market.swap
163
+ if self._account_type.is_spot:
164
+ return market.spot
165
+ if self._account_type.is_future:
166
+ return market.swap
167
+ return False
168
+
169
+ def _ws_uta_api_msg_handler(self, raw: bytes):
170
+ # if raw == b"pong":
171
+ # self._ws_api_client._transport.notify_user_specific_pong_received()
172
+ # self._log.debug(f"Pong received: `{raw.decode()}`")
173
+ # return
174
+ try:
175
+ ws_msg = self._ws_api_uta_general_decoder.decode(raw)
176
+
177
+ if ws_msg.is_id_msg:
178
+ self._handle_id_messages(ws_msg)
179
+ elif ws_msg.is_error_msg:
180
+ self._log.error(f"login Error: {ws_msg.error_msg}")
181
+
182
+ except msgspec.DecodeError as e:
183
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
184
+
185
+ def _ws_api_msg_handler(self, raw: bytes):
186
+ # if raw == b"pong":
187
+ # self._ws_api_client._transport.notify_user_specific_pong_received()
188
+ # self._log.debug(f"Pong received: `{raw.decode()}`")
189
+ # return
190
+ try:
191
+ ws_msg = self._ws_api_general_decoder.decode(raw)
192
+
193
+ if ws_msg.is_arg_msg:
194
+ self._handle_arg_messages(ws_msg)
195
+ elif ws_msg.is_error_msg:
196
+ self._log.error(f"login Error: {ws_msg.error_msg}")
197
+
198
+ except msgspec.DecodeError as e:
199
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
200
+
201
+ def _handle_id_messages(self, ws_msg: BitgetWsApiUtaGeneralMsg):
202
+ """Handle argument messages for place and cancel orders"""
203
+ if ws_msg.id and ws_msg.id.startswith("n"):
204
+ self._handle_uta_place_order_response(ws_msg)
205
+ else:
206
+ self._handle_uta_cancel_order_response(ws_msg)
207
+
208
+ def _handle_arg_messages(self, ws_msg: BitgetWsApiGeneralMsg):
209
+ """Handle argument messages for place and cancel orders"""
210
+ for arg_msg in ws_msg.arg or []:
211
+ if arg_msg.is_place_order:
212
+ self._handle_place_order_response(ws_msg, arg_msg)
213
+ elif arg_msg.is_cancel_order:
214
+ self._handle_cancel_order_response(ws_msg, arg_msg)
215
+
216
+ def _handle_uta_place_order_response(self, ws_msg: BitgetWsApiUtaGeneralMsg):
217
+ oid = ws_msg.oid
218
+ tmp_order = self._registry.get_tmp_order(oid)
219
+ if not tmp_order:
220
+ return
221
+ ts = self._clock.timestamp_ms()
222
+
223
+ if ws_msg.is_success:
224
+ for arg_msg in ws_msg.args or []:
225
+ ordId = arg_msg.orderId
226
+ self._log.debug(
227
+ f"[{tmp_order.symbol}] placing order success: oid: {oid} id: {ordId}"
228
+ )
229
+ order = self._create_order_from_tmp(
230
+ tmp_order, oid, ordId, OrderStatus.PENDING, ts
231
+ )
232
+ self.order_status_update(order) # INITIALIZED -> PENDING
233
+ else:
234
+ self._log.error(
235
+ f"[{tmp_order.symbol}] new order failed: oid: {oid} {ws_msg.error_msg}"
236
+ )
237
+ order = self._create_order_from_tmp(
238
+ tmp_order,
239
+ oid,
240
+ None,
241
+ OrderStatus.FAILED,
242
+ ts,
243
+ reason=ws_msg.error_msg,
244
+ )
245
+ self.order_status_update(order)
246
+
247
+ def _handle_uta_cancel_order_response(self, ws_msg: BitgetWsApiUtaGeneralMsg):
248
+ """Handle cancel order response"""
249
+ oid = ws_msg.oid
250
+ tmp_order = self._registry.get_tmp_order(oid)
251
+ if not tmp_order:
252
+ return
253
+ ts = self._clock.timestamp_ms()
254
+
255
+ if ws_msg.is_success:
256
+ for arg_msg in ws_msg.args or []:
257
+ ordId = arg_msg.orderId
258
+ self._log.debug(
259
+ f"[{tmp_order.symbol}] canceling order success: oid: {oid} id: {ordId}"
260
+ )
261
+ order = self._create_order_from_tmp(
262
+ tmp_order, oid, ordId, OrderStatus.CANCELING, ts
263
+ )
264
+ self.order_status_update(order) # SOME STATUS -> CANCELING
265
+ else:
266
+ self._log.error(
267
+ f"[{tmp_order.symbol}] canceling order failed: oid: {oid} {ws_msg.error_msg}"
268
+ )
269
+ order = self._create_order_from_tmp(
270
+ tmp_order,
271
+ oid,
272
+ None,
273
+ OrderStatus.CANCEL_FAILED,
274
+ ts,
275
+ reason=ws_msg.error_msg,
276
+ )
277
+ self.order_status_update(order)
278
+
279
+ def _handle_place_order_response(
280
+ self, ws_msg: BitgetWsApiGeneralMsg, arg_msg: BitgetWsApiArgMsg
281
+ ):
282
+ """Handle place order response"""
283
+ oid = arg_msg.id
284
+ tmp_order = self._registry.get_tmp_order(oid)
285
+ if not tmp_order:
286
+ return
287
+ ts = self._clock.timestamp_ms()
288
+
289
+ if ws_msg.is_success:
290
+ ordId = arg_msg.params.orderId
291
+ self._log.debug(
292
+ f"[{tmp_order.symbol}] placing order success: oid: {oid} id: {ordId}"
293
+ )
294
+ order = self._create_order_from_tmp(
295
+ tmp_order, oid, ordId, OrderStatus.PENDING, ts
296
+ )
297
+ self.order_status_update(order) # INITIALIZED -> PENDING
298
+ else:
299
+ self._log.error(
300
+ f"[{tmp_order.symbol}] new order failed: oid: {oid} {ws_msg.error_msg}"
301
+ )
302
+ order = self._create_order_from_tmp(
303
+ tmp_order,
304
+ oid,
305
+ None,
306
+ OrderStatus.FAILED,
307
+ ts,
308
+ reason=ws_msg.error_msg,
309
+ )
310
+ self.order_status_update(order) # INITIALIZED -> FAILED
311
+
312
+ def _handle_cancel_order_response(
313
+ self, ws_msg: BitgetWsApiGeneralMsg, arg_msg: BitgetWsApiArgMsg
314
+ ):
315
+ """Handle cancel order response"""
316
+ oid = arg_msg.id
317
+ tmp_order = self._registry.get_tmp_order(oid)
318
+ if not tmp_order:
319
+ return
320
+ ts = self._clock.timestamp_ms()
321
+
322
+ if ws_msg.is_success:
323
+ ordId = arg_msg.params.orderId
324
+ self._log.debug(
325
+ f"[{tmp_order.symbol}] canceling order success: oid: {oid} id: {ordId}"
326
+ )
327
+ order = self._create_order_from_tmp(
328
+ tmp_order, oid, ordId, OrderStatus.CANCELING, ts
329
+ )
330
+ self.order_status_update(order)
331
+ else:
332
+ self._log.error(
333
+ f"[{tmp_order.symbol}] canceling order failed: oid: {oid} {ws_msg.error_msg}"
334
+ )
335
+ order = self._create_order_from_tmp(
336
+ tmp_order,
337
+ oid,
338
+ None,
339
+ OrderStatus.CANCEL_FAILED,
340
+ ts,
341
+ reason=ws_msg.error_msg,
342
+ )
343
+ self.order_status_update(order)
344
+
345
+ def _create_order_from_tmp(
346
+ self,
347
+ tmp_order: Order,
348
+ oid: str,
349
+ order_id: str | None,
350
+ status: OrderStatus,
351
+ timestamp: int,
352
+ reason: str | None = None,
353
+ ) -> Order:
354
+ """Create Order object from temporary order data"""
355
+ return Order(
356
+ exchange=self._exchange_id,
357
+ symbol=tmp_order.symbol,
358
+ oid=oid,
359
+ eid=order_id,
360
+ side=tmp_order.side,
361
+ status=status,
362
+ amount=tmp_order.amount,
363
+ timestamp=timestamp,
364
+ type=tmp_order.type,
365
+ price=tmp_order.price,
366
+ time_in_force=tmp_order.time_in_force,
367
+ reduce_only=tmp_order.reduce_only,
368
+ reason=reason,
369
+ )
370
+
371
+ def _inst_type_suffix(self, inst_type: BitgetInstType):
372
+ return self._inst_type_map[inst_type]
373
+
374
+ def _uta_inst_type_suffix(self, category: BitgetUtaInstType) -> str:
375
+ """Convert UTA category to internal suffix"""
376
+ return self._uta_inst_type_map[category]
377
+
378
+ def _get_inst_type(self, market: BitgetMarket):
379
+ if market.spot:
380
+ return "SPOT"
381
+ elif market.linear:
382
+ return "USDT-FUTURES" if market.quote == "USDT" else "USDC-FUTURES"
383
+ elif market.inverse:
384
+ return "COIN-FUTURES"
385
+
386
+ async def _init_account_balance(self):
387
+ """Initialize account balance"""
388
+ pass
389
+
390
+ async def _init_position(self):
391
+ """Initialize position"""
392
+ # Bitget has different product types for different instrument types
393
+
394
+ if self._account_type.is_uta:
395
+ # For UTA accounts, use the v3 position API
396
+ categories = ["USDT-FUTURES", "USDC-FUTURES", "COIN-FUTURES"]
397
+
398
+ for category in categories:
399
+ response = await self._api_client.get_api_v3_position_current_position(
400
+ category=category
401
+ )
402
+
403
+ if not response.data.list:
404
+ continue
405
+
406
+ for pos_data in response.data.list:
407
+ # Skip positions with zero total
408
+ if float(pos_data.total) == 0:
409
+ continue
410
+
411
+ # Check position mode - only support one-way mode for UTA
412
+ if pos_data.holdMode != "one_way_mode":
413
+ raise PositionModeError(
414
+ f"Only one-way mode is supported for UTA accounts. Current mode: {pos_data.holdMode}"
415
+ )
416
+
417
+ # Determine suffix based on category
418
+ if category == "USDT-FUTURES":
419
+ inst_type_suffix = "linear"
420
+ elif category == "USDC-FUTURES":
421
+ inst_type_suffix = "linear"
422
+ elif category == "COIN-FUTURES":
423
+ inst_type_suffix = "inverse"
424
+
425
+ # Map symbol from Bitget format to our internal format
426
+ symbol = self._market_id.get(
427
+ f"{pos_data.symbol}_{inst_type_suffix}"
428
+ )
429
+
430
+ if not symbol:
431
+ self._log.warning(
432
+ f"Symbol {pos_data.symbol} not found in market mapping"
433
+ )
434
+ continue
435
+
436
+ # Convert position side string to BitgetPositionSide enum
437
+ hold_side = BitgetPositionSide(pos_data.posSide)
438
+
439
+ # Convert to signed amount based on position side
440
+ signed_amount = Decimal(pos_data.total)
441
+ if hold_side == BitgetPositionSide.SHORT:
442
+ signed_amount = -signed_amount
443
+
444
+ # Parse position side
445
+ position_side = hold_side.parse_to_position_side()
446
+
447
+ # Create Position object
448
+ position = Position(
449
+ symbol=symbol,
450
+ exchange=self._exchange_id,
451
+ signed_amount=signed_amount,
452
+ entry_price=float(pos_data.avgPrice),
453
+ side=position_side,
454
+ unrealized_pnl=float(pos_data.unrealisedPnl),
455
+ realized_pnl=float(pos_data.curRealisedPnl),
456
+ )
457
+
458
+ # Apply position to cache
459
+ self._cache._apply_position(position)
460
+ self._log.debug(f"Initialized UTA position: {str(position)}")
461
+
462
+ else:
463
+ product_types = ["USDT-FUTURES", "USDC-FUTURES", "COIN-FUTURES"]
464
+
465
+ for product_type in product_types:
466
+ # Get all positions for this product type
467
+ response = await self._api_client.get_api_v2_mix_position_all_position(
468
+ productType=product_type
469
+ )
470
+
471
+ for pos_data in response.data:
472
+ # Check position mode - only support one-way mode
473
+ if pos_data.posMode != "one_way_mode":
474
+ raise PositionModeError(
475
+ f"Only one-way position mode is supported. Current mode: {pos_data.posMode}"
476
+ )
477
+
478
+ # Skip positions with zero amount
479
+ if float(pos_data.total) == 0:
480
+ continue
481
+
482
+ # Map symbol from Bitget format to our internal format
483
+ inst_type = BitgetInstType(product_type)
484
+ inst_type_suffix = self._inst_type_suffix(inst_type)
485
+ symbol = self._market_id.get(
486
+ f"{pos_data.symbol}_{inst_type_suffix}"
487
+ )
488
+
489
+ if not symbol:
490
+ self._log.warning(
491
+ f"Symbol {pos_data.symbol} not found in market mapping"
492
+ )
493
+ continue
494
+
495
+ # Convert holdSide string to BitgetPositionSide enum
496
+ hold_side = BitgetPositionSide(pos_data.holdSide)
497
+
498
+ # Convert to signed amount based on position side
499
+ signed_amount = Decimal(pos_data.total)
500
+ if hold_side == BitgetPositionSide.SHORT:
501
+ signed_amount = -signed_amount
502
+
503
+ # Parse position side
504
+ position_side = hold_side.parse_to_position_side()
505
+
506
+ # Create Position object
507
+ position = Position(
508
+ symbol=symbol,
509
+ exchange=self._exchange_id,
510
+ signed_amount=signed_amount,
511
+ entry_price=float(pos_data.openPriceAvg),
512
+ side=position_side,
513
+ unrealized_pnl=float(pos_data.unrealizedPL),
514
+ realized_pnl=float(pos_data.achievedProfits or 0.0),
515
+ )
516
+
517
+ # Apply position to cache
518
+ self._cache._apply_position(position)
519
+ self._log.debug(f"Initialized position: {str(position)}")
520
+
521
+ async def _position_mode_check(self):
522
+ """Check the position mode"""
523
+ # Position mode check is performed in _init_position for each position
524
+ # Only one-way mode is supported
525
+ pass
526
+
527
+ async def create_tp_sl_order(
528
+ self,
529
+ oid: str,
530
+ symbol: str,
531
+ side: OrderSide,
532
+ type: OrderType,
533
+ amount: Decimal,
534
+ price: Decimal | None = None,
535
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
536
+ tp_order_type: OrderType | None = None,
537
+ tp_trigger_price: Decimal | None = None,
538
+ tp_price: Decimal | None = None,
539
+ tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
540
+ sl_order_type: OrderType | None = None,
541
+ sl_trigger_price: Decimal | None = None,
542
+ sl_price: Decimal | None = None,
543
+ sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
544
+ **kwargs,
545
+ ) -> Order:
546
+ """Create a take profit and stop loss order"""
547
+ raise NotImplementedError
548
+
549
+ def _price_to_precision(
550
+ self,
551
+ symbol: str,
552
+ price: float,
553
+ mode: Literal["round", "ceil", "floor"] = "round",
554
+ ) -> Decimal:
555
+ market = self._market[symbol]
556
+ if market.spot:
557
+ return super()._price_to_precision(symbol, price, mode)
558
+ else:
559
+ price: Decimal = Decimal(str(price))
560
+ price_multiplier = Decimal(market.info.priceEndStep or "1")
561
+ multiplier_count = price / price_multiplier
562
+
563
+ if mode == "round":
564
+ price = (
565
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
566
+ ) * price_multiplier
567
+ elif mode == "ceil":
568
+ price = (
569
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_CEILING)
570
+ ) * price_multiplier
571
+ elif mode == "floor":
572
+ price = (
573
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_FLOOR)
574
+ ) * price_multiplier
575
+ return price
576
+
577
+ async def create_order_ws(
578
+ self,
579
+ oid: str,
580
+ symbol: str,
581
+ side: OrderSide,
582
+ type: OrderType,
583
+ amount: Decimal,
584
+ price: Decimal | None,
585
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
586
+ reduce_only: bool = False,
587
+ **kwargs,
588
+ ) -> None:
589
+ """Create an order"""
590
+ self._registry.register_tmp_order(
591
+ order=Order(
592
+ oid=oid,
593
+ exchange=self._exchange_id,
594
+ symbol=symbol,
595
+ status=OrderStatus.INITIALIZED,
596
+ amount=amount,
597
+ type=type,
598
+ side=side,
599
+ price=float(price) if price else None,
600
+ time_in_force=time_in_force,
601
+ timestamp=self._clock.timestamp_ms(),
602
+ reduce_only=reduce_only,
603
+ )
604
+ )
605
+ market = self._market.get(symbol)
606
+ if not market:
607
+ raise ValueError(f"Symbol {symbol} not found in market data.")
608
+
609
+ if self._account_type.is_uta:
610
+ category: str = kwargs.pop("category", self._get_inst_type(market))
611
+ params = {
612
+ "category": category.lower(),
613
+ "symbol": market.id,
614
+ "side": BitgetEnumParser.to_bitget_order_side(side).value,
615
+ "qty": str(amount),
616
+ "clientOid": oid,
617
+ }
618
+ if type.is_limit:
619
+ if price is None or time_in_force is None:
620
+ raise ValueError(
621
+ "price and time_in_force are required for limit orders"
622
+ )
623
+ params["price"] = str(price)
624
+ params["timeInForce"] = BitgetEnumParser.to_bitget_time_in_force(
625
+ time_in_force
626
+ ).value
627
+ params["orderType"] = "limit"
628
+ elif type.is_post_only:
629
+ if price is None:
630
+ raise ValueError("price is required for post-only orders")
631
+ params["price"] = str(price)
632
+ params["timeInForce"] = "post_only"
633
+ params["orderType"] = "limit"
634
+ elif type.is_market:
635
+ bookl1 = self._cache.bookl1(symbol)
636
+ if not bookl1:
637
+ raise ValueError(
638
+ "Please Subscribe to bookl1 first, since market order requires bookl1 data"
639
+ )
640
+ if side.is_buy:
641
+ price = self._price_to_precision(
642
+ symbol, bookl1.ask * (1 + self._max_slippage), mode="ceil"
643
+ )
644
+ else:
645
+ price = self._price_to_precision(
646
+ symbol, bookl1.bid * (1 - self._max_slippage), mode="floor"
647
+ )
648
+ params["price"] = str(price)
649
+ params["timeInForce"] = BitgetEnumParser.to_bitget_time_in_force(
650
+ TimeInForce.IOC
651
+ ).value
652
+ params["orderType"] = "limit"
653
+
654
+ if reduce_only:
655
+ params["reduceOnly"] = "yes"
656
+
657
+ params.update(kwargs)
658
+
659
+ try:
660
+ await self._ws_api_client.uta_place_order(id=oid, **params)
661
+ except BitgetRateLimitError as e:
662
+ order = self._rate_limit_failed_order(
663
+ oid=oid,
664
+ symbol=symbol,
665
+ side=side,
666
+ type=type,
667
+ amount=amount,
668
+ price=price,
669
+ time_in_force=time_in_force,
670
+ reduce_only=reduce_only,
671
+ exc=e,
672
+ )
673
+ self.order_status_update(order)
674
+
675
+ else:
676
+ params = {
677
+ "instId": market.id,
678
+ "side": BitgetEnumParser.to_bitget_order_side(side).value,
679
+ "size": str(amount),
680
+ "clientOid": oid,
681
+ }
682
+ if type.is_limit:
683
+ if price is None or time_in_force is None:
684
+ raise ValueError(
685
+ "price and time_in_force are required for limit orders"
686
+ )
687
+ params["price"] = str(price)
688
+ params["force"] = BitgetEnumParser.to_bitget_time_in_force(
689
+ time_in_force
690
+ ).value
691
+ params["orderType"] = "limit"
692
+ elif type.is_post_only:
693
+ if price is None:
694
+ raise ValueError("price is required for post-only orders")
695
+ params["price"] = str(price)
696
+ params["force"] = "post_only"
697
+ params["orderType"] = "limit"
698
+ elif type.is_market:
699
+ bookl1 = self._cache.bookl1(symbol)
700
+ if not bookl1:
701
+ raise ValueError(
702
+ "Please Subscribe to bookl1 first, since market order requires bookl1 data"
703
+ )
704
+ if side.is_buy:
705
+ price = self._price_to_precision(
706
+ symbol, bookl1.ask * (1 + self._max_slippage), mode="ceil"
707
+ )
708
+ else:
709
+ price = self._price_to_precision(
710
+ symbol, bookl1.bid * (1 - self._max_slippage), mode="floor"
711
+ )
712
+ params["price"] = str(price)
713
+ params["force"] = BitgetEnumParser.to_bitget_time_in_force(
714
+ TimeInForce.IOC
715
+ ).value
716
+ params["orderType"] = "limit"
717
+
718
+ params.update(kwargs)
719
+
720
+ if market.swap:
721
+ params["marginCoin"] = market.quote
722
+ params["marginMode"] = kwargs.get("marginMode", "crossed")
723
+ params["instType"] = self._get_inst_type(market)
724
+ if reduce_only:
725
+ params["reduceOnly"] = "YES"
726
+ try:
727
+ await self._ws_api_client.future_place_order(id=oid, **params)
728
+ except BitgetRateLimitError as e:
729
+ order = self._rate_limit_failed_order(
730
+ oid=oid,
731
+ symbol=symbol,
732
+ side=side,
733
+ type=type,
734
+ amount=amount,
735
+ price=price,
736
+ time_in_force=time_in_force,
737
+ reduce_only=reduce_only,
738
+ exc=e,
739
+ )
740
+ self.order_status_update(order)
741
+ else:
742
+ try:
743
+ await self._ws_api_client.spot_place_order(id=oid, **params)
744
+ except BitgetRateLimitError as e:
745
+ order = self._rate_limit_failed_order(
746
+ oid=oid,
747
+ symbol=symbol,
748
+ side=side,
749
+ type=type,
750
+ amount=amount,
751
+ price=price,
752
+ time_in_force=time_in_force,
753
+ reduce_only=reduce_only,
754
+ exc=e,
755
+ )
756
+ self.order_status_update(order)
757
+
758
+ async def cancel_order_ws(self, oid: str, symbol: str, **kwargs) -> None:
759
+ market = self._market.get(symbol)
760
+ if not market:
761
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
762
+ instId = market.id
763
+ if self._account_type.is_uta:
764
+ await self._ws_api_client.uta_cancel_order(id=oid, clientOid=oid)
765
+ else:
766
+ if market.swap:
767
+ await self._ws_api_client.future_cancel_order(
768
+ id=oid,
769
+ instId=instId,
770
+ clientOid=oid,
771
+ instType=self._get_inst_type(market),
772
+ )
773
+ else:
774
+ await self._ws_api_client.spot_cancel_order(
775
+ id=oid, instId=instId, clientOid=oid
776
+ )
777
+
778
+ async def create_order(
779
+ self,
780
+ oid: str,
781
+ symbol: str,
782
+ side: OrderSide,
783
+ type: OrderType,
784
+ amount: Decimal,
785
+ price: Decimal | None,
786
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
787
+ reduce_only: bool = False,
788
+ **kwargs,
789
+ ) -> Order:
790
+ """Create an order"""
791
+ self._registry.register_tmp_order(
792
+ order=Order(
793
+ oid=oid,
794
+ exchange=self._exchange_id,
795
+ symbol=symbol,
796
+ status=OrderStatus.INITIALIZED,
797
+ amount=amount,
798
+ type=type,
799
+ side=side,
800
+ price=float(price) if price else None,
801
+ time_in_force=time_in_force,
802
+ timestamp=self._clock.timestamp_ms(),
803
+ reduce_only=reduce_only,
804
+ )
805
+ )
806
+
807
+ market = self._market.get(symbol)
808
+ if not market:
809
+ raise ValueError(f"Symbol {symbol} not found in market data.")
810
+
811
+ if self._account_type.is_uta:
812
+ category = kwargs.pop("category", self._get_inst_type(market))
813
+ params = {
814
+ "category": category,
815
+ "symbol": market.id,
816
+ "qty": str(amount),
817
+ "clientOid": oid,
818
+ }
819
+ if type.is_limit:
820
+ if price is None or time_in_force is None:
821
+ raise ValueError(
822
+ "price and time_in_force are required for limit orders"
823
+ )
824
+ params["price"] = str(price)
825
+ params["timeInForce"] = BitgetEnumParser.to_bitget_time_in_force(
826
+ time_in_force
827
+ ).value
828
+ params["orderType"] = "limit"
829
+ elif type.is_post_only:
830
+ if price is None:
831
+ raise ValueError("price is required for post-only orders")
832
+ params["price"] = str(price)
833
+ params["timeInForce"] = "post_only"
834
+ params["orderType"] = "limit"
835
+ elif type.is_market:
836
+ bookl1 = self._cache.bookl1(symbol)
837
+ if not bookl1:
838
+ raise ValueError(
839
+ "Please Subscribe to bookl1 first, since market order requires bookl1 data"
840
+ )
841
+ if side.is_buy:
842
+ price = self._price_to_precision(
843
+ symbol, bookl1.ask * (1 + self._max_slippage), mode="ceil"
844
+ )
845
+ else:
846
+ price = self._price_to_precision(
847
+ symbol, bookl1.bid * (1 - self._max_slippage), mode="floor"
848
+ )
849
+ params["price"] = str(price)
850
+ params["timeInForce"] = BitgetEnumParser.to_bitget_time_in_force(
851
+ TimeInForce.IOC
852
+ ).value
853
+ params["orderType"] = "limit"
854
+
855
+ if reduce_only:
856
+ params["reduceOnly"] = "yes"
857
+
858
+ params.update(kwargs)
859
+
860
+ try:
861
+ res = await self._api_client.post_api_v3_trade_place_order(**params)
862
+
863
+ order = Order(
864
+ exchange=self._exchange_id,
865
+ symbol=symbol,
866
+ eid=str(res.data.orderId),
867
+ oid=oid,
868
+ status=OrderStatus.PENDING,
869
+ side=side,
870
+ type=type,
871
+ price=float(price) if price is not None else None,
872
+ amount=amount,
873
+ time_in_force=time_in_force,
874
+ filled=Decimal(0),
875
+ remaining=amount,
876
+ reduce_only=reduce_only,
877
+ timestamp=res.requestTime,
878
+ )
879
+
880
+ except Exception as e:
881
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
882
+ self._log.error(
883
+ f"Error creating order: {error_msg} params: {str(params)}"
884
+ )
885
+ order = Order(
886
+ exchange=self._exchange_id,
887
+ symbol=symbol,
888
+ oid=oid,
889
+ status=OrderStatus.FAILED,
890
+ side=side,
891
+ type=type,
892
+ price=float(price) if price is not None else None,
893
+ amount=amount,
894
+ time_in_force=time_in_force,
895
+ filled=Decimal(0),
896
+ remaining=amount,
897
+ reduce_only=reduce_only,
898
+ timestamp=self._clock.timestamp_ms(),
899
+ reason=error_msg,
900
+ )
901
+ self.order_status_update(order)
902
+ else:
903
+ params = {
904
+ "symbol": market.id,
905
+ "side": BitgetEnumParser.to_bitget_order_side(side).value,
906
+ "size": str(amount),
907
+ "clientOid": oid,
908
+ }
909
+ if type.is_limit:
910
+ if price is None or time_in_force is None:
911
+ raise ValueError(
912
+ "price and time_in_force are required for limit orders"
913
+ )
914
+ params["price"] = str(price)
915
+ params["force"] = BitgetEnumParser.to_bitget_time_in_force(
916
+ time_in_force
917
+ ).value
918
+ params["orderType"] = "limit"
919
+ elif type.is_post_only:
920
+ if price is None:
921
+ raise ValueError("price is required for post-only orders")
922
+ params["price"] = str(price)
923
+ params["force"] = "post_only"
924
+ params["orderType"] = "limit"
925
+ elif type.is_market:
926
+ bookl1 = self._cache.bookl1(symbol)
927
+ if not bookl1:
928
+ raise ValueError(
929
+ "Please Subscribe to bookl1 first, since market order requires bookl1 data"
930
+ )
931
+ if side.is_buy:
932
+ price = self._price_to_precision(
933
+ symbol, bookl1.ask * (1 + self._max_slippage), mode="ceil"
934
+ )
935
+ else:
936
+ price = self._price_to_precision(
937
+ symbol, bookl1.bid * (1 - self._max_slippage), mode="floor"
938
+ )
939
+ params["price"] = str(price)
940
+ params["force"] = BitgetEnumParser.to_bitget_time_in_force(
941
+ TimeInForce.IOC
942
+ ).value
943
+ params["orderType"] = "limit"
944
+
945
+ params.update(kwargs)
946
+
947
+ try:
948
+ if market.swap:
949
+ params["marginCoin"] = market.quote
950
+ params["marginMode"] = kwargs.get("marginMode", "crossed")
951
+ params["productType"] = self._get_inst_type(market)
952
+ if reduce_only:
953
+ params["reduceOnly"] = "YES"
954
+
955
+ res = await self._api_client.post_api_v2_mix_order_place_order(
956
+ **params
957
+ )
958
+ else:
959
+ res = await self._api_client.post_api_v2_spot_trade_place_order(
960
+ **params
961
+ )
962
+
963
+ order = Order(
964
+ exchange=self._exchange_id,
965
+ symbol=symbol,
966
+ oid=oid,
967
+ eid=str(res.data.orderId),
968
+ status=OrderStatus.PENDING,
969
+ side=side,
970
+ type=type,
971
+ price=float(price) if price is not None else None,
972
+ amount=amount,
973
+ time_in_force=time_in_force,
974
+ filled=Decimal(0),
975
+ remaining=amount,
976
+ reduce_only=reduce_only,
977
+ timestamp=res.requestTime,
978
+ )
979
+
980
+ except Exception as e:
981
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
982
+ self._log.error(
983
+ f"Error creating order: {error_msg} params: {str(params)}"
984
+ )
985
+ order = Order(
986
+ exchange=self._exchange_id,
987
+ symbol=symbol,
988
+ oid=oid,
989
+ status=OrderStatus.FAILED,
990
+ side=side,
991
+ type=type,
992
+ price=float(price) if price is not None else None,
993
+ amount=amount,
994
+ time_in_force=time_in_force,
995
+ filled=Decimal(0),
996
+ remaining=amount,
997
+ reduce_only=reduce_only,
998
+ timestamp=self._clock.timestamp_ms(),
999
+ reason=error_msg,
1000
+ )
1001
+ self.order_status_update(order)
1002
+ return order
1003
+
1004
+ async def create_batch_orders(
1005
+ self,
1006
+ orders: List[BatchOrderSubmit],
1007
+ ) -> List[Order]:
1008
+ """Create multiple orders in a batch"""
1009
+ raise NotImplementedError
1010
+
1011
+ async def cancel_order(self, oid: str, symbol: str, **kwargs) -> Order:
1012
+ """Cancel an order"""
1013
+ market = self._market.get(symbol)
1014
+ if not market:
1015
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1016
+ id = market.id
1017
+ params = {
1018
+ "symbol": id,
1019
+ "clientOid": oid,
1020
+ }
1021
+ params.update(kwargs)
1022
+ try:
1023
+ if self._account_type.is_uta:
1024
+ res = await self._api_client.post_api_v3_trade_cancel_order(
1025
+ clientOid=oid,
1026
+ )
1027
+
1028
+ else:
1029
+ if market.swap:
1030
+ params["productType"] = self._get_inst_type(market)
1031
+ res = await self._api_client.post_api_v2_mix_order_cancel_order(
1032
+ **params
1033
+ )
1034
+ else:
1035
+ res = await self._api_client.post_api_v2_spot_trade_cancel_order(
1036
+ **params
1037
+ )
1038
+ order = Order(
1039
+ oid=oid,
1040
+ exchange=self._exchange_id,
1041
+ eid=res.data.orderId,
1042
+ timestamp=res.requestTime,
1043
+ symbol=symbol,
1044
+ status=OrderStatus.CANCELING,
1045
+ )
1046
+ except BitgetRateLimitError as e:
1047
+ error_msg = f"rate_limit (retry_after={e.retry_after:.1f}s): {str(e)}"
1048
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
1049
+ order = Order(
1050
+ oid=oid,
1051
+ exchange=self._exchange_id,
1052
+ timestamp=self._clock.timestamp_ms(),
1053
+ symbol=symbol,
1054
+ status=OrderStatus.CANCEL_FAILED,
1055
+ reason=error_msg,
1056
+ )
1057
+ except BitgetError as e:
1058
+ error_msg = str(e)
1059
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
1060
+ order = Order(
1061
+ oid=oid,
1062
+ exchange=self._exchange_id,
1063
+ timestamp=self._clock.timestamp_ms(),
1064
+ symbol=symbol,
1065
+ status=OrderStatus.CANCEL_FAILED,
1066
+ reason=error_msg,
1067
+ )
1068
+ except Exception as e:
1069
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1070
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
1071
+ order = Order(
1072
+ oid=oid,
1073
+ exchange=self._exchange_id,
1074
+ timestamp=self._clock.timestamp_ms(),
1075
+ symbol=symbol,
1076
+ status=OrderStatus.CANCEL_FAILED,
1077
+ reason=error_msg,
1078
+ )
1079
+ self.order_status_update(order)
1080
+ return order
1081
+
1082
+ @staticmethod
1083
+ def _optional_decimal(value: Any) -> Decimal | None:
1084
+ if value is None or value == "":
1085
+ return None
1086
+ return Decimal(str(value))
1087
+
1088
+ @staticmethod
1089
+ def _optional_float(value: Any) -> float | None:
1090
+ if value is None or value == "":
1091
+ return None
1092
+ return float(value)
1093
+
1094
+ @staticmethod
1095
+ def _parse_reduce_only(value: Any) -> bool | None:
1096
+ if value is None or value == "":
1097
+ return None
1098
+ if isinstance(value, bool):
1099
+ return value
1100
+ return str(value).lower() in {"yes", "true", "1"}
1101
+
1102
+ def _parse_query_order_data(
1103
+ self,
1104
+ *,
1105
+ oid: str,
1106
+ symbol: str,
1107
+ data: dict[str, Any],
1108
+ ) -> Order:
1109
+ cached_order = self._cache.get_order(oid).value_or(None)
1110
+
1111
+ status_value = (
1112
+ data.get("orderStatus") or data.get("state") or data.get("status")
1113
+ )
1114
+ status = BitgetEnumParser.parse_order_status(
1115
+ BitgetOrderStatus(str(status_value))
1116
+ )
1117
+
1118
+ tif_value = data.get("timeInForce") or data.get("force")
1119
+ time_in_force = None
1120
+ if tif_value:
1121
+ tif = BitgetTimeInForce(str(tif_value).lower().replace(" ", "_"))
1122
+ time_in_force = (
1123
+ TimeInForce.GTC
1124
+ if tif.is_post_only
1125
+ else (BitgetEnumParser.parse_time_in_force(tif))
1126
+ )
1127
+
1128
+ if cached_order and cached_order.type:
1129
+ order_type = cached_order.type
1130
+ else:
1131
+ order_type_value = data.get("orderType")
1132
+ if tif_value and str(tif_value).lower().replace(" ", "_") == "post_only":
1133
+ order_type = OrderType.POST_ONLY
1134
+ else:
1135
+ order_type = BitgetEnumParser.parse_order_type(
1136
+ BitgetOrderType(str(order_type_value))
1137
+ )
1138
+
1139
+ amount = self._optional_decimal(data.get("qty"))
1140
+ if amount is None:
1141
+ amount = self._optional_decimal(data.get("size"))
1142
+ if (amount is None or amount == Decimal(0)) and cached_order:
1143
+ amount = cached_order.amount
1144
+
1145
+ filled = (
1146
+ self._optional_decimal(data.get("cumExecQty"))
1147
+ or self._optional_decimal(data.get("baseVolume"))
1148
+ or Decimal(0)
1149
+ )
1150
+ remaining = amount - filled if amount is not None else None
1151
+ if remaining is not None and remaining < Decimal(0):
1152
+ remaining = None
1153
+
1154
+ fee = None
1155
+ fee_currency = None
1156
+ fee_detail = data.get("feeDetail")
1157
+ if isinstance(fee_detail, list) and fee_detail:
1158
+ fee = sum(
1159
+ (Decimal(str(item.get("fee", "0"))) for item in fee_detail),
1160
+ Decimal(0),
1161
+ )
1162
+ fee_currency = fee_detail[0].get("feeCoin")
1163
+ elif data.get("fee"):
1164
+ fee = Decimal(str(data["fee"]))
1165
+
1166
+ position_side = None
1167
+ pos_side = data.get("posSide") or data.get("holdSide")
1168
+ if pos_side:
1169
+ position_side = BitgetPositionSide(str(pos_side)).parse_to_position_side()
1170
+
1171
+ timestamp = (
1172
+ data.get("updatedTime")
1173
+ or data.get("uTime")
1174
+ or data.get("createdTime")
1175
+ or data.get("cTime")
1176
+ or self._clock.timestamp_ms()
1177
+ )
1178
+
1179
+ return Order(
1180
+ exchange=self._exchange_id,
1181
+ symbol=symbol,
1182
+ status=status,
1183
+ eid=str(data.get("orderId")) if data.get("orderId") else None,
1184
+ oid=str(data.get("clientOid") or oid),
1185
+ amount=amount,
1186
+ filled=filled,
1187
+ timestamp=int(timestamp),
1188
+ type=order_type,
1189
+ side=BitgetEnumParser.parse_order_side(BitgetOrderSide(str(data["side"])))
1190
+ if data.get("side")
1191
+ else None,
1192
+ time_in_force=time_in_force,
1193
+ price=self._optional_float(data.get("price")),
1194
+ average=self._optional_float(data.get("avgPrice") or data.get("priceAvg")),
1195
+ remaining=remaining,
1196
+ fee=fee,
1197
+ fee_currency=fee_currency,
1198
+ cum_cost=self._optional_decimal(
1199
+ data.get("cumExecValue") or data.get("quoteVolume")
1200
+ ),
1201
+ reduce_only=self._parse_reduce_only(data.get("reduceOnly")),
1202
+ position_side=position_side,
1203
+ )
1204
+
1205
+ async def query_order(self, oid: str, symbol: str) -> Order | None:
1206
+ market = self._market.get(symbol)
1207
+ if not market:
1208
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1209
+
1210
+ try:
1211
+ if self._account_type.is_uta:
1212
+ res = await self._api_client.get_api_v3_trade_order_info(clientOid=oid)
1213
+ elif market.swap:
1214
+ res = await self._api_client.get_api_v2_mix_order_detail(
1215
+ symbol=market.id,
1216
+ productType=self._get_inst_type(market),
1217
+ clientOid=oid,
1218
+ )
1219
+ else:
1220
+ res = await self._api_client.get_api_v2_spot_trade_order_info(
1221
+ clientOid=oid
1222
+ )
1223
+
1224
+ if res.get("code") != "00000":
1225
+ self._log.error(f"Error querying order: {res} oid={oid}")
1226
+ return None
1227
+
1228
+ data = res.get("data")
1229
+ if isinstance(data, list):
1230
+ data = data[0] if data else None
1231
+ if not data:
1232
+ return None
1233
+
1234
+ return self._parse_query_order_data(
1235
+ oid=oid,
1236
+ symbol=symbol,
1237
+ data=data,
1238
+ )
1239
+ except Exception as e:
1240
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1241
+ self._log.error(f"Error querying order: {error_msg} oid={oid}")
1242
+ return None
1243
+
1244
+ async def modify_order(
1245
+ self,
1246
+ oid: str,
1247
+ symbol: str,
1248
+ side: OrderSide | None = None,
1249
+ price: Decimal | None = None,
1250
+ amount: Decimal | None = None,
1251
+ **kwargs,
1252
+ ) -> Order:
1253
+ """Modify an order"""
1254
+ raise NotImplementedError
1255
+
1256
+ async def cancel_all_orders(self, symbol: str) -> bool:
1257
+ """Cancel all orders"""
1258
+ if not self._account_type.is_uta:
1259
+ raise NotImplementedError("Only UTA account type is supported")
1260
+
1261
+ try:
1262
+ market = self._market.get(symbol)
1263
+ if not market:
1264
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1265
+ symbol = market.id
1266
+ category = self._get_inst_type(market)
1267
+
1268
+ await self._api_client.post_api_v3_trade_cancel_symbol_order(
1269
+ symbol=symbol, category=category
1270
+ )
1271
+ return True
1272
+
1273
+ except Exception as e:
1274
+ error_msg = f"{e.__class__.__name__}: {str(e)} params: symbol={symbol} category={category}"
1275
+ self._log.error(f"Error canceling all orders: {error_msg}")
1276
+ return False
1277
+
1278
+ def _ws_uta_msg_handler(self, raw: bytes):
1279
+ # if raw == b"pong":
1280
+ # self._ws_client._transport.notify_user_specific_pong_received()
1281
+ # self._log.debug(f"Pong received: `{raw.decode()}`")
1282
+ # return
1283
+ try:
1284
+ ws_msg = self._ws_msg_uta_general_decoder.decode(raw)
1285
+ if ws_msg.is_event_data:
1286
+ self._handle_event_data(ws_msg)
1287
+ elif ws_msg.arg is not None and ws_msg.arg.topic == "order":
1288
+ self._handle_uta_order_event(raw)
1289
+ elif ws_msg.arg is not None and ws_msg.arg.topic == "position":
1290
+ self._handle_uta_position_event(raw)
1291
+ elif ws_msg.arg is not None and ws_msg.arg.topic == "account":
1292
+ self._handle_uta_account_event(raw)
1293
+
1294
+ except msgspec.DecodeError as e:
1295
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
1296
+
1297
+ def _handle_uta_account_event(self, raw: bytes):
1298
+ msg = self._ws_msg_uta_account_decoder.decode(raw)
1299
+ balances = msg.parse_to_balances()
1300
+ self._cache._apply_balance(
1301
+ account_type=self._account_type,
1302
+ balances=balances,
1303
+ )
1304
+ for balance in balances:
1305
+ self._log.debug(
1306
+ f"Balance update: {balance.asset} - {balance.free} free, {balance.locked} locked"
1307
+ )
1308
+
1309
+ def _handle_uta_position_event(self, raw: bytes):
1310
+ msg = self._ws_msg_uta_positions_decoder.decode(raw)
1311
+ for data in msg.data:
1312
+ # Determine suffix based on marginCoin
1313
+ if data.marginCoin in ["USDT", "USDC"]:
1314
+ suffix = "linear"
1315
+ else:
1316
+ suffix = "inverse"
1317
+
1318
+ sym_id = f"{data.symbol}_{suffix}"
1319
+ symbol = self._market_id.get(sym_id)
1320
+
1321
+ if not symbol:
1322
+ self._log.warning(f"Symbol not found for UTA position: {sym_id}")
1323
+ continue
1324
+
1325
+ # Parse position side
1326
+ position_side = data.posSide.parse_to_position_side()
1327
+ signed_amount = Decimal(data.size)
1328
+ if position_side.is_short:
1329
+ signed_amount = -signed_amount
1330
+
1331
+ # Create Position object
1332
+ position = Position(
1333
+ symbol=symbol,
1334
+ exchange=self._exchange_id,
1335
+ signed_amount=signed_amount,
1336
+ entry_price=float(data.openPriceAvg or 0.0),
1337
+ side=position_side,
1338
+ unrealized_pnl=float(data.unrealisedPnl or 0.0),
1339
+ realized_pnl=float(data.curRealisedPnl or 0.0),
1340
+ )
1341
+
1342
+ # Apply position to cache
1343
+ self._cache._apply_position(position)
1344
+ self._log.debug(f"Position update: {str(position)}")
1345
+
1346
+ def _handle_uta_order_event(self, raw: bytes):
1347
+ msg = self._ws_msg_uta_orders_decoder.decode(raw)
1348
+ self._log.debug(f"Received UTA order event: {str(msg)}")
1349
+ for data in msg.data:
1350
+ tmp_order = self._registry.get_tmp_order(str(data.clientOid))
1351
+ if not tmp_order:
1352
+ continue
1353
+
1354
+ status = BitgetEnumParser.parse_order_status(data.orderStatus)
1355
+ if not status:
1356
+ continue
1357
+
1358
+ # Build symbol ID using the helper method
1359
+ inst_type_suffix = self._uta_inst_type_suffix(data.category)
1360
+ sym_id = f"{data.symbol}_{inst_type_suffix}"
1361
+
1362
+ symbol = self._market_id.get(sym_id)
1363
+ if not symbol:
1364
+ self._log.warning(f"Symbol not found for {sym_id}")
1365
+ continue
1366
+
1367
+ # Parse order data
1368
+ timestamp = int(data.updatedTime)
1369
+
1370
+ # NOTE: since market order is sent as limit order with taker price,
1371
+ # though it is a market order, the ws data will show it as limit order
1372
+ # we rely on the tmp_order to get the correct order type
1373
+
1374
+ # Parse order type
1375
+ # if data.orderType.is_market:
1376
+ # order_type = OrderType.MARKET
1377
+ # elif data.timeInForce.is_post_only:
1378
+ # order_type = OrderType.POST_ONLY
1379
+ # else:
1380
+ # order_type = OrderType.LIMIT # Default fallback
1381
+ order_type = tmp_order.type
1382
+
1383
+ # Calculate remaining quantity
1384
+ remaining = Decimal(data.qty) - Decimal(data.cumExecQty)
1385
+
1386
+ # Calculate average price
1387
+ average_price = float(data.avgPrice or 0)
1388
+ price = float(data.price or 0)
1389
+
1390
+ # Calculate fee
1391
+ total_fee = Decimal("0")
1392
+ fee_currency = None
1393
+ if data.feeDetail:
1394
+ total_fee = sum(
1395
+ (Decimal(fee.fee) for fee in data.feeDetail),
1396
+ Decimal("0"),
1397
+ )
1398
+ if data.feeDetail:
1399
+ fee_currency = data.feeDetail[0].feeCoin
1400
+
1401
+ order = Order(
1402
+ exchange=self._exchange_id,
1403
+ eid=data.orderId,
1404
+ oid=data.clientOid,
1405
+ timestamp=timestamp,
1406
+ symbol=symbol,
1407
+ type=order_type,
1408
+ side=BitgetEnumParser.parse_order_side(data.side),
1409
+ price=price,
1410
+ average=average_price,
1411
+ amount=Decimal(data.qty),
1412
+ filled=Decimal(data.cumExecQty),
1413
+ remaining=remaining,
1414
+ status=status,
1415
+ fee=total_fee,
1416
+ fee_currency=fee_currency,
1417
+ cum_cost=Decimal(data.cumExecValue or 0),
1418
+ reduce_only=data.reduceOnly == "yes",
1419
+ )
1420
+ self._log.debug(f"Order update: {str(order)}")
1421
+ self.order_status_update(order)
1422
+
1423
+ def _ws_msg_handler(self, raw: bytes):
1424
+ """Handle incoming WebSocket messages"""
1425
+ # Process the message based on its type
1426
+ # if raw == b"pong":
1427
+ # self._ws_client._transport.notify_user_specific_pong_received()
1428
+ # self._log.debug(f"Pong received: `{raw.decode()}`")
1429
+ # return
1430
+
1431
+ try:
1432
+ ws_msg = self._ws_msg_general_decoder.decode(raw)
1433
+ if ws_msg.is_event_data:
1434
+ self._handle_event_data(ws_msg)
1435
+ elif ws_msg.arg is not None and ws_msg.arg.channel == "orders":
1436
+ self._handle_orders_event(raw, ws_msg.arg)
1437
+ elif ws_msg.arg is not None and ws_msg.arg.channel == "positions":
1438
+ self._handle_positions_event(raw, ws_msg.arg)
1439
+ elif ws_msg.arg is not None and ws_msg.arg.channel == "account":
1440
+ self._handle_account_event(raw, ws_msg.arg)
1441
+
1442
+ except msgspec.DecodeError as e:
1443
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
1444
+
1445
+ def _handle_account_event(self, raw: bytes, arg: BitgetWsArgMsg):
1446
+ if arg.instType.is_spot:
1447
+ msg = self._ws_msg_spot_account_decoder.decode(raw)
1448
+ else:
1449
+ msg = self._ws_msg_futures_account_decoder.decode(raw)
1450
+ self._cache._apply_balance(
1451
+ account_type=self._account_type, balances=msg.parse_to_balances()
1452
+ )
1453
+
1454
+ def _handle_positions_event(self, raw: bytes, arg: BitgetWsArgMsg):
1455
+ msg = self._ws_msg_positions_decoder.decode(raw)
1456
+
1457
+ # Get existing positions for this specific instrument type
1458
+ existing_positions = self._cache.get_all_positions(exchange=self._exchange_id)
1459
+ inst_type_suffix = self._inst_type_suffix(arg.instType)
1460
+
1461
+ # Filter existing positions to only include those from this instrument type
1462
+ if arg.instType.is_usdt_swap:
1463
+ existing_positions_for_inst_type = {
1464
+ symbol: pos
1465
+ for symbol, pos in existing_positions.items()
1466
+ if self._market[symbol].quote == "USDT"
1467
+ }
1468
+ elif arg.instType.is_usdc_swap:
1469
+ existing_positions_for_inst_type = {
1470
+ symbol: pos
1471
+ for symbol, pos in existing_positions.items()
1472
+ if self._market[symbol].quote == "USDC"
1473
+ }
1474
+ elif arg.instType.is_inverse:
1475
+ existing_positions_for_inst_type = {
1476
+ symbol: pos
1477
+ for symbol, pos in existing_positions.items()
1478
+ if self._market[symbol].inverse
1479
+ }
1480
+
1481
+ active_symbols = set()
1482
+
1483
+ for data in msg.data:
1484
+ sym_id = data.instId
1485
+ symbol = self._market_id[f"{sym_id}_{inst_type_suffix}"]
1486
+ active_symbols.add(symbol)
1487
+
1488
+ # Convert Bitget position data to Position
1489
+ signed_amount = Decimal(data.total)
1490
+ if data.holdSide.is_short:
1491
+ signed_amount = -signed_amount
1492
+
1493
+ position_side = data.holdSide.parse_to_position_side()
1494
+
1495
+ position = Position(
1496
+ symbol=symbol,
1497
+ exchange=self._exchange_id,
1498
+ signed_amount=signed_amount,
1499
+ entry_price=float(data.openPriceAvg),
1500
+ side=position_side,
1501
+ unrealized_pnl=float(data.unrealizedPL),
1502
+ realized_pnl=float(data.achievedProfits),
1503
+ )
1504
+
1505
+ self._cache._apply_position(position)
1506
+ self._log.debug(f"Position update: {str(position)}")
1507
+
1508
+ # Close positions that are not in the snapshot (position goes to 0)
1509
+ # Only check positions for the current instrument type
1510
+ for symbol, existing_position in existing_positions_for_inst_type.items():
1511
+ if symbol not in active_symbols:
1512
+ # Create a closed position with signed_amount = 0
1513
+ closed_position = Position(
1514
+ symbol=symbol,
1515
+ exchange=self._exchange_id,
1516
+ signed_amount=Decimal("0"),
1517
+ entry_price=existing_position.entry_price,
1518
+ side=None,
1519
+ unrealized_pnl=0,
1520
+ realized_pnl=existing_position.realized_pnl,
1521
+ )
1522
+ self._cache._apply_position(closed_position)
1523
+ self._log.debug(f"Position closed: {str(closed_position)}")
1524
+
1525
+ def _handle_event_data(self, msg: BitgetWsGeneralMsg | BitgetWsUtaGeneralMsg):
1526
+ if msg.event == "subscribe":
1527
+ arg = msg.arg
1528
+ if arg is None:
1529
+ return
1530
+ self._log.debug(f"Subscribed to {arg.message}")
1531
+ elif msg.event == "error":
1532
+ code = msg.code
1533
+ error_msg = msg.msg
1534
+ self._log.error(f"Subscribed error code={code} {error_msg}")
1535
+ elif msg.event == "login":
1536
+ if msg.code == 0:
1537
+ self._log.debug("WebSocket login successful")
1538
+ else:
1539
+ self._log.error(f"WebSocket login failed: {msg.msg}")
1540
+
1541
+ def _handle_orders_event(self, raw: bytes, arg: BitgetWsArgMsg):
1542
+ msg = self._ws_msg_orders_decoder.decode(raw)
1543
+ self._log.debug(f"Received order event: {str(msg)}")
1544
+ for data in msg.data:
1545
+ tmp_order = self._registry.get_tmp_order(str(data.clientOid))
1546
+ if not tmp_order:
1547
+ continue
1548
+
1549
+ sym_id = data.instId
1550
+ timestamp = int(data.uTime)
1551
+ typ = tmp_order.type
1552
+
1553
+ status = BitgetEnumParser.parse_order_status(data.status)
1554
+ side = BitgetEnumParser.parse_order_side(data.side)
1555
+
1556
+ if fee_details := data.feeDetail:
1557
+ fee = fee_details[0].fee
1558
+ fee_currency = fee_details[0].feeCoin
1559
+ else:
1560
+ fee = 0
1561
+ fee_currency = None
1562
+ filled = data.accBaseVolume or "0"
1563
+ last_filled = data.accBaseVolume or "0"
1564
+ last_filled_price = data.fillPrice or 0
1565
+ price = data.price or 0
1566
+ average = data.priceAvg or 0
1567
+ new_size = data.newSize or "0"
1568
+ size = data.size or "0"
1569
+
1570
+ if arg.instType.is_spot:
1571
+ symbol = self._market_id[f"{sym_id}_spot"]
1572
+ order = Order(
1573
+ exchange=self._exchange_id,
1574
+ symbol=symbol,
1575
+ eid=str(data.orderId),
1576
+ oid=str(data.clientOid),
1577
+ amount=Decimal(new_size),
1578
+ filled=Decimal(filled),
1579
+ timestamp=timestamp,
1580
+ status=status,
1581
+ side=side,
1582
+ type=typ,
1583
+ last_filled=Decimal(last_filled),
1584
+ last_filled_price=float(last_filled_price),
1585
+ remaining=Decimal(new_size) - Decimal(filled),
1586
+ fee=Decimal(fee),
1587
+ fee_currency=fee_currency,
1588
+ price=float(price),
1589
+ average=float(average),
1590
+ cost=Decimal(last_filled) * Decimal(last_filled_price),
1591
+ cum_cost=Decimal(filled) * Decimal(average),
1592
+ )
1593
+ else:
1594
+ symbol = self._market_id[
1595
+ f"{sym_id}_{self._inst_type_suffix(arg.instType)}"
1596
+ ]
1597
+ order = Order(
1598
+ exchange=self._exchange_id,
1599
+ symbol=symbol,
1600
+ eid=str(data.orderId),
1601
+ oid=str(data.clientOid),
1602
+ amount=Decimal(size),
1603
+ filled=Decimal(filled),
1604
+ timestamp=timestamp,
1605
+ status=status,
1606
+ side=side,
1607
+ type=typ,
1608
+ last_filled=Decimal(last_filled),
1609
+ last_filled_price=float(last_filled_price),
1610
+ remaining=Decimal(size) - Decimal(filled),
1611
+ fee=Decimal(fee),
1612
+ fee_currency=fee_currency,
1613
+ average=float(average),
1614
+ price=float(price),
1615
+ cost=Decimal(last_filled) * Decimal(last_filled_price),
1616
+ cum_cost=Decimal(filled) * Decimal(average),
1617
+ reduce_only=data.reduceOnly == "yes",
1618
+ )
1619
+ self.order_status_update(order)