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,1081 @@
1
+ import msgspec
2
+ from decimal import Decimal
3
+ from typing import Any, Dict, List
4
+ from walrasquant.error import PositionModeError
5
+ from walrasquant.config import OrderQueryConfig
6
+ from walrasquant.exchange.hyperliquid.error import (
7
+ HyperliquidRateLimitError,
8
+ HyperLiquidHttpError,
9
+ HyperLiquidOrderError,
10
+ )
11
+ from walrasquant.constants import (
12
+ ExchangeType,
13
+ OrderSide,
14
+ OrderType,
15
+ TimeInForce,
16
+ OrderStatus,
17
+ TriggerType,
18
+ PositionSide,
19
+ )
20
+ from walrasquant.exchange.hyperliquid.rest_api import HyperLiquidApiClient
21
+ from walrasquant.base import OrderManagementSystem
22
+ from walrasquant.core.registry import OrderRegistry
23
+ from walrasquant.core.nautilius_core import LiveClock, MessageBus
24
+ from walrasquant.core.entity import TaskManager
25
+ from walrasquant.core.cache import AsyncCache
26
+ from walrasquant.schema import (
27
+ Balance,
28
+ Order,
29
+ BatchOrderSubmit,
30
+ CancelOrderSubmit,
31
+ InstrumentId,
32
+ Position,
33
+ )
34
+ from walrasquant.exchange.hyperliquid.schema import (
35
+ HyperLiquidMarket,
36
+ HyperLiquidWsMessageGeneral,
37
+ HyperLiquidWsOrderUpdatesMsg,
38
+ HyperLiquidWsUserFillsMsg,
39
+ HyperLiquidWsApiGeneralMsg,
40
+ HyperLiquidWsApiOrderData,
41
+ HyperLiquidWsApiMessageData,
42
+ HyperLiquidWsApiOrderStatus,
43
+ )
44
+ from walrasquant.exchange.hyperliquid.websockets import (
45
+ HyperLiquidWSClient,
46
+ HyperLiquidWSApiClient,
47
+ )
48
+ from walrasquant.exchange.hyperliquid.constants import (
49
+ HyperLiquidAccountType,
50
+ HyperLiquidEnumParser,
51
+ HyperLiquidOrderRequest,
52
+ HyperLiquidCloidCancelRequest,
53
+ HyperLiquidTimeInForce,
54
+ HyperLiquidOrderStatusType,
55
+ )
56
+
57
+
58
+ class HyperLiquidOrderManagementSystem(OrderManagementSystem):
59
+ _ws_client: HyperLiquidWSClient
60
+ _ws_api_client: HyperLiquidWSApiClient
61
+ _account_type: HyperLiquidAccountType
62
+ _market: Dict[str, HyperLiquidMarket]
63
+ _market_id: Dict[str, str]
64
+ _api_client: HyperLiquidApiClient
65
+
66
+ def __init__(
67
+ self,
68
+ account_type: HyperLiquidAccountType,
69
+ api_key: str,
70
+ secret: str,
71
+ market: Dict[str, HyperLiquidMarket],
72
+ market_id: Dict[str, str],
73
+ registry: OrderRegistry,
74
+ cache: AsyncCache,
75
+ api_client: HyperLiquidApiClient,
76
+ exchange_id: ExchangeType,
77
+ clock: LiveClock,
78
+ msgbus: MessageBus,
79
+ task_manager: TaskManager,
80
+ max_slippage: float,
81
+ order_query_config: OrderQueryConfig,
82
+ max_subscriptions_per_client: int | None = None,
83
+ max_clients: int | None = None,
84
+ ):
85
+ super().__init__(
86
+ account_type=account_type,
87
+ market=market, # type: ignore[arg-type]
88
+ market_id=market_id,
89
+ registry=registry,
90
+ cache=cache,
91
+ api_client=api_client,
92
+ ws_client=HyperLiquidWSClient(
93
+ account_type=account_type,
94
+ handler=self._ws_msg_handler,
95
+ clock=clock,
96
+ task_manager=task_manager,
97
+ api_key=api_key,
98
+ max_subscriptions_per_client=max_subscriptions_per_client,
99
+ max_clients=max_clients,
100
+ ),
101
+ exchange_id=exchange_id,
102
+ clock=clock,
103
+ msgbus=msgbus,
104
+ task_manager=task_manager,
105
+ order_query_config=order_query_config,
106
+ )
107
+
108
+ self._max_slippage = max_slippage
109
+ self._ws_msg_general_decoder = msgspec.json.Decoder(HyperLiquidWsMessageGeneral)
110
+ self._ws_msg_order_updates_decoder = msgspec.json.Decoder(
111
+ HyperLiquidWsOrderUpdatesMsg
112
+ )
113
+ self._ws_msg_user_events_decoder = msgspec.json.Decoder(
114
+ HyperLiquidWsUserFillsMsg
115
+ )
116
+
117
+ # Initialize WebSocket API client
118
+ self._ws_api_client = HyperLiquidWSApiClient(
119
+ account_type=account_type,
120
+ api_key=api_key,
121
+ secret=secret,
122
+ handler=self._ws_api_msg_handler,
123
+ task_manager=task_manager,
124
+ clock=clock,
125
+ enable_rate_limit=True,
126
+ )
127
+ self._ws_api_msg_decoder = msgspec.json.Decoder(HyperLiquidWsApiGeneralMsg)
128
+
129
+ async def _init_account_balance(self):
130
+ """Initialize the account balance"""
131
+ res = await self._api_client.get_user_spot_summary()
132
+ self._cache._apply_balance(
133
+ account_type=self._account_type, balances=res.parse_to_balances()
134
+ )
135
+
136
+ async def _init_position(self):
137
+ """Initialize the position"""
138
+ res = await self._api_client.get_user_perps_summary()
139
+ for pos_data in res.assetPositions:
140
+ if pos_data.type != "oneWay":
141
+ raise PositionModeError(
142
+ f"HyperLiquid only supports one-way position mode, but got {pos_data.type}"
143
+ )
144
+
145
+ symbol = self._market_id.get(pos_data.position.coin, None)
146
+ if not symbol:
147
+ continue
148
+
149
+ signed_amount = Decimal(pos_data.position.szi)
150
+ if signed_amount == Decimal("0"):
151
+ continue
152
+
153
+ position = Position(
154
+ symbol=symbol,
155
+ exchange=self._exchange_id,
156
+ signed_amount=Decimal(pos_data.position.szi),
157
+ side=PositionSide.LONG
158
+ if signed_amount > Decimal("0")
159
+ else PositionSide.SHORT,
160
+ entry_price=float(pos_data.position.entryPx),
161
+ unrealized_pnl=float(pos_data.position.unrealizedPnl),
162
+ )
163
+ self._cache._apply_position(position)
164
+
165
+ async def _position_mode_check(self):
166
+ """Check the position mode"""
167
+ # NOTE: HyperLiquid only supports one-way position mode
168
+ pass
169
+
170
+ async def create_tp_sl_order(
171
+ self,
172
+ oid: str,
173
+ symbol: str,
174
+ side: OrderSide,
175
+ type: OrderType,
176
+ amount: Decimal,
177
+ price: Decimal | None = None,
178
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
179
+ tp_order_type: OrderType | None = None,
180
+ tp_trigger_price: Decimal | None = None,
181
+ tp_price: Decimal | None = None,
182
+ tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
183
+ sl_order_type: OrderType | None = None,
184
+ sl_trigger_price: Decimal | None = None,
185
+ sl_price: Decimal | None = None,
186
+ sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
187
+ **kwargs,
188
+ ) -> Order:
189
+ """Create a take profit and stop loss order"""
190
+ raise NotImplementedError
191
+
192
+ async def create_order(
193
+ self,
194
+ oid: str,
195
+ symbol: str,
196
+ side: OrderSide,
197
+ type: OrderType,
198
+ amount: Decimal,
199
+ price: Decimal | None,
200
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
201
+ reduce_only: bool = False,
202
+ **kwargs,
203
+ ) -> Order:
204
+ """Create an order"""
205
+ self._registry.register_tmp_order(
206
+ order=Order(
207
+ oid=oid,
208
+ exchange=self._exchange_id,
209
+ symbol=symbol,
210
+ status=OrderStatus.INITIALIZED,
211
+ amount=amount,
212
+ type=type,
213
+ price=float(price) if price else None,
214
+ time_in_force=time_in_force,
215
+ timestamp=self._clock.timestamp_ms(),
216
+ reduce_only=reduce_only,
217
+ )
218
+ )
219
+
220
+ market = self._market.get(symbol)
221
+ if not market:
222
+ raise ValueError(
223
+ f"Market {symbol} not found in exchange {self._exchange_id}"
224
+ )
225
+
226
+ hl_time_in_force: HyperLiquidTimeInForce
227
+ if type.is_limit:
228
+ if price is None or time_in_force is None:
229
+ raise ValueError(
230
+ "price and time_in_force are required for limit orders"
231
+ )
232
+ hl_time_in_force = HyperLiquidEnumParser.to_hyperliquid_time_in_force(
233
+ time_in_force
234
+ )
235
+ elif type.is_market:
236
+ hl_time_in_force = HyperLiquidTimeInForce.IOC
237
+ bookl1 = self._cache.bookl1(symbol)
238
+ if not bookl1:
239
+ raise ValueError(
240
+ "Please subscribe to bookl1 first, Market requires bookl1"
241
+ )
242
+ if side.is_buy:
243
+ price = self._price_to_precision(
244
+ symbol, bookl1.ask * (1 + self._max_slippage), "ceil"
245
+ )
246
+ else:
247
+ price = self._price_to_precision(
248
+ symbol, bookl1.bid * (1 - self._max_slippage), "floor"
249
+ )
250
+
251
+ elif type.is_post_only:
252
+ if price is None:
253
+ raise ValueError("price is required for post-only orders")
254
+ hl_time_in_force = HyperLiquidTimeInForce.ALO
255
+ else:
256
+ raise ValueError(f"Unsupported order type: {type}")
257
+
258
+ params: HyperLiquidOrderRequest = {
259
+ "a": int(market.baseId),
260
+ "b": side.is_buy,
261
+ "p": str(price),
262
+ "s": str(amount),
263
+ "r": reduce_only,
264
+ "t": {"limit": {"tif": hl_time_in_force.value}},
265
+ "c": oid, # client order id
266
+ }
267
+ params.update(kwargs)
268
+
269
+ try:
270
+ res = await self._api_client.place_orders(orders=[params])
271
+ status = res.response.data.statuses[0]
272
+
273
+ if status.error:
274
+ error_msg = status.error
275
+ self._log.error(
276
+ f"Failed to place order for {symbol}: {error_msg} params: {str(params)}"
277
+ )
278
+ order = Order(
279
+ exchange=self._exchange_id,
280
+ oid=oid,
281
+ timestamp=self._clock.timestamp_ms(),
282
+ symbol=symbol,
283
+ type=type,
284
+ side=side,
285
+ amount=amount,
286
+ price=float(price),
287
+ time_in_force=time_in_force,
288
+ status=OrderStatus.FAILED,
289
+ filled=Decimal(0),
290
+ remaining=amount,
291
+ reduce_only=reduce_only,
292
+ reason=error_msg,
293
+ )
294
+ else:
295
+ order_status = status.resting or status.filled
296
+ order = Order(
297
+ oid=oid,
298
+ eid=str(order_status.oid) if order_status is not None else None,
299
+ exchange=self._exchange_id,
300
+ timestamp=self._clock.timestamp_ms(),
301
+ symbol=symbol,
302
+ type=type,
303
+ side=side,
304
+ amount=amount,
305
+ price=float(price),
306
+ time_in_force=time_in_force,
307
+ status=OrderStatus.PENDING,
308
+ filled=Decimal(0),
309
+ remaining=amount,
310
+ reduce_only=reduce_only,
311
+ )
312
+ except Exception as e:
313
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
314
+ self._log.error(
315
+ f"Failed to place order for {symbol}: {error_msg} params: {str(params)}"
316
+ )
317
+ order = Order(
318
+ oid=oid,
319
+ exchange=self._exchange_id,
320
+ timestamp=self._clock.timestamp_ms(),
321
+ symbol=symbol,
322
+ type=type,
323
+ side=side,
324
+ amount=amount,
325
+ price=float(price),
326
+ time_in_force=time_in_force,
327
+ status=OrderStatus.FAILED,
328
+ filled=Decimal(0),
329
+ remaining=amount,
330
+ reduce_only=reduce_only,
331
+ reason=error_msg,
332
+ )
333
+ self.order_status_update(order)
334
+ return order
335
+
336
+ async def create_batch_orders(
337
+ self,
338
+ orders: List[BatchOrderSubmit],
339
+ ) -> List[Order]:
340
+ """Create a batch of orders"""
341
+
342
+ batch_orders: List[HyperLiquidOrderRequest] = []
343
+ for order in orders:
344
+ self._registry.register_tmp_order(
345
+ order=Order(
346
+ oid=order.oid,
347
+ exchange=self._exchange_id,
348
+ symbol=order.symbol,
349
+ status=OrderStatus.INITIALIZED,
350
+ amount=order.amount,
351
+ type=order.type,
352
+ price=float(order.price) if order.price else None,
353
+ time_in_force=order.time_in_force,
354
+ timestamp=self._clock.timestamp_ms(),
355
+ reduce_only=order.reduce_only,
356
+ )
357
+ )
358
+
359
+ market = self._market.get(order.symbol)
360
+ if not market:
361
+ raise ValueError(
362
+ f"Market {order.symbol} not found in exchange {self._exchange_id}"
363
+ )
364
+ price = order.price
365
+ hl_time_in_force: HyperLiquidTimeInForce
366
+ if order.type.is_limit:
367
+ if price is None:
368
+ raise ValueError("price is required for limit batch orders")
369
+ time_in_force = HyperLiquidEnumParser.to_hyperliquid_time_in_force(
370
+ order.time_in_force
371
+ )
372
+ hl_time_in_force = time_in_force
373
+ elif order.type.is_market:
374
+ hl_time_in_force = HyperLiquidTimeInForce.IOC
375
+ bookl1 = self._cache.bookl1(order.symbol)
376
+ if not bookl1:
377
+ raise ValueError(
378
+ "Please subscribe to bookl1 first, Market requires bookl1"
379
+ )
380
+ if order.side.is_buy:
381
+ price = self._price_to_precision(
382
+ order.symbol, bookl1.ask * (1 + self._max_slippage), "ceil"
383
+ )
384
+ else:
385
+ price = self._price_to_precision(
386
+ order.symbol, bookl1.bid * (1 - self._max_slippage), "floor"
387
+ )
388
+
389
+ elif order.type.is_post_only:
390
+ if price is None:
391
+ raise ValueError("price is required for post-only batch orders")
392
+ hl_time_in_force = HyperLiquidTimeInForce.ALO
393
+ else:
394
+ raise ValueError(f"Unsupported order type: {order.type}")
395
+
396
+ params: HyperLiquidOrderRequest = {
397
+ "a": int(market.baseId),
398
+ "b": order.side.is_buy,
399
+ "p": str(price),
400
+ "s": str(order.amount),
401
+ "r": order.reduce_only,
402
+ "t": {"limit": {"tif": hl_time_in_force.value}},
403
+ "c": order.oid, # client order id
404
+ }
405
+ params.update(order.kwargs)
406
+ batch_orders.append(params)
407
+
408
+ try:
409
+ res = await self._api_client.place_orders(orders=batch_orders)
410
+ for order, status in zip(orders, res.response.data.statuses):
411
+ if status.error:
412
+ error_msg = status.error
413
+ self._log.error(
414
+ f"Failed to place order for {order.symbol}: {error_msg} params: {str(params)}"
415
+ )
416
+ res_batch_order = Order(
417
+ oid=order.oid,
418
+ exchange=self._exchange_id,
419
+ timestamp=self._clock.timestamp_ms(),
420
+ symbol=order.symbol,
421
+ type=order.type,
422
+ side=order.side,
423
+ amount=order.amount,
424
+ price=float(order.price) if order.price is not None else None,
425
+ time_in_force=order.time_in_force,
426
+ status=OrderStatus.FAILED,
427
+ filled=Decimal(0),
428
+ remaining=order.amount,
429
+ reduce_only=order.reduce_only,
430
+ reason=error_msg,
431
+ )
432
+ else:
433
+ order_status = status.resting or status.filled
434
+ res_batch_order = Order(
435
+ exchange=self._exchange_id,
436
+ timestamp=self._clock.timestamp_ms(),
437
+ eid=str(order_status.oid) if order_status is not None else None,
438
+ oid=order.oid,
439
+ symbol=order.symbol,
440
+ type=order.type,
441
+ side=order.side,
442
+ amount=order.amount,
443
+ price=float(order.price) if order.price is not None else None,
444
+ time_in_force=order.time_in_force,
445
+ status=OrderStatus.PENDING,
446
+ filled=Decimal(0),
447
+ remaining=order.amount,
448
+ reduce_only=order.reduce_only,
449
+ )
450
+ self.order_status_update(res_batch_order)
451
+
452
+ except Exception as e:
453
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
454
+ self._log.error(f"Error creating batch orders: {error_msg}")
455
+ for order in orders:
456
+ order = Order(
457
+ exchange=self._exchange_id,
458
+ timestamp=self._clock.timestamp_ms(),
459
+ symbol=order.symbol,
460
+ oid=order.oid,
461
+ type=order.type,
462
+ side=order.side,
463
+ amount=order.amount,
464
+ price=float(order.price) if order.price else None,
465
+ time_in_force=order.time_in_force,
466
+ status=OrderStatus.FAILED,
467
+ filled=Decimal(0),
468
+ remaining=order.amount,
469
+ reduce_only=order.reduce_only,
470
+ reason=error_msg,
471
+ )
472
+ self.order_status_update(order)
473
+ return []
474
+
475
+ async def cancel_order(self, oid: str, symbol: str, **kwargs) -> Order:
476
+ """Cancel an order"""
477
+ market = self._market.get(symbol)
478
+ if not market:
479
+ raise ValueError(
480
+ f"Market {symbol} not found in exchange {self._exchange_id}"
481
+ )
482
+
483
+ params: HyperLiquidCloidCancelRequest = {
484
+ "asset": int(market.baseId),
485
+ "cloid": oid,
486
+ }
487
+
488
+ try:
489
+ res = await self._api_client.cancel_orders_by_cloid(cancels=[params])
490
+ status = res.response.data.statuses[0]
491
+ if status == "success":
492
+ order = Order(
493
+ oid=oid,
494
+ exchange=self._exchange_id,
495
+ timestamp=self._clock.timestamp_ms(),
496
+ symbol=symbol,
497
+ status=OrderStatus.CANCELING,
498
+ )
499
+ else:
500
+ error_msg = status.error if status.error else "Unknown error"
501
+ self._log.error(
502
+ f"Failed to cancel order for {symbol}: {error_msg} params: {str(params)}"
503
+ )
504
+ order = Order(
505
+ oid=oid,
506
+ exchange=self._exchange_id,
507
+ timestamp=self._clock.timestamp_ms(),
508
+ symbol=symbol,
509
+ status=OrderStatus.CANCEL_FAILED,
510
+ reason=error_msg,
511
+ )
512
+ self.order_status_update(order)
513
+ except HyperliquidRateLimitError as e:
514
+ error_msg = f"rate_limit (retry_after={e.retry_after:.1f}s): {str(e)}"
515
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
516
+ order = Order(
517
+ oid=oid,
518
+ exchange=self._exchange_id,
519
+ timestamp=self._clock.timestamp_ms(),
520
+ symbol=symbol,
521
+ status=OrderStatus.CANCEL_FAILED,
522
+ reason=error_msg,
523
+ )
524
+ self.order_status_update(order)
525
+ except (HyperLiquidHttpError, HyperLiquidOrderError) as e:
526
+ error_msg = str(e)
527
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
528
+ order = Order(
529
+ oid=oid,
530
+ exchange=self._exchange_id,
531
+ timestamp=self._clock.timestamp_ms(),
532
+ symbol=symbol,
533
+ status=OrderStatus.CANCEL_FAILED,
534
+ reason=error_msg,
535
+ )
536
+ self.order_status_update(order)
537
+ except Exception as e:
538
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
539
+ self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
540
+ order = Order(
541
+ oid=oid,
542
+ exchange=self._exchange_id,
543
+ timestamp=self._clock.timestamp_ms(),
544
+ symbol=symbol,
545
+ status=OrderStatus.CANCEL_FAILED,
546
+ reason=error_msg,
547
+ )
548
+ self.order_status_update(order)
549
+ return order
550
+
551
+ async def modify_order(
552
+ self,
553
+ oid: str,
554
+ symbol: str,
555
+ side: OrderSide | None = None,
556
+ price: Decimal | None = None,
557
+ amount: Decimal | None = None,
558
+ **kwargs,
559
+ ) -> Order:
560
+ """Modify an order"""
561
+ raise NotImplementedError
562
+
563
+ async def cancel_all_orders(self, symbol: str) -> bool:
564
+ await self._cache.wait_for_inflight_orders(symbol)
565
+ oids = self._cache.get_open_orders(symbol)
566
+ if not oids:
567
+ return True
568
+
569
+ orders = [
570
+ CancelOrderSubmit(
571
+ symbol=symbol,
572
+ instrument_id=InstrumentId.from_str(symbol),
573
+ oid=oid,
574
+ )
575
+ for oid in oids
576
+ ]
577
+ return await self.cancel_batch_orders(orders)
578
+
579
+ async def create_order_ws(
580
+ self,
581
+ oid: str,
582
+ symbol: str,
583
+ side: OrderSide,
584
+ type: OrderType,
585
+ amount: Decimal,
586
+ price: Decimal | None,
587
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
588
+ reduce_only: bool = False,
589
+ **kwargs,
590
+ ) -> None:
591
+ """Create an order via WebSocket API"""
592
+ self._registry.register_tmp_order(
593
+ order=Order(
594
+ oid=oid,
595
+ exchange=self._exchange_id,
596
+ symbol=symbol,
597
+ status=OrderStatus.INITIALIZED,
598
+ amount=amount,
599
+ type=type,
600
+ price=float(price) if price else None,
601
+ time_in_force=time_in_force,
602
+ timestamp=self._clock.timestamp_ms(),
603
+ reduce_only=reduce_only,
604
+ )
605
+ )
606
+
607
+ market = self._market.get(symbol)
608
+ if not market:
609
+ raise ValueError(
610
+ f"Market {symbol} not found in exchange {self._exchange_id}"
611
+ )
612
+
613
+ hl_time_in_force: HyperLiquidTimeInForce
614
+ if type.is_limit:
615
+ if price is None or time_in_force is None:
616
+ raise ValueError(
617
+ "price and time_in_force are required for limit orders"
618
+ )
619
+ hl_time_in_force = HyperLiquidEnumParser.to_hyperliquid_time_in_force(
620
+ time_in_force
621
+ )
622
+ elif type.is_market:
623
+ hl_time_in_force = HyperLiquidTimeInForce.IOC
624
+ bookl1 = self._cache.bookl1(symbol)
625
+ if not bookl1:
626
+ raise ValueError(
627
+ "Please subscribe to bookl1 first, Market requires bookl1"
628
+ )
629
+ if side.is_buy:
630
+ price = self._price_to_precision(
631
+ symbol, bookl1.ask * (1 + self._max_slippage), "ceil"
632
+ )
633
+ else:
634
+ price = self._price_to_precision(
635
+ symbol, bookl1.bid * (1 - self._max_slippage), "floor"
636
+ )
637
+ elif type.is_post_only:
638
+ if price is None:
639
+ raise ValueError("price is required for post-only orders")
640
+ hl_time_in_force = HyperLiquidTimeInForce.ALO
641
+ else:
642
+ raise ValueError(f"Unsupported order type: {type}")
643
+
644
+ params: HyperLiquidOrderRequest = {
645
+ "a": int(market.baseId),
646
+ "b": side.is_buy,
647
+ "p": str(price),
648
+ "s": str(amount),
649
+ "r": reduce_only,
650
+ "t": {"limit": {"tif": hl_time_in_force.value}},
651
+ "c": oid, # client order id
652
+ }
653
+ params.update(kwargs)
654
+
655
+ try:
656
+ await self._ws_api_client.place_order(id=oid, orders=[params])
657
+ except HyperliquidRateLimitError as e:
658
+ order = self._rate_limit_failed_order(
659
+ oid=oid,
660
+ symbol=symbol,
661
+ side=side,
662
+ type=type,
663
+ amount=amount,
664
+ price=price,
665
+ time_in_force=time_in_force,
666
+ reduce_only=reduce_only,
667
+ exc=e,
668
+ )
669
+ self.order_status_update(order)
670
+
671
+ async def cancel_batch_orders(self, orders: List[CancelOrderSubmit]) -> bool:
672
+ valid_orders = [order for order in orders if order.oid is not None]
673
+ if not valid_orders:
674
+ return True
675
+
676
+ cancels: List[HyperLiquidCloidCancelRequest] = []
677
+ for order in valid_orders:
678
+ market = self._market.get(order.symbol)
679
+ if not market:
680
+ raise ValueError(
681
+ f"Market {order.symbol} not found in exchange {self._exchange_id}"
682
+ )
683
+ cancels.append({"asset": int(market.baseId), "cloid": order.oid})
684
+
685
+ try:
686
+ res = await self._api_client.cancel_orders_by_cloid(cancels=cancels)
687
+ all_success = True
688
+ for order, status in zip(valid_orders, res.response.data.statuses):
689
+ if status == "success":
690
+ cancel_order = Order(
691
+ oid=order.oid,
692
+ exchange=self._exchange_id,
693
+ timestamp=self._clock.timestamp_ms(),
694
+ symbol=order.symbol,
695
+ status=OrderStatus.CANCELING,
696
+ )
697
+ else:
698
+ error_msg = status if isinstance(status, str) else status.error
699
+ all_success = False
700
+ self._log.error(
701
+ f"Failed to cancel order for {order.symbol}: {error_msg} params: {str(cancels)}"
702
+ )
703
+ cancel_order = Order(
704
+ oid=order.oid,
705
+ exchange=self._exchange_id,
706
+ timestamp=self._clock.timestamp_ms(),
707
+ symbol=order.symbol,
708
+ status=OrderStatus.CANCEL_FAILED,
709
+ reason=error_msg,
710
+ )
711
+ self.order_status_update(cancel_order)
712
+ return all_success
713
+ except Exception as e:
714
+ if isinstance(e, HyperliquidRateLimitError):
715
+ error_msg = f"rate_limit (retry_after={e.retry_after:.1f}s): {str(e)}"
716
+ elif isinstance(e, (HyperLiquidHttpError, HyperLiquidOrderError)):
717
+ error_msg = str(e)
718
+ else:
719
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
720
+ self._log.error(
721
+ f"Error canceling batch orders: {error_msg} params: {str(cancels)}"
722
+ )
723
+ for order in valid_orders:
724
+ self.order_status_update(
725
+ Order(
726
+ oid=order.oid,
727
+ exchange=self._exchange_id,
728
+ timestamp=self._clock.timestamp_ms(),
729
+ symbol=order.symbol,
730
+ status=OrderStatus.CANCEL_FAILED,
731
+ reason=error_msg,
732
+ )
733
+ )
734
+ return False
735
+
736
+ async def cancel_order_ws(self, oid: str, symbol: str, **kwargs) -> None:
737
+ market = self._market.get(symbol)
738
+ if not market:
739
+ raise ValueError(
740
+ f"Market {symbol} not found in exchange {self._exchange_id}"
741
+ )
742
+
743
+ params: HyperLiquidCloidCancelRequest = {
744
+ "asset": int(market.baseId),
745
+ "cloid": oid,
746
+ }
747
+
748
+ await self._ws_api_client.cancel_orders_by_cloid(id=oid, cancels=[params])
749
+
750
+ def _parse_query_order_response(
751
+ self,
752
+ *,
753
+ oid: str,
754
+ symbol: str,
755
+ res: dict[str, Any],
756
+ ) -> Order | None:
757
+ wrapper = res.get("order")
758
+ response_status = res.get("status")
759
+ if not isinstance(wrapper, dict):
760
+ return None
761
+
762
+ if isinstance(wrapper.get("order"), dict):
763
+ order_data = wrapper["order"]
764
+ status_value = wrapper.get("status") or order_data.get("status")
765
+ status_timestamp = wrapper.get("statusTimestamp")
766
+ else:
767
+ order_data = wrapper
768
+ status_value = order_data.get("status") or response_status
769
+ status_timestamp = order_data.get("statusTimestamp")
770
+
771
+ if not status_value or status_value == "order":
772
+ return None
773
+
774
+ try:
775
+ order_status = HyperLiquidEnumParser.parse_order_status(
776
+ HyperLiquidOrderStatusType(str(status_value))
777
+ )
778
+ except ValueError:
779
+ return None
780
+
781
+ cached_order = self._cache.get_order(oid).value_or(None)
782
+ coin = order_data.get("coin")
783
+ parsed_symbol = self._market_id.get(coin, symbol) if coin else symbol
784
+
785
+ remaining = Decimal(str(order_data.get("sz", "0")))
786
+ if order_data.get("origSz") is not None:
787
+ amount = Decimal(str(order_data["origSz"]))
788
+ elif cached_order and cached_order.amount is not None:
789
+ amount = cached_order.amount
790
+ else:
791
+ amount = remaining
792
+
793
+ side_value = order_data.get("side")
794
+ side = cached_order.side if cached_order else None
795
+ if side_value == "B":
796
+ side = OrderSide.BUY
797
+ elif side_value == "A":
798
+ side = OrderSide.SELL
799
+
800
+ price = order_data.get("limitPx")
801
+
802
+ return Order(
803
+ exchange=self._exchange_id,
804
+ symbol=parsed_symbol,
805
+ status=order_status,
806
+ eid=str(order_data.get("oid")) if order_data.get("oid") else None,
807
+ oid=str(order_data.get("cloid") or oid),
808
+ amount=amount,
809
+ filled=amount - remaining if amount is not None else None,
810
+ timestamp=int(
811
+ status_timestamp
812
+ or order_data.get("timestamp")
813
+ or self._clock.timestamp_ms()
814
+ ),
815
+ type=cached_order.type if cached_order else OrderType.LIMIT,
816
+ side=side,
817
+ time_in_force=cached_order.time_in_force
818
+ if cached_order
819
+ else TimeInForce.GTC,
820
+ price=float(price) if price is not None else None,
821
+ average=float(price) if price is not None else None,
822
+ remaining=remaining,
823
+ reduce_only=cached_order.reduce_only if cached_order else None,
824
+ )
825
+
826
+ async def query_order(self, oid: str, symbol: str) -> Order | None:
827
+ try:
828
+ res = await self._api_client.get_order_status(oid)
829
+ return self._parse_query_order_response(oid=oid, symbol=symbol, res=res)
830
+ except Exception as e:
831
+ error_msg = f"{e.__class__.__name__}: {str(e)}"
832
+ self._log.error(f"Error querying order: {error_msg} oid={oid}")
833
+ return None
834
+
835
+ def _ws_msg_handler(self, raw: bytes):
836
+ """Handle WebSocket messages"""
837
+ try:
838
+ ws_msg: HyperLiquidWsMessageGeneral = self._ws_msg_general_decoder.decode(
839
+ raw
840
+ )
841
+ # if ws_msg.channel == "pong":
842
+ # self._ws_client._transport.notify_user_specific_pong_received()
843
+ # self._log.debug("Pong received")
844
+ # return
845
+
846
+ if ws_msg.channel == "orderUpdates":
847
+ self._parse_order_update(raw)
848
+ elif ws_msg.channel == "user":
849
+ self._parse_user_events(raw)
850
+ except msgspec.DecodeError as e:
851
+ self._log.error(f"Error decoding WebSocket message: {str(raw)} {e}")
852
+ return
853
+
854
+ def _ws_api_msg_handler(self, raw: bytes):
855
+ """Handle WebSocket API messages for order operations"""
856
+ try:
857
+ ws_msg: HyperLiquidWsApiGeneralMsg = self._ws_api_msg_decoder.decode(raw)
858
+
859
+ # if ws_msg.is_pong:
860
+ # self._ws_api_client._transport.notify_user_specific_pong_received()
861
+ # self._log.debug("API Pong received")
862
+ # return
863
+
864
+ if ws_msg.is_order_response and ws_msg.data:
865
+ self._parse_ws_api_response(ws_msg.data)
866
+
867
+ except msgspec.DecodeError as e:
868
+ self._log.error(f"Error decoding WebSocket API message: {str(raw)} {e}")
869
+ return
870
+
871
+ def _parse_ws_api_response(self, data: HyperLiquidWsApiMessageData):
872
+ """Parse WebSocket API response and create Order objects"""
873
+ request_id = data.id
874
+ oid = self._ws_api_client._int_to_oid(request_id)
875
+ response = data.response
876
+
877
+ if response.payload.status != "ok":
878
+ self._log.error(
879
+ f"WebSocket API error for request {request_id}: {response.payload.status}"
880
+ )
881
+ return
882
+
883
+ response_data = response.payload.response
884
+ if response_data.type == "order":
885
+ # Handle order placement response
886
+ order_data: HyperLiquidWsApiOrderData = response_data.data
887
+ for status in order_data.statuses:
888
+ self._create_order_from_ws_response(oid, status) # type: ignore[arg-type]
889
+ elif response_data.type == "cancel":
890
+ # Handle order cancellation response
891
+ cancel_data: HyperLiquidWsApiOrderData = response_data.data
892
+ for status in cancel_data.statuses:
893
+ self._create_cancel_order_from_ws_response(oid, status)
894
+
895
+ def _create_order_from_ws_response(
896
+ self, oid: str, status: HyperLiquidWsApiOrderStatus
897
+ ) -> Order | None:
898
+ """Create Order object from WebSocket API order response"""
899
+ temp_order = self._registry.get_tmp_order(oid)
900
+ if not temp_order:
901
+ return
902
+
903
+ if status.error:
904
+ self._log.error(f"Order placement failed for {oid}: {status.error}")
905
+ order = Order(
906
+ oid=oid,
907
+ exchange=self._exchange_id,
908
+ timestamp=self._clock.timestamp_ms(),
909
+ symbol=temp_order.symbol,
910
+ type=temp_order.type,
911
+ side=temp_order.side,
912
+ amount=temp_order.amount,
913
+ price=temp_order.price,
914
+ time_in_force=temp_order.time_in_force,
915
+ status=OrderStatus.FAILED,
916
+ filled=Decimal(0),
917
+ remaining=temp_order.amount,
918
+ reduce_only=temp_order.reduce_only,
919
+ reason=status.error,
920
+ )
921
+ self.order_status_update(order)
922
+ else:
923
+ eid = None
924
+ if status.filled:
925
+ eid = status.filled.oid
926
+ elif status.resting:
927
+ eid = status.resting.oid
928
+
929
+ if eid:
930
+ order = Order(
931
+ oid=oid,
932
+ exchange=self._exchange_id,
933
+ timestamp=self._clock.timestamp_ms(),
934
+ eid=str(eid),
935
+ symbol=temp_order.symbol,
936
+ type=temp_order.type,
937
+ side=temp_order.side,
938
+ amount=temp_order.amount,
939
+ price=temp_order.price,
940
+ time_in_force=temp_order.time_in_force,
941
+ status=OrderStatus.PENDING,
942
+ filled=Decimal(0),
943
+ remaining=temp_order.amount,
944
+ reduce_only=temp_order.reduce_only,
945
+ )
946
+ self.order_status_update(order)
947
+
948
+ def _create_cancel_order_from_ws_response(
949
+ self, oid: str, status: str | HyperLiquidWsApiOrderStatus
950
+ ) -> Order | None:
951
+ """Create Order object from WebSocket API cancel response"""
952
+ temp_order = self._registry.get_tmp_order(oid)
953
+ if not temp_order:
954
+ return
955
+
956
+ if isinstance(status, str) and status == "success":
957
+ order = Order(
958
+ oid=oid,
959
+ exchange=self._exchange_id,
960
+ timestamp=self._clock.timestamp_ms(),
961
+ symbol=temp_order.symbol,
962
+ status=OrderStatus.CANCELING,
963
+ )
964
+ elif isinstance(status, HyperLiquidWsApiOrderStatus):
965
+ self._log.error(f"Order cancellation failed for {oid}: {status.error}")
966
+ order = Order(
967
+ oid=oid,
968
+ exchange=self._exchange_id,
969
+ timestamp=self._clock.timestamp_ms(),
970
+ symbol=temp_order.symbol,
971
+ status=OrderStatus.CANCEL_FAILED,
972
+ reason=status.error,
973
+ )
974
+ self.order_status_update(order)
975
+
976
+ def _parse_user_events(self, raw: bytes):
977
+ user_fills_msg = self._ws_msg_user_events_decoder.decode(raw)
978
+ self._log.debug(f"User fills received: {str(user_fills_msg)}")
979
+ if not user_fills_msg.data.fills:
980
+ return
981
+
982
+ fills = user_fills_msg.data.fills
983
+ for fill in fills:
984
+ symbol = self._market_id.get(fill.coin, None)
985
+ if not symbol:
986
+ return
987
+
988
+ market = self._market[symbol]
989
+ if market.swap:
990
+ sz = Decimal(fill.sz) if fill.side.is_buy else -Decimal(fill.sz)
991
+ signed_amount = Decimal(fill.startPosition) + sz
992
+ position = Position(
993
+ symbol=symbol,
994
+ exchange=self._exchange_id,
995
+ signed_amount=signed_amount,
996
+ entry_price=float(
997
+ fill.px
998
+ ), # NOTE: HyperLiquid does not provide entry price in fills, using fill price instead,
999
+ # I know we can use current position to calculate the entry price, but it costs performance
1000
+ side=PositionSide.LONG
1001
+ if signed_amount > Decimal("0")
1002
+ else PositionSide.SHORT,
1003
+ )
1004
+ self._cache._apply_position(position)
1005
+ self._log.debug(f"Position updated: {str(position)}")
1006
+ else:
1007
+ current_balance = self._cache.get_balance(
1008
+ account_type=self._account_type
1009
+ )
1010
+
1011
+ quote_total = current_balance.balance_total.get(
1012
+ market.quote, Decimal("0")
1013
+ ) # zero if not found
1014
+ if fill.side.is_buy:
1015
+ base_amount = (
1016
+ Decimal(fill.sz)
1017
+ + Decimal(fill.startPosition)
1018
+ - Decimal(fill.fee)
1019
+ )
1020
+ quote_amount = quote_total - Decimal(fill.px) * Decimal(fill.sz)
1021
+ balances = [
1022
+ Balance(asset=market.baseName, free=base_amount),
1023
+ Balance(asset=market.quote, free=quote_amount),
1024
+ ]
1025
+
1026
+ else:
1027
+ base_amount = Decimal(fill.startPosition) - Decimal(fill.sz)
1028
+ quote_amount = (
1029
+ quote_total
1030
+ + Decimal(fill.px) * Decimal(fill.sz)
1031
+ - Decimal(fill.fee)
1032
+ )
1033
+
1034
+ balances = [
1035
+ Balance(asset=market.baseName, free=base_amount),
1036
+ Balance(asset=market.quote, free=quote_amount),
1037
+ ]
1038
+
1039
+ self._log.debug(
1040
+ f"\nbase: {market.baseName}, free: {base_amount}\n"
1041
+ f"quote: {market.quote}, free: {quote_amount}"
1042
+ )
1043
+
1044
+ self._cache._apply_balance(
1045
+ account_type=self._account_type, balances=balances
1046
+ )
1047
+
1048
+ def _parse_order_update(self, raw: bytes):
1049
+ order_msg: HyperLiquidWsOrderUpdatesMsg = (
1050
+ self._ws_msg_order_updates_decoder.decode(raw)
1051
+ )
1052
+ self._log.debug(f"Order update received: {str(order_msg)}")
1053
+
1054
+ for data in order_msg.data:
1055
+ if data.order.cloid is None:
1056
+ continue
1057
+ tmp_order = self._registry.get_tmp_order(data.order.cloid)
1058
+ if not tmp_order:
1059
+ continue
1060
+
1061
+ id = data.order.coin
1062
+ symbol = self._market_id[id]
1063
+ order_status = HyperLiquidEnumParser.parse_order_status(data.status)
1064
+ order = Order(
1065
+ exchange=self._exchange_id,
1066
+ eid=str(data.order.oid),
1067
+ oid=data.order.cloid,
1068
+ symbol=symbol,
1069
+ type=tmp_order.type, # HyperLiquid only have limit orders
1070
+ side=OrderSide.BUY if data.order.side.is_buy else OrderSide.SELL,
1071
+ amount=Decimal(data.order.origSz),
1072
+ price=float(data.order.limitPx),
1073
+ average=float(data.order.limitPx),
1074
+ status=order_status,
1075
+ filled=Decimal(data.order.origSz) - Decimal(data.order.sz),
1076
+ remaining=Decimal(data.order.sz),
1077
+ timestamp=data.order.timestamp,
1078
+ reduce_only=tmp_order.reduce_only,
1079
+ )
1080
+ self._log.debug(f"Parsed order: {str(order)}")
1081
+ self.order_status_update(order)