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