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,1807 @@
1
+ import msgspec
2
+ import asyncio
3
+ from decimal import Decimal
4
+
5
+ from typing import Dict, Any, Mapping, List
6
+ from walrasquant.constants import (
7
+ PositionSide,
8
+ ExchangeType,
9
+ OrderSide,
10
+ OrderStatus,
11
+ OrderType,
12
+ TimeInForce,
13
+ TriggerType,
14
+ )
15
+ from walrasquant.config import OrderQueryConfig
16
+ from walrasquant.schema import Order, Position, BatchOrderSubmit, CancelOrderSubmit
17
+ from walrasquant.base import OrderManagementSystem
18
+ from walrasquant.core.registry import OrderRegistry
19
+ from walrasquant.core.entity import TaskManager
20
+ from walrasquant.error import PositionModeError
21
+ from walrasquant.exchange.binance.error import (
22
+ BinanceRateLimitError,
23
+ BinanceClientError,
24
+ BinanceServerError,
25
+ )
26
+ from walrasquant.exchange.binance.schema import (
27
+ BinanceMarket,
28
+ BinanceOrder,
29
+ BinanceFuturesPositionInfo,
30
+ BinancePortfolioMarginPositionRisk,
31
+ )
32
+ from walrasquant.exchange.binance.constants import (
33
+ BinanceAccountType,
34
+ BinanceOrderType,
35
+ BinanceTimeInForce,
36
+ )
37
+ from walrasquant.exchange.binance.rest_api import BinanceApiClient
38
+ from walrasquant.exchange.binance.websockets import BinanceWSClient, BinanceWSApiClient
39
+ from walrasquant.core.nautilius_core import LiveClock, MessageBus
40
+ from walrasquant.exchange.binance.constants import (
41
+ BinanceUserDataStreamWsEventType,
42
+ BinanceBusinessUnit,
43
+ BinanceEnumParser,
44
+ )
45
+ from walrasquant.exchange.binance.schema import (
46
+ BinanceUserDataStreamMsg,
47
+ BinanceSpotOrderUpdateMsg,
48
+ _BinanceSpotOrderUpdateMsg,
49
+ _BinanceSpotUpdateMsg,
50
+ BinanceFuturesOrderUpdateMsg,
51
+ BinanceWsOrderResponse,
52
+ BinanceSpotUpdateMsg,
53
+ BinanceFuturesUpdateMsg,
54
+ BinancePortfolioMarginBalance,
55
+ BinanceSpotUserDataStreamMsg,
56
+ )
57
+ from walrasquant.core.cache import AsyncCache
58
+
59
+
60
+ class BinanceOrderManagementSystem(OrderManagementSystem):
61
+ _account_type: BinanceAccountType
62
+ _market: Mapping[str, BinanceMarket]
63
+ _market_id: Dict[str, str]
64
+ _api_client: BinanceApiClient
65
+ _ws_client: BinanceWSClient
66
+
67
+ def __init__(
68
+ self,
69
+ account_type: BinanceAccountType,
70
+ api_key: str,
71
+ secret: str,
72
+ market: Dict[str, BinanceMarket],
73
+ market_id: Dict[str, str],
74
+ registry: OrderRegistry,
75
+ cache: AsyncCache,
76
+ api_client: BinanceApiClient,
77
+ exchange_id: ExchangeType,
78
+ clock: LiveClock,
79
+ msgbus: MessageBus,
80
+ task_manager: TaskManager,
81
+ enable_rate_limit: bool,
82
+ order_query_config: OrderQueryConfig,
83
+ max_subscriptions_per_client: int | None = None,
84
+ max_clients: int | None = None,
85
+ ):
86
+ super().__init__(
87
+ account_type=account_type,
88
+ market=market, # type: ignore[arg-type]
89
+ market_id=market_id,
90
+ registry=registry,
91
+ cache=cache,
92
+ api_client=api_client,
93
+ ws_client=BinanceWSClient(
94
+ account_type=account_type,
95
+ handler=self._ws_msg_handler,
96
+ task_manager=task_manager,
97
+ clock=clock,
98
+ # For USD-M Futures, connect to the dedicated /private channel URL
99
+ custom_url=(
100
+ (account_type.ws_private_url + "/ws")
101
+ if account_type.ws_private_url
102
+ else None
103
+ ),
104
+ max_subscriptions_per_client=max_subscriptions_per_client,
105
+ max_clients=max_clients,
106
+ )
107
+ if not account_type.is_spot
108
+ else BinanceWSApiClient(
109
+ account_type=account_type,
110
+ api_key=api_key,
111
+ secret=secret,
112
+ handler=self._ws_spot_msg_handler,
113
+ task_manager=task_manager,
114
+ clock=clock,
115
+ enable_rate_limit=enable_rate_limit,
116
+ ),
117
+ exchange_id=exchange_id,
118
+ clock=clock,
119
+ msgbus=msgbus,
120
+ task_manager=task_manager,
121
+ order_query_config=order_query_config,
122
+ )
123
+ self._ws_api_client = None
124
+ if self._account_type.is_spot or self._account_type.is_future:
125
+ self._ws_api_client = BinanceWSApiClient(
126
+ account_type=account_type,
127
+ api_key=api_key,
128
+ secret=secret,
129
+ handler=self._ws_api_msg_handler,
130
+ task_manager=task_manager,
131
+ clock=clock,
132
+ enable_rate_limit=enable_rate_limit,
133
+ )
134
+
135
+ self._ws_msg_general_decoder = msgspec.json.Decoder(BinanceUserDataStreamMsg)
136
+ self._ws_spot_msg_general_decoder = msgspec.json.Decoder(
137
+ BinanceSpotUserDataStreamMsg
138
+ )
139
+ self._ws_msg_spot_order_update_decoder = msgspec.json.Decoder(
140
+ BinanceSpotOrderUpdateMsg
141
+ )
142
+
143
+ self._ws_msg_pm_margin_order_update_decoder = msgspec.json.Decoder(
144
+ _BinanceSpotOrderUpdateMsg
145
+ )
146
+
147
+ self._ws_msg_pm_margin_account_update_decoder = msgspec.json.Decoder(
148
+ _BinanceSpotUpdateMsg
149
+ )
150
+
151
+ self._ws_msg_futures_order_update_decoder = msgspec.json.Decoder(
152
+ BinanceFuturesOrderUpdateMsg
153
+ )
154
+ self._ws_msg_spot_account_update_decoder = msgspec.json.Decoder(
155
+ BinanceSpotUpdateMsg
156
+ )
157
+ self._ws_msg_futures_account_update_decoder = msgspec.json.Decoder(
158
+ BinanceFuturesUpdateMsg
159
+ )
160
+ self._ws_msg_ws_api_response_decoder = msgspec.json.Decoder(
161
+ BinanceWsOrderResponse
162
+ )
163
+
164
+ @property
165
+ def market_type(self):
166
+ if self._account_type.is_spot:
167
+ return "_spot"
168
+ elif self._account_type.is_linear:
169
+ return "_linear"
170
+ elif self._account_type.is_inverse:
171
+ return "_inverse"
172
+
173
+ def _supports_query_market(self, market: BinanceMarket) -> bool:
174
+ if self._account_type.is_spot:
175
+ return market.spot
176
+ if self._account_type.is_isolated_margin_or_margin:
177
+ return market.margin
178
+ if self._account_type.is_linear:
179
+ return market.linear
180
+ if self._account_type.is_inverse:
181
+ return market.inverse
182
+ if self._account_type.is_portfolio_margin:
183
+ return market.margin or market.linear or market.inverse
184
+ return False
185
+
186
+ def _ws_api_msg_handler(self, raw: bytes):
187
+ try:
188
+ msg = self._ws_msg_ws_api_response_decoder.decode(raw)
189
+ id = msg.id
190
+ oid = id[1:] # remove the prefix 'n' or 'c'
191
+
192
+ tmp_order = self._registry.get_tmp_order(oid)
193
+ if not tmp_order:
194
+ return
195
+
196
+ ts = self._clock.timestamp_ms()
197
+ if id.startswith("n"): # new order
198
+ if msg.is_success:
199
+ assert msg.result is not None
200
+ sym_id = f"{msg.result.symbol}{self.market_type}"
201
+ symbol = self._market_id[sym_id]
202
+ eid = str(msg.result.orderId)
203
+ self._log.debug(
204
+ f"[{symbol}] new order success: oid: {oid} eid: {eid}"
205
+ )
206
+ order = Order(
207
+ exchange=self._exchange_id,
208
+ symbol=symbol,
209
+ oid=oid,
210
+ eid=eid,
211
+ status=OrderStatus.PENDING,
212
+ side=tmp_order.side,
213
+ timestamp=ts,
214
+ amount=tmp_order.amount,
215
+ type=tmp_order.type,
216
+ price=tmp_order.price,
217
+ time_in_force=tmp_order.time_in_force,
218
+ reduce_only=tmp_order.reduce_only,
219
+ )
220
+ self.order_status_update(order)
221
+ else:
222
+ assert msg.error is not None
223
+ symbol = tmp_order.symbol
224
+ self._log.error(
225
+ f"[{symbol}] new order failed: oid: {oid} {msg.error.format_str}"
226
+ )
227
+ order = Order(
228
+ exchange=self._exchange_id,
229
+ symbol=symbol,
230
+ oid=oid,
231
+ status=OrderStatus.FAILED,
232
+ amount=tmp_order.amount,
233
+ type=tmp_order.type,
234
+ timestamp=ts,
235
+ side=tmp_order.side,
236
+ price=tmp_order.price,
237
+ time_in_force=tmp_order.time_in_force,
238
+ reduce_only=tmp_order.reduce_only,
239
+ reason=msg.error.format_str,
240
+ )
241
+ self.order_status_update(order)
242
+ else:
243
+ if msg.is_success:
244
+ assert msg.result is not None
245
+ sym_id = f"{msg.result.symbol}{self.market_type}"
246
+ symbol = self._market_id[sym_id]
247
+ eid = str(msg.result.orderId)
248
+ self._log.debug(
249
+ f"[{symbol}] canceling order success: oid: {oid} eid: {eid}"
250
+ )
251
+ order = Order(
252
+ exchange=self._exchange_id,
253
+ symbol=symbol,
254
+ oid=oid,
255
+ eid=eid,
256
+ status=OrderStatus.CANCELING,
257
+ amount=tmp_order.amount,
258
+ side=tmp_order.side,
259
+ timestamp=ts,
260
+ type=tmp_order.type,
261
+ price=tmp_order.price,
262
+ time_in_force=tmp_order.time_in_force,
263
+ reduce_only=tmp_order.reduce_only,
264
+ )
265
+ self.order_status_update(order)
266
+ else:
267
+ assert msg.error is not None
268
+ self._log.error(
269
+ f"[{tmp_order.symbol}] canceling order failed: oid: {oid} {msg.error.format_str}"
270
+ )
271
+ order = Order(
272
+ exchange=self._exchange_id,
273
+ symbol=tmp_order.symbol,
274
+ oid=oid,
275
+ status=OrderStatus.CANCEL_FAILED,
276
+ amount=tmp_order.amount,
277
+ type=tmp_order.type,
278
+ side=tmp_order.side,
279
+ timestamp=ts,
280
+ price=tmp_order.price,
281
+ time_in_force=tmp_order.time_in_force,
282
+ reduce_only=tmp_order.reduce_only,
283
+ reason=msg.error.format_str,
284
+ )
285
+ self.order_status_update(order) # SOME STATUS -> FAILED
286
+
287
+ except msgspec.DecodeError as e:
288
+ self._log.error(f"Error decoding WebSocket API message: {str(raw)} {e}")
289
+
290
+ def _ws_spot_msg_handler(self, raw: bytes):
291
+ try:
292
+ msg = self._ws_spot_msg_general_decoder.decode(raw)
293
+ if msg.event:
294
+ match msg.event.e:
295
+ case (
296
+ BinanceUserDataStreamWsEventType.EXECUTION_REPORT
297
+ ): # spot order update
298
+ self._parse_execution_report(raw)
299
+ case (
300
+ BinanceUserDataStreamWsEventType.OUT_BOUND_ACCOUNT_POSITION
301
+ ): # spot account update
302
+ self._parse_out_bound_account_position(raw)
303
+
304
+ except msgspec.DecodeError as e:
305
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
306
+
307
+ def _ws_msg_handler(self, raw: bytes):
308
+ try:
309
+ msg = self._ws_msg_general_decoder.decode(raw)
310
+ if msg.e:
311
+ match msg.e:
312
+ case (
313
+ BinanceUserDataStreamWsEventType.ORDER_TRADE_UPDATE
314
+ ): # futures order update
315
+ self._parse_order_trade_update(raw)
316
+ case (
317
+ BinanceUserDataStreamWsEventType.EXECUTION_REPORT
318
+ ): # spot order update
319
+ self._parse_pm_execution_report(raw)
320
+ case (
321
+ BinanceUserDataStreamWsEventType.ACCOUNT_UPDATE
322
+ ): # futures account update
323
+ self._parse_account_update(raw)
324
+ case (
325
+ BinanceUserDataStreamWsEventType.OUT_BOUND_ACCOUNT_POSITION
326
+ ): # spot account update
327
+ self._parse_pm_out_bound_account_position(raw)
328
+ case BinanceUserDataStreamWsEventType.LISTEN_KEY_EXPIRED:
329
+ res = self._ws_msg_general_decoder.decode(raw)
330
+ self._log.warning(f"Listen key expired: {res}")
331
+ except msgspec.DecodeError as e:
332
+ self._log.error(f"Error decoding message: {str(raw)} {e}")
333
+
334
+ def _parse_order_trade_update(self, raw: bytes):
335
+ res = self._ws_msg_futures_order_update_decoder.decode(raw)
336
+ self._log.debug(f"Order trade update: {res}")
337
+
338
+ event_data = res.o
339
+ event_unit = res.fs
340
+
341
+ # Only portfolio margin has "UM" and "CM" event business unit
342
+ if event_unit == BinanceBusinessUnit.UM:
343
+ id = event_data.s + "_linear"
344
+ symbol = self._market_id[id]
345
+ elif event_unit == BinanceBusinessUnit.CM:
346
+ id = event_data.s + "_inverse"
347
+ symbol = self._market_id[id]
348
+ else:
349
+ assert self.market_type is not None
350
+ id = event_data.s + self.market_type
351
+ symbol = self._market_id[id]
352
+
353
+ # we use the last filled quantity to calculate the cost, instead of the accumulated filled quantity
354
+ type = event_data.o
355
+ if type.is_market:
356
+ cost = Decimal(event_data.l) * Decimal(event_data.ap)
357
+ cum_cost = Decimal(event_data.z) * Decimal(event_data.ap)
358
+ elif type.is_limit:
359
+ price = Decimal(event_data.ap) or Decimal(
360
+ event_data.p
361
+ ) # if average price is 0 or empty, use price
362
+ cost = Decimal(event_data.l) * price
363
+ cum_cost = Decimal(event_data.z) * price
364
+
365
+ order = Order(
366
+ exchange=self._exchange_id,
367
+ symbol=symbol,
368
+ status=BinanceEnumParser.parse_order_status(event_data.X),
369
+ eid=str(event_data.i),
370
+ oid=event_data.c,
371
+ amount=Decimal(event_data.q),
372
+ filled=Decimal(event_data.z),
373
+ timestamp=res.E,
374
+ type=BinanceEnumParser.parse_futures_order_type(event_data.o, event_data.f),
375
+ side=BinanceEnumParser.parse_order_side(event_data.S),
376
+ time_in_force=BinanceEnumParser.parse_time_in_force(event_data.f),
377
+ price=float(event_data.p),
378
+ average=float(event_data.ap),
379
+ last_filled_price=float(event_data.L),
380
+ last_filled=Decimal(event_data.l),
381
+ remaining=Decimal(event_data.q) - Decimal(event_data.z),
382
+ fee=Decimal(event_data.n) if event_data.n else None,
383
+ fee_currency=event_data.N,
384
+ cum_cost=cum_cost,
385
+ cost=cost,
386
+ reduce_only=event_data.R,
387
+ position_side=BinanceEnumParser.parse_position_side(event_data.ps),
388
+ )
389
+
390
+ self.order_status_update(order)
391
+
392
+ def _parse_execution_report(self, raw: bytes) -> None:
393
+ data = self._ws_msg_spot_order_update_decoder.decode(raw)
394
+ event_data = data.event
395
+ self._log.debug(f"Execution report: {event_data}")
396
+
397
+ market_type = self.market_type or "_spot"
398
+ id = event_data.s + market_type
399
+ symbol = self._market_id[id]
400
+
401
+ # Calculate average price only if filled amount is non-zero
402
+ average = (
403
+ float(event_data.Z) / float(event_data.z)
404
+ if float(event_data.z) != 0
405
+ else None
406
+ )
407
+
408
+ order = Order(
409
+ exchange=self._exchange_id,
410
+ symbol=symbol,
411
+ status=BinanceEnumParser.parse_order_status(event_data.X),
412
+ eid=str(event_data.i),
413
+ oid=event_data.c,
414
+ amount=Decimal(event_data.q),
415
+ filled=Decimal(event_data.z),
416
+ timestamp=event_data.E,
417
+ type=BinanceEnumParser.parse_spot_order_type(event_data.o),
418
+ side=BinanceEnumParser.parse_order_side(event_data.S),
419
+ time_in_force=BinanceEnumParser.parse_time_in_force(event_data.f),
420
+ price=float(event_data.p),
421
+ average=average,
422
+ last_filled_price=float(event_data.L),
423
+ last_filled=Decimal(event_data.l),
424
+ remaining=Decimal(event_data.q) - Decimal(event_data.z),
425
+ fee=Decimal(event_data.n) if event_data.n else None,
426
+ fee_currency=event_data.N,
427
+ cum_cost=Decimal(event_data.Z),
428
+ cost=Decimal(event_data.Y),
429
+ )
430
+
431
+ self.order_status_update(order)
432
+
433
+ def _parse_pm_execution_report(self, raw: bytes) -> None:
434
+ event_data = self._ws_msg_pm_margin_order_update_decoder.decode(raw)
435
+ self._log.debug(f"Execution report: {event_data}")
436
+
437
+ market_type = self.market_type or "_spot"
438
+ id = event_data.s + market_type
439
+ symbol = self._market_id[id]
440
+
441
+ # Calculate average price only if filled amount is non-zero
442
+ average = (
443
+ float(event_data.Z) / float(event_data.z)
444
+ if float(event_data.z) != 0
445
+ else None
446
+ )
447
+
448
+ order = Order(
449
+ exchange=self._exchange_id,
450
+ symbol=symbol,
451
+ status=BinanceEnumParser.parse_order_status(event_data.X),
452
+ eid=str(event_data.i),
453
+ oid=event_data.c,
454
+ amount=Decimal(event_data.q),
455
+ filled=Decimal(event_data.z),
456
+ timestamp=event_data.E,
457
+ type=BinanceEnumParser.parse_spot_order_type(event_data.o),
458
+ side=BinanceEnumParser.parse_order_side(event_data.S),
459
+ time_in_force=BinanceEnumParser.parse_time_in_force(event_data.f),
460
+ price=float(event_data.p),
461
+ average=average,
462
+ last_filled_price=float(event_data.L),
463
+ last_filled=Decimal(event_data.l),
464
+ remaining=Decimal(event_data.q) - Decimal(event_data.z),
465
+ fee=Decimal(event_data.n) if event_data.n else None,
466
+ fee_currency=event_data.N,
467
+ cum_cost=Decimal(event_data.Z),
468
+ cost=Decimal(event_data.Y),
469
+ )
470
+
471
+ self.order_status_update(order)
472
+
473
+ def _parse_account_update(self, raw: bytes):
474
+ res = self._ws_msg_futures_account_update_decoder.decode(raw)
475
+ self._log.debug(f"Account update: {res}")
476
+
477
+ balances = res.parse_to_balances()
478
+ self._cache._apply_balance(account_type=self._account_type, balances=balances)
479
+
480
+ event_unit = res.fs
481
+ for position in res.a.P:
482
+ if event_unit == BinanceBusinessUnit.UM:
483
+ id = position.s + "_linear"
484
+ symbol = self._market_id[id]
485
+ elif event_unit == BinanceBusinessUnit.CM:
486
+ id = position.s + "_inverse"
487
+ symbol = self._market_id[id]
488
+ else:
489
+ assert self.market_type is not None
490
+ id = position.s + self.market_type
491
+ symbol = self._market_id[id]
492
+
493
+ signed_amount = Decimal(position.pa)
494
+ side = position.ps.parse_to_position_side()
495
+ if signed_amount == 0:
496
+ side = None # 0 means no position side
497
+ else:
498
+ if side == PositionSide.FLAT:
499
+ if signed_amount > 0:
500
+ side = PositionSide.LONG
501
+ elif signed_amount < 0:
502
+ side = PositionSide.SHORT
503
+ position = Position(
504
+ symbol=symbol,
505
+ exchange=self._exchange_id,
506
+ signed_amount=signed_amount,
507
+ side=side,
508
+ entry_price=float(position.ep),
509
+ unrealized_pnl=float(position.up),
510
+ realized_pnl=float(position.cr),
511
+ )
512
+ self._cache._apply_position(position)
513
+
514
+ def _parse_out_bound_account_position(self, raw: bytes):
515
+ data = self._ws_msg_spot_account_update_decoder.decode(raw)
516
+ res = data.event
517
+ self._log.debug(f"Out bound account position: {res}")
518
+
519
+ balances = res.parse_to_balances()
520
+ self._cache._apply_balance(account_type=self._account_type, balances=balances)
521
+
522
+ def _parse_pm_out_bound_account_position(self, raw: bytes):
523
+ res = self._ws_msg_pm_margin_account_update_decoder.decode(raw)
524
+ self._log.debug(f"Out bound account position: {res}")
525
+
526
+ balances = res.pm_parse_to_balances()
527
+ self._cache._apply_balance(account_type=self._account_type, balances=balances)
528
+
529
+ async def _execute_order_request(
530
+ self, market: BinanceMarket, symbol: str, params: Dict[str, Any]
531
+ ):
532
+ """Execute order request based on account type and market.
533
+
534
+ Args:
535
+ market: BinanceMarket object
536
+ symbol: Trading symbol
537
+ params: Order parameters
538
+
539
+ Returns:
540
+ API response
541
+
542
+ Raises:
543
+ ValueError: If market type is not supported for the account type
544
+ """
545
+ if self._account_type.is_spot:
546
+ if not market.spot:
547
+ raise ValueError(
548
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
549
+ )
550
+ return await self._api_client.post_api_v3_order(**params)
551
+ elif self._account_type.is_isolated_margin_or_margin:
552
+ if not market.margin:
553
+ raise ValueError(
554
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
555
+ )
556
+ return await self._api_client.post_sapi_v1_margin_order(**params)
557
+ elif self._account_type.is_linear:
558
+ if not market.linear:
559
+ raise ValueError(
560
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
561
+ )
562
+ return await self._api_client.post_fapi_v1_order(**params)
563
+ elif self._account_type.is_inverse:
564
+ if not market.inverse:
565
+ raise ValueError(
566
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
567
+ )
568
+ return await self._api_client.post_dapi_v1_order(**params)
569
+ elif self._account_type.is_portfolio_margin:
570
+ if market.margin:
571
+ return await self._api_client.post_papi_v1_margin_order(**params)
572
+ elif market.linear:
573
+ return await self._api_client.post_papi_v1_um_order(**params)
574
+ elif market.inverse:
575
+ return await self._api_client.post_papi_v1_cm_order(**params)
576
+ else:
577
+ raise ValueError(
578
+ f"Unsupported market type for portfolio margin order: {symbol}"
579
+ )
580
+ else:
581
+ raise ValueError(
582
+ f"Unsupported account type for create order: {self._account_type}"
583
+ )
584
+
585
+ async def _execute_modify_order_request(
586
+ self, market: BinanceMarket, symbol: str, params: Dict[str, Any]
587
+ ):
588
+ if self._account_type.is_spot_or_margin:
589
+ raise ValueError(
590
+ "Modify order is not supported for `spot` or `margin` account"
591
+ )
592
+
593
+ elif self._account_type.is_linear:
594
+ return await self._api_client.put_fapi_v1_order(**params)
595
+ elif self._account_type.is_inverse:
596
+ return await self._api_client.put_dapi_v1_order(**params)
597
+ elif self._account_type.is_portfolio_margin:
598
+ if market.inverse:
599
+ return await self._api_client.put_papi_v1_cm_order(**params)
600
+ elif market.linear:
601
+ return await self._api_client.put_papi_v1_um_order(**params)
602
+ else:
603
+ raise ValueError(f"Modify order is not supported for {symbol}")
604
+ else:
605
+ raise ValueError(
606
+ f"Unsupported account type for modify order: {self._account_type}"
607
+ )
608
+
609
+ async def _execute_cancel_order_request(
610
+ self, market: BinanceMarket, symbol: str, params: Dict[str, Any]
611
+ ):
612
+ if self._account_type.is_spot:
613
+ if not market.spot:
614
+ raise ValueError(
615
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
616
+ )
617
+ return await self._api_client.delete_api_v3_order(**params)
618
+ elif self._account_type.is_isolated_margin_or_margin:
619
+ if not market.margin:
620
+ raise ValueError(
621
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
622
+ )
623
+ return await self._api_client.delete_sapi_v1_margin_order(**params)
624
+ elif self._account_type.is_linear:
625
+ if not market.linear:
626
+ raise ValueError(
627
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
628
+ )
629
+ return await self._api_client.delete_fapi_v1_order(**params)
630
+ elif self._account_type.is_inverse:
631
+ if not market.inverse:
632
+ raise ValueError(
633
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
634
+ )
635
+ return await self._api_client.delete_dapi_v1_order(**params)
636
+ elif self._account_type.is_portfolio_margin:
637
+ if market.margin:
638
+ return await self._api_client.delete_papi_v1_margin_order(**params)
639
+ elif market.linear:
640
+ return await self._api_client.delete_papi_v1_um_order(**params)
641
+ elif market.inverse:
642
+ return await self._api_client.delete_papi_v1_cm_order(**params)
643
+ else:
644
+ raise ValueError(
645
+ f"Unsupported market type for portfolio margin cancel: {symbol}"
646
+ )
647
+ else:
648
+ raise ValueError(
649
+ f"Unsupported account type for cancel order: {self._account_type}"
650
+ )
651
+
652
+ async def _execute_query_order_request(
653
+ self, market: BinanceMarket, symbol: str, params: Dict[str, Any]
654
+ ):
655
+ if self._account_type.is_spot:
656
+ if not market.spot:
657
+ raise ValueError(
658
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
659
+ )
660
+ return await self._api_client.get_api_v3_order(**params)
661
+ elif self._account_type.is_isolated_margin_or_margin:
662
+ if not market.margin:
663
+ raise ValueError(
664
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
665
+ )
666
+ return await self._api_client.get_sapi_v1_margin_order(**params)
667
+ elif self._account_type.is_linear:
668
+ if not market.linear:
669
+ raise ValueError(
670
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
671
+ )
672
+ return await self._api_client.get_fapi_v1_order(**params)
673
+ elif self._account_type.is_inverse:
674
+ if not market.inverse:
675
+ raise ValueError(
676
+ f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
677
+ )
678
+ return await self._api_client.get_dapi_v1_order(**params)
679
+ elif self._account_type.is_portfolio_margin:
680
+ if market.spot:
681
+ return await self._api_client.get_papi_v1_margin_order(**params)
682
+ elif market.linear:
683
+ return await self._api_client.get_papi_v1_um_order(**params)
684
+ elif market.inverse:
685
+ return await self._api_client.get_papi_v1_cm_order(**params)
686
+ else:
687
+ raise ValueError(
688
+ f"Unsupported market type for portfolio margin query: {symbol}"
689
+ )
690
+ else:
691
+ raise ValueError(
692
+ f"Unsupported account type for query order: {self._account_type}"
693
+ )
694
+
695
+ def _parse_query_order(
696
+ self,
697
+ *,
698
+ res: BinanceOrder,
699
+ market: BinanceMarket,
700
+ symbol: str,
701
+ fallback_oid: str,
702
+ ) -> Order | None:
703
+ if res.status is None:
704
+ return None
705
+
706
+ if market.spot:
707
+ type = BinanceEnumParser.parse_spot_order_type(res.type)
708
+ else:
709
+ type = BinanceEnumParser.parse_futures_order_type(res.type, res.timeInForce)
710
+
711
+ amount = Decimal(res.origQty)
712
+ filled = Decimal(res.executedQty) if res.executedQty else Decimal(0)
713
+ remaining = amount - filled
714
+
715
+ return Order(
716
+ exchange=self._exchange_id,
717
+ symbol=symbol,
718
+ status=BinanceEnumParser.parse_order_status(res.status),
719
+ eid=str(res.orderId),
720
+ oid=res.clientOrderId or fallback_oid,
721
+ amount=amount,
722
+ filled=filled,
723
+ timestamp=res.updateTime or res.time or self._clock.timestamp_ms(),
724
+ type=type,
725
+ side=BinanceEnumParser.parse_order_side(res.side) if res.side else None,
726
+ time_in_force=BinanceEnumParser.parse_time_in_force(res.timeInForce)
727
+ if res.timeInForce
728
+ else None,
729
+ price=float(res.price) if res.price else None,
730
+ average=float(res.avgPrice) if res.avgPrice else None,
731
+ remaining=remaining,
732
+ reduce_only=res.reduceOnly,
733
+ position_side=BinanceEnumParser.parse_position_side(res.positionSide)
734
+ if res.positionSide
735
+ else None,
736
+ )
737
+
738
+ async def _execute_cancel_all_orders_request(
739
+ self, market: BinanceMarket, params: Dict[str, Any]
740
+ ):
741
+ res = {}
742
+ if self._account_type.is_spot:
743
+ res = await self._api_client.delete_api_v3_open_orders(**params)
744
+ elif self._account_type.is_isolated_margin_or_margin:
745
+ res = await self._api_client.delete_sapi_v1_margin_open_orders(**params)
746
+ elif self._account_type.is_linear:
747
+ res = await self._api_client.delete_fapi_v1_all_open_orders(**params)
748
+ elif self._account_type.is_inverse:
749
+ res = await self._api_client.delete_dapi_v1_all_open_orders(**params)
750
+ elif self._account_type.is_portfolio_margin:
751
+ if market.margin:
752
+ res = await self._api_client.delete_papi_v1_margin_all_open_orders(
753
+ **params
754
+ )
755
+ elif market.linear:
756
+ res = await self._api_client.delete_papi_v1_um_all_open_orders(**params)
757
+ elif market.inverse:
758
+ res = await self._api_client.delete_papi_v1_cm_all_open_orders(**params)
759
+
760
+ if isinstance(res, list):
761
+ return # spot and margin return a list of canceled orders
762
+
763
+ if not (code := int(res.get("code", 0))) == 200:
764
+ msg = res.get("msg", "Unknown error")
765
+ raise ValueError(f"Cancel all orders failed: {code} {msg}")
766
+
767
+ async def _execute_batch_order_request(self, batch_orders: list[Dict[str, Any]]):
768
+ if self._account_type.is_linear:
769
+ return await self._api_client.post_fapi_v1_batch_orders(
770
+ batch_orders=batch_orders
771
+ )
772
+ elif self._account_type.is_inverse:
773
+ return await self._api_client.post_dapi_v1_batch_orders(
774
+ batch_orders=batch_orders
775
+ )
776
+ else:
777
+ raise ValueError(
778
+ f"Batch order is not supported for {self._account_type.value} account type"
779
+ )
780
+
781
+ async def _execute_order_request_ws(
782
+ self, oid: str, market: BinanceMarket, symbol: str, params: Dict[str, Any]
783
+ ):
784
+ if self._ws_api_client is None:
785
+ raise ValueError(
786
+ f"`create_order_ws` is not supported for {self._account_type.value} account type"
787
+ )
788
+
789
+ if market.spot:
790
+ await self._ws_api_client.spot_new_order(oid=oid, **params)
791
+ elif market.linear:
792
+ await self._ws_api_client.usdm_new_order(oid=oid, **params)
793
+ else:
794
+ await self._ws_api_client.coinm_new_order(oid=oid, **params)
795
+
796
+ async def create_order_ws(
797
+ self,
798
+ oid: str,
799
+ symbol: str,
800
+ side: OrderSide,
801
+ type: OrderType,
802
+ amount: Decimal,
803
+ price: Decimal | None = None,
804
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
805
+ reduce_only: bool = False,
806
+ **kwargs,
807
+ ) -> None:
808
+ self._registry.register_tmp_order(
809
+ order=Order(
810
+ oid=oid,
811
+ exchange=self._exchange_id,
812
+ symbol=symbol,
813
+ status=OrderStatus.INITIALIZED,
814
+ amount=amount,
815
+ type=type,
816
+ side=side,
817
+ price=float(price) if price else None,
818
+ time_in_force=time_in_force,
819
+ timestamp=self._clock.timestamp_ms(),
820
+ reduce_only=reduce_only,
821
+ )
822
+ )
823
+ market = self._market.get(symbol)
824
+ if not market:
825
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
826
+ id = market.id
827
+
828
+ params = {
829
+ "symbol": id,
830
+ "newClientOrderId": oid,
831
+ "side": BinanceEnumParser.to_binance_order_side(side).value,
832
+ "quantity": amount,
833
+ }
834
+
835
+ if type.is_post_only:
836
+ if market.spot:
837
+ params["type"] = BinanceOrderType.LIMIT_MAKER.value
838
+ else:
839
+ params["type"] = BinanceOrderType.LIMIT.value
840
+ params["timeInForce"] = (
841
+ BinanceTimeInForce.GTX.value
842
+ ) # for future, you need to set ordertype to LIMIT and timeinforce to GTX to place a post only order
843
+ else:
844
+ params["type"] = BinanceEnumParser.to_binance_order_type(type).value
845
+
846
+ if type.is_limit or type.is_post_only:
847
+ if not price:
848
+ raise ValueError("Price is required for order")
849
+ params["price"] = price
850
+
851
+ if type.is_limit:
852
+ if time_in_force is None:
853
+ raise ValueError("time_in_force is required for limit orders")
854
+ params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
855
+ time_in_force
856
+ ).value
857
+
858
+ if reduce_only:
859
+ params["reduceOnly"] = "true"
860
+
861
+ params.update(kwargs)
862
+ try:
863
+ await self._execute_order_request_ws(
864
+ oid=oid, market=market, symbol=symbol, params=params
865
+ )
866
+ except BinanceRateLimitError as e:
867
+ order = self._rate_limit_failed_order(
868
+ oid=oid,
869
+ symbol=symbol,
870
+ side=side,
871
+ type=type,
872
+ amount=amount,
873
+ price=price,
874
+ time_in_force=time_in_force,
875
+ reduce_only=reduce_only,
876
+ exc=e,
877
+ )
878
+ self.order_status_update(order)
879
+
880
+ async def create_order(
881
+ self,
882
+ oid: str,
883
+ symbol: str,
884
+ side: OrderSide,
885
+ type: OrderType,
886
+ amount: Decimal,
887
+ price: Decimal | None = None,
888
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
889
+ reduce_only: bool = False,
890
+ **kwargs,
891
+ ) -> Order:
892
+ market = self._market.get(symbol)
893
+ if not market:
894
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
895
+ id = market.id
896
+
897
+ params = {
898
+ "symbol": id,
899
+ "newClientOrderId": oid,
900
+ "side": BinanceEnumParser.to_binance_order_side(side).value,
901
+ "quantity": str(amount),
902
+ }
903
+
904
+ if type.is_post_only:
905
+ if market.spot:
906
+ params["type"] = BinanceOrderType.LIMIT_MAKER.value
907
+ else:
908
+ params["type"] = BinanceOrderType.LIMIT.value
909
+ params["timeInForce"] = (
910
+ BinanceTimeInForce.GTX.value
911
+ ) # for future, you need to set ordertype to LIMIT and timeinforce to GTX to place a post only order
912
+ else:
913
+ params["type"] = BinanceEnumParser.to_binance_order_type(type).value
914
+
915
+ if type.is_limit or type.is_post_only:
916
+ if not price:
917
+ raise ValueError("Price is required for order")
918
+ params["price"] = str(price)
919
+
920
+ if type.is_limit:
921
+ if time_in_force is None:
922
+ raise ValueError("time_in_force is required for limit orders")
923
+ params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
924
+ time_in_force
925
+ ).value
926
+
927
+ if reduce_only:
928
+ params["reduceOnly"] = "true"
929
+
930
+ params.update(kwargs)
931
+
932
+ try:
933
+ res = await self._execute_order_request(market, symbol, params)
934
+ order = Order(
935
+ oid=oid,
936
+ eid=str(res.orderId),
937
+ exchange=self._exchange_id,
938
+ symbol=symbol,
939
+ status=OrderStatus.PENDING,
940
+ amount=amount,
941
+ filled=Decimal(0),
942
+ timestamp=res.updateTime,
943
+ type=type,
944
+ side=side,
945
+ time_in_force=time_in_force,
946
+ price=float(res.price) if res.price else None,
947
+ average=float(res.avgPrice) if res.avgPrice else None,
948
+ remaining=amount,
949
+ reduce_only=reduce_only,
950
+ )
951
+ except Exception as e:
952
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
953
+ self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
954
+ order = Order(
955
+ oid=oid,
956
+ exchange=self._exchange_id,
957
+ timestamp=self._clock.timestamp_ms(),
958
+ symbol=symbol,
959
+ type=type,
960
+ side=side,
961
+ amount=amount,
962
+ price=float(price) if price else None,
963
+ time_in_force=time_in_force,
964
+ status=OrderStatus.FAILED,
965
+ filled=Decimal(0),
966
+ remaining=amount,
967
+ reduce_only=reduce_only,
968
+ reason=error_msg,
969
+ )
970
+ self.order_status_update(order)
971
+ return order
972
+
973
+ async def _execute_cancel_order_request_ws(
974
+ self, oid: str, market: BinanceMarket, params: Dict[str, Any]
975
+ ):
976
+ if self._ws_api_client is None:
977
+ raise ValueError(
978
+ f"`create_order_ws` is not supported for {self._account_type.value} account type"
979
+ )
980
+
981
+ if market.spot:
982
+ await self._ws_api_client.spot_cancel_order(oid=oid, **params)
983
+ elif market.linear:
984
+ await self._ws_api_client.usdm_cancel_order(oid=oid, **params)
985
+ else:
986
+ await self._ws_api_client.coinm_cancel_order(oid=oid, **params)
987
+
988
+ async def cancel_order_ws(self, oid: str, symbol: str, **kwargs) -> None:
989
+ market = self._market.get(symbol)
990
+ if not market:
991
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
992
+ id = market.id
993
+ params = {
994
+ "symbol": id,
995
+ "origClientOrderId": oid,
996
+ **kwargs,
997
+ }
998
+ await self._execute_cancel_order_request_ws(oid, market, params)
999
+
1000
+ async def cancel_order(self, oid: str, symbol: str, **kwargs) -> Order:
1001
+ try:
1002
+ market = self._market.get(symbol)
1003
+ if not market:
1004
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1005
+ id = market.id
1006
+
1007
+ params = {
1008
+ "symbol": id,
1009
+ "origClientOrderId": oid,
1010
+ **kwargs,
1011
+ }
1012
+ if not market.linear or not market.inverse:
1013
+ params["newClientOrderId"] = oid
1014
+
1015
+ res = await self._execute_cancel_order_request(market, symbol, params)
1016
+
1017
+ if market.spot:
1018
+ type = (
1019
+ BinanceEnumParser.parse_spot_order_type(res.type)
1020
+ if res.type
1021
+ else None
1022
+ )
1023
+ else:
1024
+ type = (
1025
+ BinanceEnumParser.parse_futures_order_type(
1026
+ res.type, res.timeInForce
1027
+ )
1028
+ if res.type
1029
+ else None
1030
+ )
1031
+
1032
+ order = Order(
1033
+ exchange=self._exchange_id,
1034
+ symbol=symbol,
1035
+ status=OrderStatus.CANCELING,
1036
+ eid=str(res.orderId),
1037
+ oid=res.clientOrderId,
1038
+ amount=Decimal(res.origQty) if res.origQty else None,
1039
+ filled=Decimal(res.executedQty) if res.executedQty else Decimal(0),
1040
+ timestamp=res.updateTime,
1041
+ type=type,
1042
+ side=BinanceEnumParser.parse_order_side(res.side) if res.side else None,
1043
+ time_in_force=BinanceEnumParser.parse_time_in_force(res.timeInForce)
1044
+ if res.timeInForce
1045
+ else None,
1046
+ price=float(res.price) if res.price else None,
1047
+ average=float(res.avgPrice) if res.avgPrice else None,
1048
+ remaining=(Decimal(res.origQty) - Decimal(res.executedQty))
1049
+ if res.origQty and res.executedQty
1050
+ else None,
1051
+ reduce_only=res.reduceOnly,
1052
+ position_side=BinanceEnumParser.parse_position_side(res.positionSide)
1053
+ if res.positionSide
1054
+ else None,
1055
+ )
1056
+ except BinanceRateLimitError as e:
1057
+ error_msg = f"rate_limit (retry_after={e.retry_after:.1f}s, type={e.rate_limit_type}): {str(e)}"
1058
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
1059
+ order = Order(
1060
+ exchange=self._exchange_id,
1061
+ timestamp=self._clock.timestamp_ms(),
1062
+ symbol=symbol,
1063
+ oid=oid,
1064
+ status=OrderStatus.CANCEL_FAILED,
1065
+ reason=error_msg,
1066
+ )
1067
+ except (BinanceClientError, BinanceServerError) as e:
1068
+ error_msg = str(e)
1069
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
1070
+ order = Order(
1071
+ exchange=self._exchange_id,
1072
+ timestamp=self._clock.timestamp_ms(),
1073
+ symbol=symbol,
1074
+ oid=oid,
1075
+ status=OrderStatus.CANCEL_FAILED,
1076
+ reason=error_msg,
1077
+ )
1078
+ except Exception as e:
1079
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1080
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
1081
+ order = Order(
1082
+ exchange=self._exchange_id,
1083
+ timestamp=self._clock.timestamp_ms(),
1084
+ symbol=symbol,
1085
+ oid=oid,
1086
+ status=OrderStatus.CANCEL_FAILED,
1087
+ reason=error_msg,
1088
+ )
1089
+ self.order_status_update(order)
1090
+ return order
1091
+
1092
+ async def query_order(self, oid: str, symbol: str) -> Order | None:
1093
+ market = self._market.get(symbol)
1094
+ if not market:
1095
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1096
+
1097
+ params = {
1098
+ "symbol": market.id,
1099
+ "origClientOrderId": oid,
1100
+ }
1101
+ try:
1102
+ res = await self._execute_query_order_request(market, symbol, params)
1103
+ return self._parse_query_order(
1104
+ res=res,
1105
+ market=market,
1106
+ symbol=symbol,
1107
+ fallback_oid=oid,
1108
+ )
1109
+ except BinanceClientError as e:
1110
+ if e.code in {-2013, -2026}:
1111
+ return Order(
1112
+ exchange=self._exchange_id,
1113
+ timestamp=self._clock.timestamp_ms(),
1114
+ symbol=symbol,
1115
+ oid=oid,
1116
+ status=OrderStatus.CANCELED,
1117
+ reason=str(e),
1118
+ )
1119
+ error_msg = str(e)
1120
+ self._log.error(f"Error querying order: {error_msg} params: {params}")
1121
+ return None
1122
+ except (BinanceRateLimitError, BinanceServerError) as e:
1123
+ error_msg = str(e)
1124
+ self._log.error(f"Error querying order: {error_msg} params: {params}")
1125
+ return None
1126
+ except Exception as e:
1127
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1128
+ self._log.error(f"Error querying order: {error_msg} params: {params}")
1129
+ return None
1130
+
1131
+ async def cancel_all_orders(self, symbol: str) -> bool:
1132
+ try:
1133
+ market = self._market.get(symbol)
1134
+ if not market:
1135
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1136
+ symbol = market.id
1137
+
1138
+ params = {
1139
+ "symbol": symbol,
1140
+ }
1141
+ await self._execute_cancel_all_orders_request(market, params)
1142
+ return True
1143
+ except Exception as e:
1144
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1145
+ self._log.error(
1146
+ f"Error canceling all orders: {error_msg} params: {str(params)}"
1147
+ )
1148
+ return False
1149
+
1150
+ async def modify_order(
1151
+ self,
1152
+ oid: str,
1153
+ symbol: str,
1154
+ side: OrderSide | None = None,
1155
+ price: Decimal | None = None,
1156
+ amount: Decimal | None = None,
1157
+ **kwargs,
1158
+ ) -> Order:
1159
+ market = self._market.get(symbol)
1160
+ if not market:
1161
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1162
+ id = market.id
1163
+
1164
+ if market.spot:
1165
+ raise ValueError(
1166
+ "Modify order is not supported for `spot` account type, please cancel and create a new order"
1167
+ )
1168
+ if side is None:
1169
+ raise ValueError("side is required to modify a Binance order")
1170
+
1171
+ params = {
1172
+ "symbol": id,
1173
+ "origClientOrderId": oid,
1174
+ "side": BinanceEnumParser.to_binance_order_side(side).value,
1175
+ "quantity": str(amount) if amount else None,
1176
+ "price": str(price) if price else None,
1177
+ **kwargs,
1178
+ }
1179
+
1180
+ try:
1181
+ res = await self._execute_modify_order_request(market, symbol, params)
1182
+ order = Order(
1183
+ exchange=self._exchange_id,
1184
+ symbol=symbol,
1185
+ status=OrderStatus.PENDING,
1186
+ eid=str(res.orderId),
1187
+ oid=oid,
1188
+ amount=amount,
1189
+ filled=Decimal(res.executedQty),
1190
+ timestamp=res.updateTime,
1191
+ type=BinanceEnumParser.parse_futures_order_type(
1192
+ res.type, res.timeInForce
1193
+ ),
1194
+ side=side,
1195
+ time_in_force=BinanceEnumParser.parse_time_in_force(res.timeInForce),
1196
+ price=float(res.price) if res.price else None,
1197
+ average=float(res.avgPrice) if res.avgPrice else None,
1198
+ remaining=Decimal(res.origQty) - Decimal(res.executedQty),
1199
+ reduce_only=res.reduceOnly,
1200
+ position_side=BinanceEnumParser.parse_position_side(res.positionSide)
1201
+ if res.positionSide
1202
+ else None,
1203
+ )
1204
+ except Exception as e:
1205
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1206
+ self._log.error(f"Error modifying order: {error_msg} params: {str(params)}")
1207
+ order = Order(
1208
+ exchange=self._exchange_id,
1209
+ timestamp=self._clock.timestamp_ms(),
1210
+ oid=oid,
1211
+ symbol=symbol,
1212
+ side=side,
1213
+ amount=amount,
1214
+ price=float(price) if price else None,
1215
+ status=OrderStatus.FAILED,
1216
+ filled=Decimal("0"),
1217
+ remaining=amount,
1218
+ reason=error_msg,
1219
+ )
1220
+ self.order_status_update(order)
1221
+ return order
1222
+
1223
+ async def create_tp_sl_order(
1224
+ self,
1225
+ oid: str,
1226
+ symbol: str,
1227
+ side: OrderSide,
1228
+ type: OrderType,
1229
+ amount: Decimal,
1230
+ price: Decimal | None = None,
1231
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
1232
+ tp_order_type: OrderType | None = None,
1233
+ tp_trigger_price: Decimal | None = None,
1234
+ tp_price: Decimal | None = None,
1235
+ tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
1236
+ sl_order_type: OrderType | None = None,
1237
+ sl_trigger_price: Decimal | None = None,
1238
+ sl_price: Decimal | None = None,
1239
+ sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
1240
+ **kwargs,
1241
+ ) -> Order:
1242
+ tasks = []
1243
+ tasks.append(
1244
+ self.create_order(
1245
+ oid=oid,
1246
+ symbol=symbol,
1247
+ side=side,
1248
+ type=type,
1249
+ amount=amount,
1250
+ price=price,
1251
+ time_in_force=time_in_force,
1252
+ **kwargs,
1253
+ )
1254
+ )
1255
+ tp_sl_side = OrderSide.SELL if side.is_buy else OrderSide.BUY
1256
+ effective_tp_trigger_type = tp_trigger_type or TriggerType.LAST_PRICE
1257
+ effective_sl_trigger_type = sl_trigger_type or TriggerType.LAST_PRICE
1258
+ if tp_order_type and tp_trigger_price:
1259
+ tasks.append(
1260
+ self._create_take_profit_order(
1261
+ symbol=symbol,
1262
+ side=tp_sl_side,
1263
+ type=tp_order_type,
1264
+ amount=amount,
1265
+ trigger_price=tp_trigger_price,
1266
+ price=tp_price,
1267
+ trigger_type=effective_tp_trigger_type,
1268
+ )
1269
+ )
1270
+ if sl_order_type and sl_trigger_price:
1271
+ tasks.append(
1272
+ self._create_stop_loss_order(
1273
+ symbol=symbol,
1274
+ side=tp_sl_side,
1275
+ type=sl_order_type,
1276
+ amount=amount,
1277
+ trigger_price=sl_trigger_price,
1278
+ price=sl_price,
1279
+ trigger_type=effective_sl_trigger_type,
1280
+ )
1281
+ )
1282
+ results = await asyncio.gather(*tasks)
1283
+ return results[0]
1284
+ # return res[0]
1285
+
1286
+ async def _create_stop_loss_order(
1287
+ self,
1288
+ symbol: str,
1289
+ side: OrderSide,
1290
+ type: OrderType,
1291
+ amount: Decimal,
1292
+ trigger_price: Decimal,
1293
+ trigger_type: TriggerType = TriggerType.LAST_PRICE,
1294
+ price: Decimal | None = None,
1295
+ time_in_force: TimeInForce = TimeInForce.GTC,
1296
+ position_side: PositionSide | None = None,
1297
+ **kwargs,
1298
+ ):
1299
+ market = self._market.get(symbol)
1300
+ if not market:
1301
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1302
+
1303
+ id = market.id
1304
+
1305
+ if market.inverse or market.linear:
1306
+ binance_type = (
1307
+ BinanceOrderType.STOP if type.is_limit else BinanceOrderType.STOP_MARKET
1308
+ )
1309
+ elif market.spot:
1310
+ binance_type = (
1311
+ BinanceOrderType.STOP_LOSS_LIMIT
1312
+ if type.is_limit
1313
+ else BinanceOrderType.STOP_LOSS
1314
+ )
1315
+ elif market.margin:
1316
+ # TODO: margin order is not supported yet
1317
+ pass
1318
+
1319
+ params = {
1320
+ "symbol": id,
1321
+ "side": BinanceEnumParser.to_binance_order_side(side).value,
1322
+ "type": binance_type.value,
1323
+ "quantity": str(amount),
1324
+ "stopPrice": trigger_price,
1325
+ "workingType": BinanceEnumParser.to_binance_trigger_type(
1326
+ trigger_type
1327
+ ).value,
1328
+ }
1329
+
1330
+ if type.is_limit:
1331
+ if price is None:
1332
+ raise ValueError("Price must be provided for limit stop loss orders")
1333
+
1334
+ params["price"] = str(price)
1335
+ params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
1336
+ time_in_force
1337
+ ).value
1338
+
1339
+ if position_side:
1340
+ params["positionSide"] = BinanceEnumParser.to_binance_position_side(
1341
+ position_side
1342
+ ).value
1343
+
1344
+ params.update(kwargs)
1345
+
1346
+ try:
1347
+ await self._execute_order_request(market, symbol, params)
1348
+ # order = Order(
1349
+ # exchange=self._exchange_id,
1350
+ # symbol=symbol,
1351
+ # status=OrderStatus.PENDING,
1352
+ #
1353
+ # id=str(res.orderId),
1354
+ # uuid=uuid,
1355
+ # amount=amount,
1356
+ # filled=Decimal(0),
1357
+ # client_order_id=res.clientOrderId,
1358
+ # timestamp=res.updateTime,
1359
+ # type=type,
1360
+ # side=side,
1361
+ # time_in_force=time_in_force,
1362
+ # price=float(res.price) if res.price else None,
1363
+ # average=float(res.avgPrice) if res.avgPrice else None,
1364
+ # trigger_price=float(res.stopPrice),
1365
+ # remaining=amount,
1366
+ # reduce_only=res.reduceOnly if res.reduceOnly else None,
1367
+ # position_side=BinanceEnumParser.parse_position_side(res.positionSide)
1368
+ # if res.positionSide
1369
+ # else None,
1370
+ # )
1371
+ # return order
1372
+ except Exception as e:
1373
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1374
+ self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
1375
+ # order = Order(
1376
+ # exchange=self._exchange_id,
1377
+ # timestamp=self._clock.timestamp_ms(),
1378
+ # symbol=symbol,
1379
+ # uuid=uuid,
1380
+ # type=type,
1381
+ # side=side,
1382
+ # amount=amount,
1383
+ # trigger_price=trigger_price,
1384
+ # price=float(price) if price else None,
1385
+ # time_in_force=time_in_force,
1386
+ # position_side=position_side,
1387
+ # status=OrderStatus.FAILED,
1388
+ #
1389
+ # filled=Decimal(0),
1390
+ # remaining=amount,
1391
+ # )
1392
+ # return order
1393
+
1394
+ async def _create_take_profit_order(
1395
+ self,
1396
+ symbol: str,
1397
+ side: OrderSide,
1398
+ type: OrderType,
1399
+ amount: Decimal,
1400
+ trigger_price: Decimal,
1401
+ trigger_type: TriggerType = TriggerType.LAST_PRICE,
1402
+ price: Decimal | None = None,
1403
+ time_in_force: TimeInForce = TimeInForce.GTC,
1404
+ position_side: PositionSide | None = None,
1405
+ **kwargs,
1406
+ ):
1407
+ market = self._market.get(symbol)
1408
+ if not market:
1409
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
1410
+
1411
+ id = market.id
1412
+
1413
+ if market.inverse or market.linear:
1414
+ binance_type = (
1415
+ BinanceOrderType.TAKE_PROFIT
1416
+ if type.is_limit
1417
+ else BinanceOrderType.TAKE_PROFIT_MARKET
1418
+ )
1419
+ elif market.spot:
1420
+ binance_type = (
1421
+ BinanceOrderType.TAKE_PROFIT_LIMIT
1422
+ if type.is_limit
1423
+ else BinanceOrderType.TAKE_PROFIT
1424
+ )
1425
+ elif market.margin:
1426
+ # TODO: margin order is not supported yet
1427
+ pass
1428
+
1429
+ params = {
1430
+ "symbol": id,
1431
+ "side": BinanceEnumParser.to_binance_order_side(side).value,
1432
+ "type": binance_type.value,
1433
+ "quantity": str(amount),
1434
+ "stopPrice": trigger_price,
1435
+ "workingType": BinanceEnumParser.to_binance_trigger_type(
1436
+ trigger_type
1437
+ ).value,
1438
+ }
1439
+
1440
+ if type.is_limit:
1441
+ if price is None:
1442
+ raise ValueError("Price must be provided for limit take profit orders")
1443
+
1444
+ params["price"] = str(price)
1445
+ params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
1446
+ time_in_force
1447
+ ).value
1448
+
1449
+ if position_side:
1450
+ params["positionSide"] = BinanceEnumParser.to_binance_position_side(
1451
+ position_side
1452
+ ).value
1453
+
1454
+ params.update(kwargs)
1455
+
1456
+ try:
1457
+ await self._execute_order_request(market, symbol, params)
1458
+ # order = Order(
1459
+ # exchange=self._exchange_id,
1460
+ # symbol=symbol,
1461
+ # status=OrderStatus.PENDING,
1462
+ #
1463
+ # eid=str(res.orderId),
1464
+ # amount=amount,
1465
+ # filled=Decimal(0),
1466
+ # client_order_id=res.clientOrderId,
1467
+ # timestamp=res.updateTime,
1468
+ # type=type,
1469
+ # side=side,
1470
+ # time_in_force=time_in_force,
1471
+ # price=float(res.price) if res.price else None,
1472
+ # average=float(res.avgPrice) if res.avgPrice else None,
1473
+ # trigger_price=float(res.stopPrice),
1474
+ # remaining=amount,
1475
+ # reduce_only=res.reduceOnly if res.reduceOnly else None,
1476
+ # position_side=BinanceEnumParser.parse_position_side(res.positionSide)
1477
+ # if res.positionSide
1478
+ # else None,
1479
+ # )
1480
+ # return order
1481
+ except Exception as e:
1482
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1483
+ self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
1484
+ # order = Order(
1485
+ # exchange=self._exchange_id,
1486
+ # timestamp=self._clock.timestamp_ms(),
1487
+ # symbol=symbol,
1488
+ # type=type,
1489
+ # side=side,
1490
+ # amount=amount,
1491
+ # trigger_price=trigger_price,
1492
+ # price=float(price) if price else None,
1493
+ # time_in_force=time_in_force,
1494
+ # position_side=position_side,
1495
+ # status=OrderStatus.FAILED,
1496
+ #
1497
+ # filled=Decimal(0),
1498
+ # remaining=amount,
1499
+ # )
1500
+ # return order
1501
+
1502
+ async def create_batch_orders(self, orders: list[BatchOrderSubmit]):
1503
+ if self._account_type.is_portfolio_margin:
1504
+ tasks = [
1505
+ self.create_order(
1506
+ oid=order.oid,
1507
+ symbol=order.symbol,
1508
+ side=order.side,
1509
+ amount=order.amount,
1510
+ price=order.price,
1511
+ type=order.type,
1512
+ time_in_force=order.time_in_force,
1513
+ reduce_only=order.reduce_only,
1514
+ **order.kwargs,
1515
+ )
1516
+ for order in orders
1517
+ ]
1518
+ await asyncio.gather(*tasks)
1519
+ else:
1520
+ batch_orders = []
1521
+ for order in orders:
1522
+ market = self._market.get(order.symbol)
1523
+ if not market:
1524
+ raise ValueError(
1525
+ f"Symbol {order.symbol} formated wrongly, or not supported"
1526
+ )
1527
+ id = market.id
1528
+
1529
+ params = {
1530
+ "symbol": id,
1531
+ "newClientOrderId": order.oid,
1532
+ "side": BinanceEnumParser.to_binance_order_side(order.side).value,
1533
+ "quantity": str(order.amount),
1534
+ }
1535
+
1536
+ if order.type.is_post_only:
1537
+ if market.spot:
1538
+ params["type"] = BinanceOrderType.LIMIT_MAKER.value
1539
+ else:
1540
+ params["type"] = BinanceOrderType.LIMIT.value
1541
+ params["timeInForce"] = BinanceTimeInForce.GTX.value
1542
+ else:
1543
+ params["type"] = BinanceEnumParser.to_binance_order_type(
1544
+ order.type
1545
+ ).value
1546
+
1547
+ if order.type.is_limit or order.type.is_post_only:
1548
+ if not order.price:
1549
+ raise ValueError("Price is required for limit order")
1550
+
1551
+ params["price"] = str(order.price)
1552
+
1553
+ if order.type.is_limit:
1554
+ params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
1555
+ order.time_in_force
1556
+ ).value
1557
+
1558
+ if order.reduce_only:
1559
+ params["reduceOnly"] = "true"
1560
+
1561
+ params.update(order.kwargs)
1562
+ batch_orders.append(params)
1563
+ try:
1564
+ res = await self._execute_batch_order_request(batch_orders)
1565
+ for order, res_order in zip(orders, res):
1566
+ if not res_order.code:
1567
+ res_batch_order = Order(
1568
+ exchange=self._exchange_id,
1569
+ symbol=order.symbol,
1570
+ status=OrderStatus.PENDING,
1571
+ eid=str(res_order.orderId),
1572
+ oid=order.oid,
1573
+ amount=order.amount,
1574
+ filled=Decimal(0),
1575
+ timestamp=res_order.updateTime,
1576
+ type=order.type,
1577
+ side=order.side,
1578
+ time_in_force=order.time_in_force,
1579
+ price=float(order.price) if order.price else None,
1580
+ average=float(res_order.avgPrice)
1581
+ if res_order.avgPrice
1582
+ else None,
1583
+ remaining=order.amount,
1584
+ reduce_only=order.reduce_only,
1585
+ position_side=BinanceEnumParser.parse_position_side(
1586
+ res_order.positionSide
1587
+ )
1588
+ if res_order.positionSide
1589
+ else None,
1590
+ )
1591
+ else:
1592
+ res_batch_order = Order(
1593
+ exchange=self._exchange_id,
1594
+ timestamp=self._clock.timestamp_ms(),
1595
+ oid=order.oid,
1596
+ symbol=order.symbol,
1597
+ type=order.type,
1598
+ side=order.side,
1599
+ amount=order.amount,
1600
+ price=float(order.price) if order.price else None,
1601
+ time_in_force=order.time_in_force,
1602
+ status=OrderStatus.FAILED,
1603
+ filled=Decimal(0),
1604
+ reduce_only=order.reduce_only,
1605
+ remaining=order.amount,
1606
+ reason=res_order.msg,
1607
+ )
1608
+ self._log.error(
1609
+ f"Failed to place order for {order.symbol}: {res_order.msg}: oid: {order.oid}"
1610
+ )
1611
+ self.order_status_update(res_batch_order)
1612
+ except Exception as e:
1613
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1614
+ self._log.error(f"Error placing batch orders: {error_msg}")
1615
+ for order in orders:
1616
+ res_batch_order = Order(
1617
+ exchange=self._exchange_id,
1618
+ timestamp=self._clock.timestamp_ms(),
1619
+ oid=order.oid,
1620
+ symbol=order.symbol,
1621
+ type=order.type,
1622
+ side=order.side,
1623
+ amount=order.amount,
1624
+ price=float(order.price) if order.price else None,
1625
+ time_in_force=order.time_in_force,
1626
+ status=OrderStatus.FAILED,
1627
+ filled=Decimal(0),
1628
+ remaining=order.amount,
1629
+ reason=error_msg,
1630
+ )
1631
+ self.order_status_update(res_batch_order)
1632
+
1633
+ async def _execute_cancel_batch_order_request(
1634
+ self, symbol_id: str, oids: list[str]
1635
+ ):
1636
+ if self._account_type.is_linear:
1637
+ return await self._api_client.delete_fapi_v1_batch_orders(symbol_id, oids)
1638
+ elif self._account_type.is_inverse:
1639
+ return await self._api_client.delete_dapi_v1_batch_orders(symbol_id, oids)
1640
+ else:
1641
+ raise ValueError(
1642
+ f"Batch cancel not supported for {self._account_type.value}"
1643
+ )
1644
+
1645
+ async def cancel_batch_orders(self, orders: List[CancelOrderSubmit]):
1646
+ from collections import defaultdict
1647
+
1648
+ groups = defaultdict(list)
1649
+ for order in orders:
1650
+ market = self._market.get(order.symbol)
1651
+ if not market:
1652
+ raise ValueError(f"Symbol {order.symbol} not found")
1653
+ groups[market.id].append(order)
1654
+
1655
+ for symbol_id, symbol_orders in groups.items():
1656
+ for i in range(0, len(symbol_orders), 10):
1657
+ chunk = symbol_orders[i : i + 10]
1658
+ oids = [o.oid for o in chunk]
1659
+ try:
1660
+ results = await self._execute_cancel_batch_order_request(
1661
+ symbol_id, oids
1662
+ )
1663
+ for res_order, submit in zip(results, chunk):
1664
+ if not res_order.code:
1665
+ order = Order(
1666
+ exchange=self._exchange_id,
1667
+ symbol=submit.symbol,
1668
+ oid=res_order.clientOrderId,
1669
+ eid=str(res_order.orderId),
1670
+ timestamp=res_order.updateTime,
1671
+ status=OrderStatus.CANCELING,
1672
+ )
1673
+ else:
1674
+ order = Order(
1675
+ exchange=self._exchange_id,
1676
+ symbol=submit.symbol,
1677
+ oid=submit.oid,
1678
+ timestamp=self._clock.timestamp_ms(),
1679
+ status=OrderStatus.CANCEL_FAILED,
1680
+ reason=res_order.msg,
1681
+ )
1682
+ self._log.error(
1683
+ f"Batch cancel failed for {submit.symbol} oid={submit.oid}: {res_order.msg}"
1684
+ )
1685
+ self.order_status_update(order)
1686
+ except Exception as e:
1687
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
1688
+ self._log.error(f"Error canceling batch orders: {error_msg}")
1689
+ for submit in chunk:
1690
+ order = Order(
1691
+ exchange=self._exchange_id,
1692
+ symbol=submit.symbol,
1693
+ oid=submit.oid,
1694
+ timestamp=self._clock.timestamp_ms(),
1695
+ status=OrderStatus.CANCEL_FAILED,
1696
+ reason=error_msg,
1697
+ )
1698
+ self.order_status_update(order)
1699
+
1700
+ def _apply_position(
1701
+ self,
1702
+ pos: BinanceFuturesPositionInfo | BinancePortfolioMarginPositionRisk,
1703
+ market_type: str | None = None,
1704
+ ):
1705
+ market_type = market_type or self.market_type
1706
+ assert market_type is not None
1707
+ id = pos.symbol + market_type
1708
+ symbol = self._market_id.get(id)
1709
+ side = pos.positionSide.parse_to_position_side()
1710
+ signed_amount = Decimal(pos.positionAmt)
1711
+
1712
+ if not symbol:
1713
+ return
1714
+
1715
+ if signed_amount == 0:
1716
+ side = None
1717
+ else:
1718
+ if side == PositionSide.FLAT:
1719
+ if signed_amount > 0:
1720
+ side = PositionSide.LONG
1721
+ elif signed_amount < 0:
1722
+ side = PositionSide.SHORT
1723
+
1724
+ if isinstance(pos, BinancePortfolioMarginPositionRisk):
1725
+ unrealized_pnl = float(pos.unRealizedProfit)
1726
+ elif isinstance(pos, BinanceFuturesPositionInfo):
1727
+ unrealized_pnl = float(pos.unrealizedProfit)
1728
+
1729
+ position = Position(
1730
+ symbol=symbol,
1731
+ exchange=self._exchange_id,
1732
+ signed_amount=signed_amount,
1733
+ side=side,
1734
+ entry_price=float(pos.entryPrice),
1735
+ unrealized_pnl=unrealized_pnl,
1736
+ )
1737
+ if position.is_opened:
1738
+ self._cache._apply_position(position)
1739
+
1740
+ async def _init_account_balance(self):
1741
+ if (
1742
+ self._account_type.is_spot
1743
+ or self._account_type.is_isolated_margin_or_margin
1744
+ ):
1745
+ res = await self._api_client.get_api_v3_account()
1746
+ elif self._account_type.is_linear:
1747
+ res = await self._api_client.get_fapi_v2_account()
1748
+ elif self._account_type.is_inverse:
1749
+ res = await self._api_client.get_dapi_v1_account()
1750
+
1751
+ if self._account_type.is_portfolio_margin:
1752
+ balances = []
1753
+ res_pm: list[
1754
+ BinancePortfolioMarginBalance
1755
+ ] = await self._api_client.get_papi_v1_balance()
1756
+ for balance in res_pm:
1757
+ balances.extend(balance.parse_to_balances())
1758
+ else:
1759
+ balances = res.parse_to_balances()
1760
+
1761
+ self._cache._apply_balance(self._account_type, balances)
1762
+
1763
+ if self._account_type.is_linear or self._account_type.is_inverse:
1764
+ for pos in res.positions: # type: ignore
1765
+ self._apply_position(pos)
1766
+
1767
+ async def _init_position(self):
1768
+ # NOTE: Implement in `_init_account_balance`, only portfolio margin need to implement this
1769
+ if self._account_type.is_portfolio_margin:
1770
+ res_linear: list[
1771
+ BinancePortfolioMarginPositionRisk
1772
+ ] = await self._api_client.get_papi_v1_um_position_risk()
1773
+ res_inverse: list[
1774
+ BinancePortfolioMarginPositionRisk
1775
+ ] = await self._api_client.get_papi_v1_cm_position_risk()
1776
+
1777
+ for pos in res_linear:
1778
+ self._apply_position(pos, market_type="_linear")
1779
+ for pos in res_inverse:
1780
+ self._apply_position(pos, market_type="_inverse")
1781
+
1782
+ async def _position_mode_check(self):
1783
+ error_msg = "Please Set Position Mode to `One-Way Mode` in Binance App"
1784
+
1785
+ if self._account_type.is_linear:
1786
+ res = await self._api_client.get_fapi_v1_positionSide_dual()
1787
+ if res["dualSidePosition"]:
1788
+ raise PositionModeError(error_msg)
1789
+
1790
+ elif self._account_type.is_inverse:
1791
+ res = await self._api_client.get_dapi_v1_positionSide_dual()
1792
+ if res["dualSidePosition"]:
1793
+ raise PositionModeError(error_msg)
1794
+
1795
+ elif self._account_type.is_portfolio_margin:
1796
+ res_linear = await self._api_client.get_papi_v1_um_positionSide_dual()
1797
+ res_inverse = await self._api_client.get_papi_v1_cm_positionSide_dual()
1798
+
1799
+ if res_linear["dualSidePosition"]:
1800
+ raise PositionModeError(
1801
+ "Please Set Position Mode to `One-Way Mode` in Binance App for USD-M Future"
1802
+ )
1803
+
1804
+ if res_inverse["dualSidePosition"]:
1805
+ raise PositionModeError(
1806
+ "Please Set Position Mode to `One-Way Mode` in Binance App for Coin-M Future"
1807
+ )