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,371 @@
1
+ from datetime import timedelta
2
+ from typing import Dict, TypedDict, NotRequired
3
+ from walrasquant.constants import (
4
+ AccountType,
5
+ OrderStatus,
6
+ PositionSide,
7
+ OrderSide,
8
+ TimeInForce,
9
+ OrderType,
10
+ KlineInterval,
11
+ )
12
+ from enum import Enum
13
+ from walrasquant.error import KlineSupportedError
14
+ from throttled.asyncio import Throttled, rate_limiter, RateLimiterType
15
+ from throttled import Throttled as ThrottledSync
16
+ from walrasquant.exchange.hyperliquid.error import HyperliquidRateLimitError
17
+
18
+
19
+ def oid_to_cloid_hex(oid: str) -> str:
20
+ """Convert NexusTrader OidGen's decimal OID to 128-bit hex string.
21
+
22
+ Produces a lowercase hex string with a `0x` prefix and exactly 32 hex
23
+ characters (128 bits), e.g. `0x0000...abcd`.
24
+
25
+ Notes:
26
+ - OidGen always emits a positive decimal string (timestamp+seq+shard), so
27
+ negativity checks are unnecessary.
28
+ - The numeric value (currently ~20 decimal digits) is far below 2^128, so
29
+ overflow checks are unnecessary.
30
+ - No stripping is performed because OidGen-controlled inputs have no
31
+ surrounding whitespace.
32
+ """
33
+ n = int(oid)
34
+ return f"0x{n:032x}"
35
+
36
+
37
+ class HyperLiquidAccountType(AccountType):
38
+ MAINNET = "mainnet"
39
+ TESTNET = "testnet"
40
+
41
+ @property
42
+ def exchange_id(self):
43
+ return "hyperliquid"
44
+
45
+ @property
46
+ def is_testnet(self):
47
+ return self == self.TESTNET
48
+
49
+ @property
50
+ def ws_url(self):
51
+ if self.is_testnet:
52
+ return "wss://api.hyperliquid-testnet.xyz/ws"
53
+ return "wss://api.hyperliquid.xyz/ws"
54
+
55
+ @property
56
+ def rest_url(self):
57
+ if self.is_testnet:
58
+ return "https://api.hyperliquid-testnet.xyz"
59
+ return "https://api.hyperliquid.xyz"
60
+
61
+
62
+ class HyperLiquidTimeInForce(Enum):
63
+ GTC = "Gtc"
64
+ IOC = "Ioc"
65
+ ALO = "Alo" # Post Only
66
+
67
+
68
+ class HyperLiquidFillDirection(Enum):
69
+ OPEN_LONG = "Open Long"
70
+ OPEN_SHORT = "Open Short"
71
+ CLOSE_LONG = "Close Long"
72
+ CLOSE_SHORT = "Close Short"
73
+ BUY = "Buy"
74
+ SELL = "Sell"
75
+
76
+
77
+ class HyperLiquidOrderSide(Enum):
78
+ BUY = "B" # Bid
79
+ SELL = "A" # Ask
80
+
81
+ @property
82
+ def is_buy(self):
83
+ return self == self.BUY
84
+
85
+ @property
86
+ def is_sell(self):
87
+ return self == self.SELL
88
+
89
+
90
+ class HyperLiquidOrderStatusType(Enum):
91
+ """
92
+ https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#query-order-status-by-oid-or-cloid
93
+ """
94
+
95
+ OPEN = "open"
96
+ FILLED = "filled"
97
+ CANCELED = "canceled"
98
+ TRIGGERED = "triggered"
99
+ REJECTED = "rejected"
100
+ MARGIN_CANCELED = "marginCanceled"
101
+ VAULT_WITHDRAWAL_CANCELED = "vaultWithdrawalCanceled"
102
+ OPEN_INTEREST_CAP_CANCELED = "openInterestCapCanceled"
103
+ SELF_TRADE_CANCELED = "selfTradeCanceled"
104
+ REDUCE_ONLY_CANCELED = "reduceOnlyCanceled"
105
+ SIBLING_FILLED_CANCELED = "siblingFilledCanceled"
106
+ DELISTED_CANCELED = "delistedCanceled"
107
+ LIQUIDATED_CANCELED = "liquidatedCanceled"
108
+ SCHEDULED_CANCEL = "scheduledCancel"
109
+ TICK_REJECTED = "tickRejected"
110
+ MIN_TRADE_NTL_REJECTED = "minTradeNtlRejected"
111
+ PERP_MARGIN_REJECTED = "perpMarginRejected"
112
+ REDUCE_ONLY_REJECTED = "reduceOnlyRejected"
113
+ BAD_ALO_PX_REJECTED = "badAloPxRejected"
114
+ IOC_CANCEL_REJECTED = "iocCancelRejected"
115
+ BAD_TRIGGER_PX_REJECTED = "badTriggerPxRejected"
116
+ MARKET_ORDER_NO_LIQUIDITY_REJECTED = "marketOrderNoLiquidityRejected"
117
+ POSITION_INCREASE_AT_OPEN_INTEREST_CAP_REJECTED = (
118
+ "positionIncreaseAtOpenInterestCapRejected"
119
+ )
120
+ POSITION_FLIP_AT_OPEN_INTEREST_CAP_REJECTED = (
121
+ "positionFlipAtOpenInterestCapRejected"
122
+ )
123
+ TOO_AGGRESSIVE_AT_OPEN_INTEREST_CAP_REJECTED = (
124
+ "tooAggressiveAtOpenInterestCapRejected"
125
+ )
126
+ OPEN_INTEREST_INCREASE_REJECTED = "openInterestIncreaseRejected"
127
+ INSUFFICIENT_SPOT_BALANCE_REJECTED = "insufficientSpotBalanceRejected"
128
+ ORACLE_REJECTED = "oracleRejected"
129
+ PERP_MAX_POSITION_REJECTED = "perpMaxPositionRejected"
130
+
131
+
132
+ class HyperLiquidKlineInterval(Enum):
133
+ MINUTE_1 = "1m"
134
+ MINUTE_3 = "3m"
135
+ MINUTE_5 = "5m"
136
+ MINUTE_15 = "15m"
137
+ MINUTE_30 = "30m"
138
+ HOUR_1 = "1h"
139
+ HOUR_2 = "2h"
140
+ HOUR_4 = "4h"
141
+ HOUR_8 = "8h"
142
+ HOUR_12 = "12h"
143
+ DAY_1 = "1d"
144
+ WEEK_1 = "1w"
145
+ MONTH_1 = "1M"
146
+
147
+
148
+ class HyperLiquidRateLimiter:
149
+ """Rate limiter for Hyperliquid API"""
150
+
151
+ def __init__(self, enable_rate_limit: bool = True):
152
+ self._enabled = enable_rate_limit
153
+ self._exchange = Throttled(
154
+ quota=rate_limiter.per_duration(timedelta(seconds=60), limit=1200),
155
+ timeout=-1,
156
+ using=RateLimiterType.FIXED_WINDOW.value,
157
+ )
158
+ self._info = Throttled(
159
+ quota=rate_limiter.per_duration(timedelta(seconds=60), limit=1200),
160
+ timeout=-1,
161
+ using=RateLimiterType.FIXED_WINDOW.value,
162
+ )
163
+
164
+ @staticmethod
165
+ def _raise_if_limited(result, message: str, scope: str, cost: int | None = None):
166
+ if result.limited:
167
+ raise HyperliquidRateLimitError(
168
+ message,
169
+ retry_after=result.state.retry_after,
170
+ scope=scope,
171
+ cost=cost,
172
+ )
173
+
174
+ async def exchange_limit(self, cost: int = 1):
175
+ if not self._enabled:
176
+ return
177
+ result = await self._exchange.limit(key="exchange", cost=cost, timeout=-1)
178
+ self._raise_if_limited(
179
+ result,
180
+ "Hyperliquid exchange rate limit exceeded",
181
+ scope="uid",
182
+ cost=cost,
183
+ )
184
+
185
+ async def info_limit(self, cost: int = 1):
186
+ if not self._enabled:
187
+ return
188
+ result = await self._info.limit(key="info", cost=cost, timeout=60)
189
+ self._raise_if_limited(
190
+ result,
191
+ "Hyperliquid info rate limit exceeded",
192
+ scope="ip",
193
+ cost=cost,
194
+ )
195
+
196
+
197
+ # class HyperLiquidRateLimiterSync:
198
+ # """Synchronous rate limiter for Hyperliquid API"""
199
+
200
+ # def __init__(self, enable_rate_limit: bool = True):
201
+ # self._enabled = enable_rate_limit
202
+ # self._info = ThrottledSync(
203
+ # quota=rate_limiter_sync.per_duration(timedelta(seconds=60), limit=1200),
204
+ # timeout=-1,
205
+ # using=RateLimiterType.FIXED_WINDOW.value,
206
+ # )
207
+
208
+ # @staticmethod
209
+ # def _raise_if_limited(result, message: str, scope: str, cost: int | None = None):
210
+ # if result.limited:
211
+ # raise HyperliquidRateLimitError(
212
+ # message,
213
+ # retry_after=result.state.retry_after,
214
+ # scope=scope,
215
+ # cost=cost,
216
+ # )
217
+
218
+ # def info_limit(self, cost: int = 1):
219
+ # if not self._enabled:
220
+ # return
221
+ # result = self._info.limit(key="info", cost=cost, timeout=120)
222
+ # self._raise_if_limited(
223
+ # result,
224
+ # "Hyperliquid info rate limit exceeded",
225
+ # scope="ip",
226
+ # cost=cost,
227
+ # )
228
+
229
+
230
+ class HyperLiquidOrderLimitTypeRequest(TypedDict):
231
+ tif: str # "Alo" other exchange called post only | Ioc (Immediate or Cancel) | Gtc (Good Till Cancel)
232
+
233
+
234
+ class HyperLiquidOrderTriggerTypeRequest(TypedDict):
235
+ isMarket: bool # True for market order, False for limit order
236
+ triggerPx: str # Trigger price for stop orders, empty for limit orders
237
+ tpsl: str # tp | sl
238
+
239
+
240
+ class HyperLiquidOrderTypeRequest(TypedDict):
241
+ limit: NotRequired[HyperLiquidOrderLimitTypeRequest] # Limit order type
242
+ trigger: NotRequired[HyperLiquidOrderTriggerTypeRequest]
243
+
244
+
245
+ class HyperLiquidOrderRequest(TypedDict):
246
+ """
247
+ HyperLiquid order request schema
248
+
249
+
250
+ a: int asset
251
+ b: bool isBuy
252
+ p: str price
253
+ s: str size
254
+ r: bool reduceOnly
255
+ t: HyperLiquidOrderTypeRequest
256
+ c: NotRequired[str] clientOrderId
257
+ """
258
+
259
+ a: int # asset
260
+ b: bool # isBuy
261
+ p: str # price
262
+ s: str # size
263
+ r: bool # reduceOnly
264
+ t: HyperLiquidOrderTypeRequest
265
+ c: NotRequired[str] # clientOrderId
266
+
267
+
268
+ class HyperLiquidOrderCancelRequest(TypedDict):
269
+ a: int # asset
270
+ o: int # oid # orderId
271
+
272
+
273
+ class HyperLiquidCloidCancelRequest(TypedDict):
274
+ asset: int # asset
275
+ cloid: str # clientOrderId
276
+
277
+
278
+ class HyperLiquidEnumParser:
279
+ _hyperliquid_order_status_map = {
280
+ HyperLiquidOrderStatusType.OPEN: OrderStatus.ACCEPTED,
281
+ HyperLiquidOrderStatusType.FILLED: OrderStatus.FILLED,
282
+ HyperLiquidOrderStatusType.CANCELED: OrderStatus.CANCELED,
283
+ HyperLiquidOrderStatusType.TRIGGERED: OrderStatus.ACCEPTED,
284
+ HyperLiquidOrderStatusType.REJECTED: OrderStatus.FAILED,
285
+ HyperLiquidOrderStatusType.MARGIN_CANCELED: OrderStatus.CANCELED,
286
+ HyperLiquidOrderStatusType.VAULT_WITHDRAWAL_CANCELED: OrderStatus.CANCELED,
287
+ HyperLiquidOrderStatusType.OPEN_INTEREST_CAP_CANCELED: OrderStatus.CANCELED,
288
+ HyperLiquidOrderStatusType.SELF_TRADE_CANCELED: OrderStatus.CANCELED,
289
+ HyperLiquidOrderStatusType.REDUCE_ONLY_CANCELED: OrderStatus.CANCELED,
290
+ HyperLiquidOrderStatusType.SIBLING_FILLED_CANCELED: OrderStatus.CANCELED,
291
+ HyperLiquidOrderStatusType.DELISTED_CANCELED: OrderStatus.CANCELED,
292
+ HyperLiquidOrderStatusType.LIQUIDATED_CANCELED: OrderStatus.CANCELED,
293
+ HyperLiquidOrderStatusType.SCHEDULED_CANCEL: OrderStatus.CANCELED,
294
+ HyperLiquidOrderStatusType.TICK_REJECTED: OrderStatus.FAILED,
295
+ HyperLiquidOrderStatusType.MIN_TRADE_NTL_REJECTED: OrderStatus.FAILED,
296
+ HyperLiquidOrderStatusType.PERP_MARGIN_REJECTED: OrderStatus.FAILED,
297
+ HyperLiquidOrderStatusType.REDUCE_ONLY_REJECTED: OrderStatus.FAILED,
298
+ HyperLiquidOrderStatusType.BAD_ALO_PX_REJECTED: OrderStatus.FAILED,
299
+ HyperLiquidOrderStatusType.IOC_CANCEL_REJECTED: OrderStatus.FAILED,
300
+ HyperLiquidOrderStatusType.BAD_TRIGGER_PX_REJECTED: OrderStatus.FAILED,
301
+ HyperLiquidOrderStatusType.MARKET_ORDER_NO_LIQUIDITY_REJECTED: OrderStatus.FAILED,
302
+ HyperLiquidOrderStatusType.POSITION_INCREASE_AT_OPEN_INTEREST_CAP_REJECTED: OrderStatus.FAILED,
303
+ HyperLiquidOrderStatusType.POSITION_FLIP_AT_OPEN_INTEREST_CAP_REJECTED: OrderStatus.FAILED,
304
+ HyperLiquidOrderStatusType.TOO_AGGRESSIVE_AT_OPEN_INTEREST_CAP_REJECTED: OrderStatus.FAILED,
305
+ HyperLiquidOrderStatusType.OPEN_INTEREST_INCREASE_REJECTED: OrderStatus.FAILED,
306
+ HyperLiquidOrderStatusType.INSUFFICIENT_SPOT_BALANCE_REJECTED: OrderStatus.FAILED,
307
+ HyperLiquidOrderStatusType.ORACLE_REJECTED: OrderStatus.FAILED,
308
+ HyperLiquidOrderStatusType.PERP_MAX_POSITION_REJECTED: OrderStatus.FAILED,
309
+ }
310
+
311
+ _hyperliquid_kline_interval_map = {
312
+ HyperLiquidKlineInterval.MINUTE_1: KlineInterval.MINUTE_1,
313
+ HyperLiquidKlineInterval.MINUTE_3: KlineInterval.MINUTE_3,
314
+ HyperLiquidKlineInterval.MINUTE_5: KlineInterval.MINUTE_5,
315
+ HyperLiquidKlineInterval.MINUTE_15: KlineInterval.MINUTE_15,
316
+ HyperLiquidKlineInterval.MINUTE_30: KlineInterval.MINUTE_30,
317
+ HyperLiquidKlineInterval.HOUR_1: KlineInterval.HOUR_1,
318
+ HyperLiquidKlineInterval.HOUR_2: KlineInterval.HOUR_2,
319
+ HyperLiquidKlineInterval.HOUR_4: KlineInterval.HOUR_4,
320
+ HyperLiquidKlineInterval.HOUR_8: KlineInterval.HOUR_8,
321
+ HyperLiquidKlineInterval.HOUR_12: KlineInterval.HOUR_12,
322
+ HyperLiquidKlineInterval.DAY_1: KlineInterval.DAY_1,
323
+ HyperLiquidKlineInterval.WEEK_1: KlineInterval.WEEK_1,
324
+ HyperLiquidKlineInterval.MONTH_1: KlineInterval.MONTH_1,
325
+ }
326
+
327
+ _kline_interval_to_hyperliquid_map = {
328
+ v: k for k, v in _hyperliquid_kline_interval_map.items()
329
+ }
330
+
331
+ _hyperliquid_time_in_force_map = {
332
+ HyperLiquidTimeInForce.GTC: TimeInForce.GTC,
333
+ HyperLiquidTimeInForce.IOC: TimeInForce.IOC,
334
+ }
335
+
336
+ _time_in_force_to_hyperliquid_map = {
337
+ v: k for k, v in _hyperliquid_time_in_force_map.items()
338
+ }
339
+
340
+ @classmethod
341
+ def parse_order_status(cls, status: HyperLiquidOrderStatusType) -> OrderStatus:
342
+ """Convert HyperLiquidOrderStatusType to OrderStatus"""
343
+ return cls._hyperliquid_order_status_map[status]
344
+
345
+ @classmethod
346
+ def parse_kline_interval(cls, interval: HyperLiquidKlineInterval) -> KlineInterval:
347
+ """Convert KlineInterval to HyperLiquidKlineInterval"""
348
+ return cls._hyperliquid_kline_interval_map[interval]
349
+
350
+ @classmethod
351
+ def to_hyperliquid_kline_interval(
352
+ cls, interval: KlineInterval
353
+ ) -> HyperLiquidKlineInterval:
354
+ """Convert KlineInterval to HyperLiquidKlineInterval"""
355
+ if interval not in cls._kline_interval_to_hyperliquid_map:
356
+ raise KlineSupportedError(
357
+ f"Unsupported kline interval: {interval}. Supported intervals: {list(cls._kline_interval_to_hyperliquid_map.keys())}"
358
+ )
359
+ return cls._kline_interval_to_hyperliquid_map[interval]
360
+
361
+ @classmethod
362
+ def parse_time_in_force(cls, time_in_force: HyperLiquidTimeInForce) -> TimeInForce:
363
+ """Convert HyperLiquidTimeInForce to TimeInForce"""
364
+ return cls._hyperliquid_time_in_force_map[time_in_force]
365
+
366
+ @classmethod
367
+ def to_hyperliquid_time_in_force(
368
+ cls, time_in_force: TimeInForce
369
+ ) -> HyperLiquidTimeInForce:
370
+ """Convert TimeInForce to HyperLiquidTimeInForce"""
371
+ return cls._time_in_force_to_hyperliquid_map[time_in_force]
@@ -0,0 +1,156 @@
1
+ from decimal import Decimal
2
+ from typing import Dict, List, cast
3
+ from walrasquant.constants import AccountType
4
+ from walrasquant.schema import (
5
+ CreateOrderSubmit,
6
+ InstrumentId,
7
+ BatchOrderSubmit,
8
+ BaseMarket,
9
+ )
10
+ from walrasquant.core.cache import AsyncCache
11
+ from walrasquant.core.nautilius_core import MessageBus, LiveClock
12
+ from walrasquant.core.entity import TaskManager
13
+ from walrasquant.core.registry import OrderRegistry
14
+ from walrasquant.exchange.hyperliquid import HyperLiquidAccountType
15
+ from walrasquant.exchange.hyperliquid.schema import HyperLiquidMarket
16
+ from walrasquant.exchange.hyperliquid.constants import oid_to_cloid_hex
17
+ from walrasquant.base import ExecutionManagementSystem
18
+ from walrasquant.base.ems import PriorityOrderQueue
19
+
20
+
21
+ class HyperLiquidExecutionManagementSystem(ExecutionManagementSystem):
22
+ _market: Dict[str, HyperLiquidMarket]
23
+
24
+ HYPER_LIQUID_ACCOUNT_TYPE_PRIORITY = [
25
+ HyperLiquidAccountType.MAINNET,
26
+ HyperLiquidAccountType.TESTNET,
27
+ ]
28
+
29
+ def __init__(
30
+ self,
31
+ market: Dict[str, HyperLiquidMarket],
32
+ cache: AsyncCache,
33
+ msgbus: MessageBus,
34
+ clock: LiveClock,
35
+ task_manager: TaskManager,
36
+ registry: OrderRegistry,
37
+ queue_maxsize: int = 100_000,
38
+ ):
39
+ super().__init__(
40
+ market=market,
41
+ cache=cache,
42
+ msgbus=msgbus,
43
+ clock=clock,
44
+ task_manager=task_manager,
45
+ registry=registry,
46
+ queue_maxsize=queue_maxsize,
47
+ )
48
+ self._hyperliquid_account_type: HyperLiquidAccountType | None = None
49
+
50
+ def _build_order_submit_queues(self):
51
+ for account_type in self._private_connectors.keys():
52
+ if isinstance(account_type, HyperLiquidAccountType):
53
+ self._order_submit_queues[account_type] = PriorityOrderQueue(
54
+ maxsize=self._queue_maxsize
55
+ )
56
+ break
57
+
58
+ def _set_account_type(self):
59
+ account_types = self._private_connectors.keys()
60
+ for account_type in self.HYPER_LIQUID_ACCOUNT_TYPE_PRIORITY:
61
+ if account_type in account_types:
62
+ self._hyperliquid_account_type = account_type
63
+ break
64
+
65
+ def _instrument_id_to_account_type(
66
+ self, instrument_id: InstrumentId
67
+ ) -> AccountType:
68
+ if self._hyperliquid_account_type is None:
69
+ raise ValueError("No HyperLiquid account type configured")
70
+ return self._hyperliquid_account_type
71
+
72
+ def _get_min_order_amount(
73
+ self, symbol: str, market: BaseMarket, px: float
74
+ ) -> Decimal:
75
+ # book = self._cache.bookl1(symbol)
76
+ hyperliquid_market = cast(HyperLiquidMarket, market)
77
+ cost_limits = hyperliquid_market.limits.cost
78
+ min_order_cost = cost_limits.min if cost_limits is not None else 0.0
79
+ min_order_cost = min_order_cost or 0.0
80
+ min_order_amount = super()._amount_to_precision(
81
+ symbol, min_order_cost / px * 1.01, mode="ceil"
82
+ )
83
+ return min_order_amount
84
+
85
+ async def _create_order(
86
+ self, order_submit: CreateOrderSubmit, account_type: AccountType
87
+ ):
88
+ """
89
+ Create an order
90
+ """
91
+ oid = oid_to_cloid_hex(order_submit.oid)
92
+ self._registry.register_order(oid)
93
+ self._cache.add_inflight_order(order_submit.symbol, oid)
94
+ await self._private_connectors[account_type]._oms.create_order(
95
+ oid=oid,
96
+ symbol=order_submit.symbol,
97
+ side=order_submit.side,
98
+ type=order_submit.type,
99
+ amount=order_submit.amount,
100
+ price=order_submit.price,
101
+ time_in_force=order_submit.time_in_force,
102
+ reduce_only=order_submit.reduce_only,
103
+ **order_submit.kwargs,
104
+ )
105
+
106
+ async def _create_batch_orders(
107
+ self, batch_orders: List[BatchOrderSubmit], account_type: AccountType
108
+ ):
109
+ new_batch_orders = []
110
+ for order in batch_orders:
111
+ oid = oid_to_cloid_hex(order.oid)
112
+ self._registry.register_order(oid=oid)
113
+ self._cache.add_inflight_order(order.symbol, oid)
114
+ new_batch_orders.append(
115
+ BatchOrderSubmit(
116
+ symbol=order.symbol,
117
+ instrument_id=order.instrument_id,
118
+ side=order.side,
119
+ type=order.type,
120
+ amount=order.amount,
121
+ price=order.price,
122
+ time_in_force=order.time_in_force,
123
+ reduce_only=order.reduce_only,
124
+ oid=oid,
125
+ kwargs=order.kwargs,
126
+ )
127
+ )
128
+ await self._private_connectors[account_type]._oms.create_batch_orders(
129
+ orders=new_batch_orders,
130
+ )
131
+
132
+ async def _create_order_ws(
133
+ self, order_submit: CreateOrderSubmit, account_type: AccountType
134
+ ):
135
+ """
136
+ Create an order
137
+ """
138
+ oid = oid_to_cloid_hex(order_submit.oid)
139
+ self._registry.register_order(oid)
140
+ self._cache.add_inflight_order(order_submit.symbol, oid)
141
+ await self._private_connectors[account_type]._oms.create_order_ws(
142
+ oid=oid,
143
+ symbol=order_submit.symbol,
144
+ side=order_submit.side,
145
+ type=order_submit.type,
146
+ amount=order_submit.amount,
147
+ price=order_submit.price,
148
+ time_in_force=order_submit.time_in_force,
149
+ reduce_only=order_submit.reduce_only,
150
+ **order_submit.kwargs,
151
+ )
152
+
153
+ def _get_max_order_amount(
154
+ self, symbol: str, market: BaseMarket, is_market: bool, px: float
155
+ ) -> Decimal:
156
+ return Decimal("Infinity")
@@ -0,0 +1,48 @@
1
+ from typing import Any
2
+
3
+
4
+ class HyperLiquidHttpError(Exception):
5
+ def __init__(self, status_code: int, message: str, headers: dict[str, Any]):
6
+ super().__init__(message)
7
+ self.status_code = status_code
8
+ self.message = message
9
+ self.headers = headers
10
+
11
+ def __repr__(self) -> str:
12
+ return f"{type(self).__name__}(status_code={self.status_code}, message='{self.message}')"
13
+
14
+ __str__ = __repr__
15
+
16
+
17
+ class HyperLiquidOrderError(Exception):
18
+ """
19
+ The base class for all HyperLiquid specific errors.
20
+ """
21
+
22
+ def __init__(self, error_type: str, message: str):
23
+ super().__init__(message)
24
+ self.error_type = error_type
25
+ self.message = message
26
+
27
+ def __repr__(self) -> str:
28
+ return f"{type(self).__name__}(error_type={self.error_type}, message='{self.message}')"
29
+
30
+ __str__ = __repr__
31
+
32
+
33
+ class HyperliquidRateLimitError(Exception):
34
+ """
35
+ Raised when Hyperliquid API rate limit is exceeded.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ message: str,
41
+ retry_after: float = 0.0,
42
+ scope: str | None = None,
43
+ cost: int | None = None,
44
+ ):
45
+ super().__init__(message)
46
+ self.retry_after = retry_after
47
+ self.scope = scope
48
+ self.cost = cost
@@ -0,0 +1,120 @@
1
+ import ccxt
2
+ import msgspec
3
+ from typing import Any, Dict, cast
4
+ from walrasquant.base import ExchangeManager
5
+ from walrasquant.config import BasicConfig
6
+ from walrasquant.constants import ConfigType
7
+
8
+ from walrasquant.constants import AccountType
9
+ from walrasquant.schema import InstrumentId
10
+ from walrasquant.error import EngineBuildError
11
+ from walrasquant.exchange.hyperliquid.schema import HyperLiquidMarket
12
+ from walrasquant.exchange.hyperliquid.constants import HyperLiquidAccountType
13
+
14
+
15
+ class HyperLiquidExchangeManager(ExchangeManager):
16
+ api: ccxt.hyperliquid
17
+ market: Dict[str, HyperLiquidMarket]
18
+ market_id: Dict[str, str]
19
+
20
+ def __init__(self, config: ConfigType | None = None):
21
+ resolved_config: ConfigType = {
22
+ "apiKey": config.get("apiKey", "") if config else "",
23
+ "secret": config.get("secret", "") if config else "",
24
+ "exchange_id": config.get("exchange_id", "hyperliquid")
25
+ if config
26
+ else "hyperliquid",
27
+ "sandbox": config.get("sandbox", False) if config else False,
28
+ }
29
+ if config and "password" in config:
30
+ resolved_config["password"] = config["password"]
31
+
32
+ super().__init__(resolved_config)
33
+ extra_config = cast(dict[str, Any], self.config)
34
+ extra_config["walletAddress"] = self.config.get("apiKey")
35
+ extra_config["privateKey"] = self.config.get("secret")
36
+ self._public_conn_account_type: HyperLiquidAccountType | None = None
37
+
38
+ def load_markets(self, reload: bool = False) -> None:
39
+ # NOTE: do not know why ccxt commented out the following code
40
+ # 'hip3': {
41
+ # 'limit': 5, # how many dexes to load max if dexes are not specified
42
+ # 'dex': ['xyz'],
43
+ # },
44
+ options = cast(dict[str, Any], self.api.options or {})
45
+ self.api.options = options
46
+ fetch_markets = cast(dict[str, Any], options.setdefault("fetchMarkets", {}))
47
+ hip3 = cast(dict[str, Any], fetch_markets.setdefault("hip3", {}))
48
+ hip3["dex"] = ["xyz"]
49
+ market = self.api.load_markets(reload=reload)
50
+ mapping = cast(dict[str, Any], options.get("spotCurrencyMapping", {}))
51
+ for symbol, mkt in market.items():
52
+ try:
53
+ mkt_json = msgspec.json.encode(mkt)
54
+ mkt = msgspec.json.decode(mkt_json, type=HyperLiquidMarket)
55
+
56
+ if (
57
+ mkt.spot or mkt.linear or mkt.inverse or mkt.future
58
+ ) and not mkt.option:
59
+ if mkt.spot and mkt.base in mapping:
60
+ continue
61
+
62
+ symbol = self._parse_symbol(mkt, exchange_suffix="HYPERLIQUID")
63
+ mkt.symbol = symbol
64
+ self.market[symbol] = mkt
65
+ self.market_id[mkt.baseName if mkt.swap else mkt.id] = symbol
66
+
67
+ except msgspec.ValidationError as ve:
68
+ self._log.warning(f"Symbol Format Error: {ve}, {symbol}, {mkt}")
69
+ continue
70
+
71
+ def validate_public_connector_config(
72
+ self, account_type: AccountType, basic_config: BasicConfig | None = None
73
+ ) -> None:
74
+ if basic_config is None:
75
+ raise EngineBuildError("basic_config is required for HyperLiquid")
76
+ if not isinstance(account_type, HyperLiquidAccountType):
77
+ raise EngineBuildError(
78
+ f"Expected HyperLiquidAccountType, got {type(account_type)}"
79
+ )
80
+
81
+ if basic_config.testnet != account_type.is_testnet:
82
+ raise EngineBuildError(
83
+ f"The `testnet` setting of HyperLiquid is not consistent with the public connector's account type `{account_type}`."
84
+ )
85
+
86
+ def validate_public_connector_limits(
87
+ self, existing_connectors: Dict[AccountType, Any]
88
+ ) -> None:
89
+ hyperliquid_connectors = [
90
+ c
91
+ for c in existing_connectors.values()
92
+ if hasattr(c, "account_type")
93
+ and isinstance(c.account_type, HyperLiquidAccountType)
94
+ ]
95
+ if len(hyperliquid_connectors) > 1:
96
+ raise EngineBuildError(
97
+ "Only one public connector is supported for HyperLiquid, please remove the extra public connector config."
98
+ )
99
+
100
+ def set_public_connector_account_type(
101
+ self, account_type: HyperLiquidAccountType
102
+ ) -> None:
103
+ """Set the account type for public connector configuration"""
104
+ self._public_conn_account_type = account_type
105
+
106
+ def instrument_id_to_account_type(self, instrument_id: InstrumentId) -> AccountType:
107
+ if self._public_conn_account_type is None:
108
+ raise EngineBuildError(
109
+ "Public connector account type not set for HyperLiquid. Please add HyperLiquid in public_conn_config."
110
+ )
111
+ return self._public_conn_account_type
112
+
113
+
114
+ def main():
115
+ exchange_manager = HyperLiquidExchangeManager()
116
+ print("Markets loaded:", exchange_manager.market)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()