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.
- walrasquant/__init__.py +7 -0
- walrasquant/aggregation.py +449 -0
- walrasquant/backends/__init__.py +5 -0
- walrasquant/backends/db.py +109 -0
- walrasquant/backends/db_memory.py +61 -0
- walrasquant/backends/db_postgresql.py +321 -0
- walrasquant/backends/db_sqlite.py +310 -0
- walrasquant/base/__init__.py +24 -0
- walrasquant/base/api_client.py +46 -0
- walrasquant/base/connector.py +863 -0
- walrasquant/base/ems.py +794 -0
- walrasquant/base/exchange.py +213 -0
- walrasquant/base/oms.py +428 -0
- walrasquant/base/retry.py +220 -0
- walrasquant/base/sms.py +545 -0
- walrasquant/base/ws_client.py +408 -0
- walrasquant/config.py +284 -0
- walrasquant/constants.py +413 -0
- walrasquant/core/__init__.py +0 -0
- walrasquant/core/cache.py +688 -0
- walrasquant/core/clock.py +59 -0
- walrasquant/core/connection.py +41 -0
- walrasquant/core/entity.py +504 -0
- walrasquant/core/nautilius_core.py +103 -0
- walrasquant/core/registry.py +41 -0
- walrasquant/engine.py +745 -0
- walrasquant/error.py +34 -0
- walrasquant/exchange/__init__.py +13 -0
- walrasquant/exchange/base_factory.py +172 -0
- walrasquant/exchange/binance/__init__.py +30 -0
- walrasquant/exchange/binance/connector.py +1093 -0
- walrasquant/exchange/binance/constants.py +934 -0
- walrasquant/exchange/binance/ems.py +140 -0
- walrasquant/exchange/binance/error.py +48 -0
- walrasquant/exchange/binance/exchange.py +144 -0
- walrasquant/exchange/binance/factory.py +115 -0
- walrasquant/exchange/binance/oms.py +1807 -0
- walrasquant/exchange/binance/rest_api.py +1653 -0
- walrasquant/exchange/binance/schema.py +1063 -0
- walrasquant/exchange/binance/websockets.py +389 -0
- walrasquant/exchange/bitget/__init__.py +28 -0
- walrasquant/exchange/bitget/connector.py +578 -0
- walrasquant/exchange/bitget/constants.py +392 -0
- walrasquant/exchange/bitget/ems.py +202 -0
- walrasquant/exchange/bitget/error.py +36 -0
- walrasquant/exchange/bitget/exchange.py +128 -0
- walrasquant/exchange/bitget/factory.py +135 -0
- walrasquant/exchange/bitget/oms.py +1619 -0
- walrasquant/exchange/bitget/rest_api.py +610 -0
- walrasquant/exchange/bitget/schema.py +885 -0
- walrasquant/exchange/bitget/websockets.py +753 -0
- walrasquant/exchange/bybit/__init__.py +32 -0
- walrasquant/exchange/bybit/connector.py +819 -0
- walrasquant/exchange/bybit/constants.py +479 -0
- walrasquant/exchange/bybit/ems.py +93 -0
- walrasquant/exchange/bybit/error.py +36 -0
- walrasquant/exchange/bybit/exchange.py +108 -0
- walrasquant/exchange/bybit/factory.py +128 -0
- walrasquant/exchange/bybit/oms.py +1195 -0
- walrasquant/exchange/bybit/rest_api.py +570 -0
- walrasquant/exchange/bybit/schema.py +867 -0
- walrasquant/exchange/bybit/websockets.py +307 -0
- walrasquant/exchange/hyperliquid/__init__.py +28 -0
- walrasquant/exchange/hyperliquid/connector.py +370 -0
- walrasquant/exchange/hyperliquid/constants.py +371 -0
- walrasquant/exchange/hyperliquid/ems.py +156 -0
- walrasquant/exchange/hyperliquid/error.py +48 -0
- walrasquant/exchange/hyperliquid/exchange.py +120 -0
- walrasquant/exchange/hyperliquid/factory.py +135 -0
- walrasquant/exchange/hyperliquid/oms.py +1081 -0
- walrasquant/exchange/hyperliquid/rest_api.py +348 -0
- walrasquant/exchange/hyperliquid/schema.py +583 -0
- walrasquant/exchange/hyperliquid/websockets.py +592 -0
- walrasquant/exchange/okx/__init__.py +25 -0
- walrasquant/exchange/okx/connector.py +931 -0
- walrasquant/exchange/okx/constants.py +518 -0
- walrasquant/exchange/okx/ems.py +144 -0
- walrasquant/exchange/okx/error.py +66 -0
- walrasquant/exchange/okx/exchange.py +102 -0
- walrasquant/exchange/okx/factory.py +138 -0
- walrasquant/exchange/okx/oms.py +1199 -0
- walrasquant/exchange/okx/rest_api.py +799 -0
- walrasquant/exchange/okx/schema.py +1449 -0
- walrasquant/exchange/okx/websockets.py +420 -0
- walrasquant/exchange/registry.py +201 -0
- walrasquant/execution/__init__.py +24 -0
- walrasquant/execution/algorithm.py +968 -0
- walrasquant/execution/algorithms/__init__.py +3 -0
- walrasquant/execution/algorithms/twap.py +392 -0
- walrasquant/execution/config.py +34 -0
- walrasquant/execution/constants.py +27 -0
- walrasquant/execution/schema.py +62 -0
- walrasquant/indicator.py +382 -0
- walrasquant/push.py +77 -0
- walrasquant/schema.py +755 -0
- walrasquant/strategy.py +1805 -0
- walrasquant/tools/__init__.py +0 -0
- walrasquant/tools/pm2_wrapper.py +1016 -0
- walrasquant/web/__init__.py +26 -0
- walrasquant/web/app.py +157 -0
- walrasquant/web/server.py +92 -0
- walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
- walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
- walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
- 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()
|