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,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)
|