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,592 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
import picows
|
|
3
|
+
import eth_account
|
|
4
|
+
from eth_account.signers.local import LocalAccount
|
|
5
|
+
from eth_account.messages import encode_typed_data
|
|
6
|
+
from Crypto.Hash import keccak
|
|
7
|
+
from typing import Any, Callable, List, Dict, Literal
|
|
8
|
+
|
|
9
|
+
from walrasquant.base import WSClient
|
|
10
|
+
from walrasquant.core.entity import TaskManager
|
|
11
|
+
from walrasquant.exchange.hyperliquid.schema import HyperLiquidWsMessageGeneral
|
|
12
|
+
from walrasquant.core.nautilius_core import LiveClock
|
|
13
|
+
from walrasquant.exchange.hyperliquid.constants import (
|
|
14
|
+
HyperLiquidAccountType,
|
|
15
|
+
HyperLiquidKlineInterval,
|
|
16
|
+
HyperLiquidRateLimiter,
|
|
17
|
+
HyperLiquidOrderRequest,
|
|
18
|
+
HyperLiquidOrderCancelRequest,
|
|
19
|
+
HyperLiquidCloidCancelRequest,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def user_api_pong_callback(self, frame: picows.WSFrame) -> bool:
|
|
24
|
+
if frame.msg_type != picows.WSMsgType.TEXT:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
raw = frame.get_payload_as_bytes()
|
|
28
|
+
try:
|
|
29
|
+
message = msgspec.json.decode(raw, type=HyperLiquidWsMessageGeneral)
|
|
30
|
+
return message.channel == "pong"
|
|
31
|
+
except msgspec.DecodeError:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HyperLiquidWSClient(WSClient):
|
|
36
|
+
_api_key: str | None
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
account_type: HyperLiquidAccountType,
|
|
41
|
+
handler: Callable[..., Any],
|
|
42
|
+
task_manager: TaskManager,
|
|
43
|
+
clock: LiveClock,
|
|
44
|
+
api_key: str | None = None, # in HyperLiquid, api_key is the wallet address
|
|
45
|
+
custom_url: str | None = None,
|
|
46
|
+
max_subscriptions_per_client: int | None = None,
|
|
47
|
+
max_clients: int | None = None,
|
|
48
|
+
):
|
|
49
|
+
self._account_type = account_type
|
|
50
|
+
|
|
51
|
+
if custom_url:
|
|
52
|
+
url = custom_url
|
|
53
|
+
else:
|
|
54
|
+
url = account_type.ws_url
|
|
55
|
+
|
|
56
|
+
self._api_key = api_key
|
|
57
|
+
|
|
58
|
+
super().__init__(
|
|
59
|
+
url=url,
|
|
60
|
+
handler=handler,
|
|
61
|
+
task_manager=task_manager,
|
|
62
|
+
clock=clock,
|
|
63
|
+
ping_idle_timeout=30,
|
|
64
|
+
ping_reply_timeout=5,
|
|
65
|
+
specific_ping_msg=msgspec.json.encode({"method": "ping"}),
|
|
66
|
+
auto_ping_strategy="ping_when_idle",
|
|
67
|
+
user_pong_callback=user_api_pong_callback,
|
|
68
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
69
|
+
max_clients=max_clients,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _send_msg(
|
|
73
|
+
self,
|
|
74
|
+
msg: Dict[str, str],
|
|
75
|
+
method: str = "subscribe",
|
|
76
|
+
client_id: int | None = None,
|
|
77
|
+
):
|
|
78
|
+
self.send(
|
|
79
|
+
{
|
|
80
|
+
"method": method,
|
|
81
|
+
"subscription": msg,
|
|
82
|
+
},
|
|
83
|
+
client_id=client_id,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _subscribe(self, msgs: List[Dict[str, str]]):
|
|
87
|
+
assigned = self._register_subscriptions(msgs)
|
|
88
|
+
if not assigned:
|
|
89
|
+
return
|
|
90
|
+
for client_id, client_msgs in assigned.items():
|
|
91
|
+
for msg in client_msgs:
|
|
92
|
+
format_msg = ".".join(msg.values())
|
|
93
|
+
self._log.debug(f"Subscribing to {format_msg}...")
|
|
94
|
+
if self._is_client_connected(client_id):
|
|
95
|
+
for msg in client_msgs:
|
|
96
|
+
self._send_msg(msg, client_id=client_id)
|
|
97
|
+
|
|
98
|
+
def _unsubscribe(self, msgs: List[Dict[str, str]]):
|
|
99
|
+
removed = self._unregister_subscriptions(msgs)
|
|
100
|
+
if not removed:
|
|
101
|
+
return
|
|
102
|
+
for client_id, client_msgs in removed.items():
|
|
103
|
+
for msg in client_msgs:
|
|
104
|
+
format_msg = ".".join(msg.values())
|
|
105
|
+
self._log.debug(f"Unsubscribing from {format_msg}...")
|
|
106
|
+
self._send_msg(msg, method="unsubscribe", client_id=client_id)
|
|
107
|
+
|
|
108
|
+
async def _resubscribe_for_client(
|
|
109
|
+
self, client_id: int, subscriptions: List[Dict[str, str]]
|
|
110
|
+
):
|
|
111
|
+
if not subscriptions:
|
|
112
|
+
return
|
|
113
|
+
for msg in subscriptions:
|
|
114
|
+
self._send_msg(msg, client_id=client_id)
|
|
115
|
+
|
|
116
|
+
def subscribe_trades(self, symbols: List[str]):
|
|
117
|
+
msgs = [{"type": "trades", "coin": symbol} for symbol in symbols]
|
|
118
|
+
self._subscribe(msgs)
|
|
119
|
+
|
|
120
|
+
def unsubscribe_trades(self, symbols: List[str]):
|
|
121
|
+
msgs = [{"type": "trades", "coin": symbol} for symbol in symbols]
|
|
122
|
+
self._unsubscribe(msgs)
|
|
123
|
+
|
|
124
|
+
def subscribe_bbo(self, symbols: List[str]):
|
|
125
|
+
msgs = [{"type": "bbo", "coin": symbol} for symbol in symbols]
|
|
126
|
+
self._subscribe(msgs)
|
|
127
|
+
|
|
128
|
+
def unsubscribe_bbo(self, symbols: List[str]):
|
|
129
|
+
msgs = [{"type": "bbo", "coin": symbol} for symbol in symbols]
|
|
130
|
+
self._unsubscribe(msgs)
|
|
131
|
+
|
|
132
|
+
def subscribe_l2book(self, symbols: List[str]):
|
|
133
|
+
msgs = [{"type": "l2Book", "coin": symbol} for symbol in symbols]
|
|
134
|
+
self._subscribe(msgs)
|
|
135
|
+
|
|
136
|
+
def unsubscribe_l2book(self, symbols: List[str]):
|
|
137
|
+
msgs = [{"type": "l2Book", "coin": symbol} for symbol in symbols]
|
|
138
|
+
self._unsubscribe(msgs)
|
|
139
|
+
|
|
140
|
+
def subscribe_candle(self, symbols: List[str], interval: HyperLiquidKlineInterval):
|
|
141
|
+
msgs = [
|
|
142
|
+
{"type": "candle", "coin": symbol, "interval": interval.value}
|
|
143
|
+
for symbol in symbols
|
|
144
|
+
]
|
|
145
|
+
self._subscribe(msgs)
|
|
146
|
+
|
|
147
|
+
def unsubscribe_candle(
|
|
148
|
+
self, symbols: List[str], interval: HyperLiquidKlineInterval
|
|
149
|
+
):
|
|
150
|
+
msgs = [
|
|
151
|
+
{"type": "candle", "coin": symbol, "interval": interval.value}
|
|
152
|
+
for symbol in symbols
|
|
153
|
+
]
|
|
154
|
+
self._unsubscribe(msgs)
|
|
155
|
+
|
|
156
|
+
def subscribe_order_updates(self):
|
|
157
|
+
if self._api_key is None:
|
|
158
|
+
raise ValueError("api_key is required for user subscriptions")
|
|
159
|
+
msg = {
|
|
160
|
+
"type": "orderUpdates",
|
|
161
|
+
"user": self._api_key,
|
|
162
|
+
}
|
|
163
|
+
self._subscribe([msg])
|
|
164
|
+
|
|
165
|
+
def subscribe_user_events(self):
|
|
166
|
+
if self._api_key is None:
|
|
167
|
+
raise ValueError("api_key is required for user subscriptions")
|
|
168
|
+
msg = {
|
|
169
|
+
"type": "userEvents",
|
|
170
|
+
"user": self._api_key,
|
|
171
|
+
}
|
|
172
|
+
self._subscribe([msg])
|
|
173
|
+
|
|
174
|
+
def subscribe_user_fills(self):
|
|
175
|
+
if self._api_key is None:
|
|
176
|
+
raise ValueError("api_key is required for user subscriptions")
|
|
177
|
+
msg = {
|
|
178
|
+
"type": "userFills",
|
|
179
|
+
"user": self._api_key,
|
|
180
|
+
}
|
|
181
|
+
self._subscribe([msg])
|
|
182
|
+
|
|
183
|
+
def subscribe_user_fundings(self):
|
|
184
|
+
if self._api_key is None:
|
|
185
|
+
raise ValueError("api_key is required for user subscriptions")
|
|
186
|
+
msg = {
|
|
187
|
+
"type": "userFundings",
|
|
188
|
+
"user": self._api_key,
|
|
189
|
+
}
|
|
190
|
+
self._subscribe([msg])
|
|
191
|
+
|
|
192
|
+
def subscribe_user_non_funding_ledger_updates(self):
|
|
193
|
+
if self._api_key is None:
|
|
194
|
+
raise ValueError("api_key is required for user subscriptions")
|
|
195
|
+
msg = {
|
|
196
|
+
"type": "userNonFundingLedgerUpdates",
|
|
197
|
+
"user": self._api_key,
|
|
198
|
+
}
|
|
199
|
+
self._subscribe([msg])
|
|
200
|
+
|
|
201
|
+
def subscribe_web_data2(self):
|
|
202
|
+
if self._api_key is None:
|
|
203
|
+
raise ValueError("api_key is required for user subscriptions")
|
|
204
|
+
msg = {
|
|
205
|
+
"type": "webData2",
|
|
206
|
+
"user": self._api_key,
|
|
207
|
+
}
|
|
208
|
+
self._subscribe([msg])
|
|
209
|
+
|
|
210
|
+
def subscribe_notification(self):
|
|
211
|
+
if self._api_key is None:
|
|
212
|
+
raise ValueError("api_key is required for user subscriptions")
|
|
213
|
+
msg = {
|
|
214
|
+
"type": "notification",
|
|
215
|
+
"user": self._api_key,
|
|
216
|
+
}
|
|
217
|
+
self._subscribe([msg])
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class HyperLiquidWSApiClient(WSClient):
|
|
221
|
+
"""WebSocket API client for HyperLiquid order operations"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
account_type: HyperLiquidAccountType,
|
|
226
|
+
api_key: str,
|
|
227
|
+
secret: str,
|
|
228
|
+
handler: Callable[..., Any],
|
|
229
|
+
task_manager: TaskManager,
|
|
230
|
+
clock: LiveClock,
|
|
231
|
+
enable_rate_limit: bool = True,
|
|
232
|
+
):
|
|
233
|
+
self._api_key = api_key
|
|
234
|
+
self._secret = secret
|
|
235
|
+
self._account_type = account_type
|
|
236
|
+
self._testnet = account_type.is_testnet
|
|
237
|
+
|
|
238
|
+
if secret:
|
|
239
|
+
self._eth_account: LocalAccount = eth_account.Account.from_key(secret)
|
|
240
|
+
|
|
241
|
+
url = account_type.ws_url
|
|
242
|
+
self._limiter = HyperLiquidRateLimiter(enable_rate_limit=enable_rate_limit)
|
|
243
|
+
|
|
244
|
+
# UUID to integer mapping for HyperLiquid API
|
|
245
|
+
self._oid_to_id: Dict[str, int] = {}
|
|
246
|
+
self._id_to_oid: Dict[int, str] = {}
|
|
247
|
+
self._next_id = 1
|
|
248
|
+
|
|
249
|
+
super().__init__(
|
|
250
|
+
url=url,
|
|
251
|
+
handler=handler,
|
|
252
|
+
task_manager=task_manager,
|
|
253
|
+
clock=clock,
|
|
254
|
+
ping_idle_timeout=30,
|
|
255
|
+
ping_reply_timeout=5,
|
|
256
|
+
specific_ping_msg=msgspec.json.encode({"method": "ping"}),
|
|
257
|
+
auto_ping_strategy="ping_when_idle",
|
|
258
|
+
user_pong_callback=user_api_pong_callback,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
async def _resubscribe_for_client(self, client_id: int, subscriptions: List[Any]):
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
def _get_rate_limit_cost(self, length: int, cost: int = 1) -> int:
|
|
265
|
+
"""Get rate limit cost for an operation
|
|
266
|
+
|
|
267
|
+
Please refer to https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/rate-limits-and-user-limits
|
|
268
|
+
"""
|
|
269
|
+
return cost + length // 40
|
|
270
|
+
|
|
271
|
+
def _oid_to_int(self, oid_str: str) -> int:
|
|
272
|
+
"""Convert oid to integer for HyperLiquid API compatibility"""
|
|
273
|
+
if oid_str not in self._oid_to_id:
|
|
274
|
+
self._oid_to_id[oid_str] = self._next_id
|
|
275
|
+
self._id_to_oid[self._next_id] = oid_str
|
|
276
|
+
self._next_id += 1
|
|
277
|
+
return self._oid_to_id[oid_str]
|
|
278
|
+
|
|
279
|
+
def _int_to_oid(self, id_int: int) -> str:
|
|
280
|
+
"""Convert integer back to oid"""
|
|
281
|
+
return self._id_to_oid.get(id_int, str(id_int))
|
|
282
|
+
|
|
283
|
+
def _construct_phantom_agent(self, hash_bytes: bytes) -> Dict[str, Any]:
|
|
284
|
+
"""Construct phantom agent for signature"""
|
|
285
|
+
return {"source": "b" if self._testnet else "a", "connectionId": hash_bytes}
|
|
286
|
+
|
|
287
|
+
def _action_hash(
|
|
288
|
+
self, action: Dict[str, Any], nonce: int, vault_address: str | None = None
|
|
289
|
+
) -> bytes:
|
|
290
|
+
"""Generate action hash for signature"""
|
|
291
|
+
data = msgspec.msgpack.encode(action)
|
|
292
|
+
data += nonce.to_bytes(8, "big")
|
|
293
|
+
if vault_address is None:
|
|
294
|
+
data += b"\x00"
|
|
295
|
+
else:
|
|
296
|
+
data += b"\x01"
|
|
297
|
+
data += bytes.fromhex(
|
|
298
|
+
vault_address[2:] if vault_address.startswith("0x") else vault_address
|
|
299
|
+
)
|
|
300
|
+
return keccak.new(digest_bits=256, data=data).digest()
|
|
301
|
+
|
|
302
|
+
def _sign_l1_action(
|
|
303
|
+
self, action: Dict[str, Any], nonce: int, vault_address: str | None = None
|
|
304
|
+
) -> Dict[str, Any]:
|
|
305
|
+
"""Sign L1 action for authentication"""
|
|
306
|
+
hash_bytes = self._action_hash(action, nonce, vault_address)
|
|
307
|
+
phantom_agent = self._construct_phantom_agent(hash_bytes)
|
|
308
|
+
encoded_data = encode_typed_data(
|
|
309
|
+
full_message={
|
|
310
|
+
"domain": {
|
|
311
|
+
"chainId": 1337,
|
|
312
|
+
"name": "Exchange",
|
|
313
|
+
"verifyingContract": "0x0000000000000000000000000000000000000000",
|
|
314
|
+
"version": "1",
|
|
315
|
+
},
|
|
316
|
+
"types": {
|
|
317
|
+
"Agent": [
|
|
318
|
+
{"name": "source", "type": "string"},
|
|
319
|
+
{"name": "connectionId", "type": "bytes32"},
|
|
320
|
+
],
|
|
321
|
+
"EIP712Domain": [
|
|
322
|
+
{"name": "name", "type": "string"},
|
|
323
|
+
{"name": "version", "type": "string"},
|
|
324
|
+
{"name": "chainId", "type": "uint256"},
|
|
325
|
+
{"name": "verifyingContract", "type": "address"},
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
"primaryType": "Agent",
|
|
329
|
+
"message": phantom_agent,
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
signed = self._eth_account.sign_message(encoded_data)
|
|
333
|
+
return {
|
|
334
|
+
"r": f"{signed.r:#x}",
|
|
335
|
+
"s": f"{signed.s:#x}",
|
|
336
|
+
"v": signed.v,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
def _submit(
|
|
340
|
+
self,
|
|
341
|
+
oid: str,
|
|
342
|
+
request_type: Literal["info", "action"],
|
|
343
|
+
payload: Dict[str, Any],
|
|
344
|
+
):
|
|
345
|
+
"""Submit request to HyperLiquid WebSocket API"""
|
|
346
|
+
message_id = self._oid_to_int(oid)
|
|
347
|
+
message = {
|
|
348
|
+
"method": "post",
|
|
349
|
+
"id": message_id,
|
|
350
|
+
"request": {"type": request_type, "payload": payload},
|
|
351
|
+
}
|
|
352
|
+
self.send(message)
|
|
353
|
+
|
|
354
|
+
async def place_order(
|
|
355
|
+
self,
|
|
356
|
+
id: str,
|
|
357
|
+
orders: List[HyperLiquidOrderRequest],
|
|
358
|
+
grouping: Literal["na", "normalTpsl", "positionTpsl"] = "na",
|
|
359
|
+
):
|
|
360
|
+
"""Place orders via WebSocket"""
|
|
361
|
+
nonce = self._clock.timestamp_ms()
|
|
362
|
+
order_action = {
|
|
363
|
+
"type": "order",
|
|
364
|
+
"orders": orders,
|
|
365
|
+
"grouping": grouping,
|
|
366
|
+
}
|
|
367
|
+
signature = self._sign_l1_action(order_action, nonce, vault_address=None)
|
|
368
|
+
|
|
369
|
+
payload = {
|
|
370
|
+
"action": order_action,
|
|
371
|
+
"nonce": nonce,
|
|
372
|
+
"signature": signature,
|
|
373
|
+
}
|
|
374
|
+
cost = self._get_rate_limit_cost(length=len(orders), cost=1)
|
|
375
|
+
await self._limiter.exchange_limit(cost=cost)
|
|
376
|
+
self._submit(oid=id, request_type="action", payload=payload)
|
|
377
|
+
|
|
378
|
+
async def cancel_order(
|
|
379
|
+
self,
|
|
380
|
+
id: str,
|
|
381
|
+
cancels: List[HyperLiquidOrderCancelRequest],
|
|
382
|
+
):
|
|
383
|
+
"""Cancel orders via WebSocket"""
|
|
384
|
+
nonce = self._clock.timestamp_ms()
|
|
385
|
+
cancel_action = {
|
|
386
|
+
"type": "cancel",
|
|
387
|
+
"cancels": cancels,
|
|
388
|
+
}
|
|
389
|
+
signature = self._sign_l1_action(cancel_action, nonce, vault_address=None)
|
|
390
|
+
|
|
391
|
+
payload = {
|
|
392
|
+
"action": cancel_action,
|
|
393
|
+
"nonce": nonce,
|
|
394
|
+
"signature": signature,
|
|
395
|
+
}
|
|
396
|
+
# cost = self._get_rate_limit_cost(length=len(cancels), cost=1)
|
|
397
|
+
# await self._limiter.exchange_limit(cost=cost)
|
|
398
|
+
self._submit(oid=id, request_type="action", payload=payload)
|
|
399
|
+
|
|
400
|
+
async def cancel_orders_by_cloid(
|
|
401
|
+
self,
|
|
402
|
+
id: str,
|
|
403
|
+
cancels: List[HyperLiquidCloidCancelRequest],
|
|
404
|
+
):
|
|
405
|
+
nounce = self._clock.timestamp_ms()
|
|
406
|
+
orderAction = {
|
|
407
|
+
"type": "cancelByCloid",
|
|
408
|
+
"cancels": cancels,
|
|
409
|
+
}
|
|
410
|
+
signature = self._sign_l1_action(orderAction, nounce, vault_address=None)
|
|
411
|
+
payload = {
|
|
412
|
+
"action": orderAction,
|
|
413
|
+
"nonce": nounce,
|
|
414
|
+
"signature": signature,
|
|
415
|
+
}
|
|
416
|
+
# cost = self._get_rate_limit_cost(length=len(cancels), cost=1)
|
|
417
|
+
# await self._limiter.exchange_limit(cost=cost)
|
|
418
|
+
self._submit(oid=id, request_type="action", payload=payload)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# import asyncio # noqa
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# async def main():
|
|
425
|
+
# from walrasquant.constants import settings
|
|
426
|
+
# from walrasquant.exchange.hyperliquid.constants import oid_to_cloid_hex
|
|
427
|
+
# from walrasquant.core.entity import TaskManager, OidGen
|
|
428
|
+
# from walrasquant.core.nautilius_core import LiveClock, setup_nexus_core
|
|
429
|
+
|
|
430
|
+
# HYPER_API_KEY = settings.HYPER.TESTNET.API_KEY
|
|
431
|
+
# HYPER_SECRET = settings.HYPER.TESTNET.SECRET
|
|
432
|
+
|
|
433
|
+
# log_guard, _, clock = setup_nexus_core( # noqa
|
|
434
|
+
# trader_id="hyper-test",
|
|
435
|
+
# level_stdout="DEBUG",
|
|
436
|
+
# )
|
|
437
|
+
|
|
438
|
+
# oidgen = OidGen(clock)
|
|
439
|
+
|
|
440
|
+
# task_manager = TaskManager(
|
|
441
|
+
# loop=asyncio.get_event_loop(),
|
|
442
|
+
# )
|
|
443
|
+
|
|
444
|
+
# ws_api_client = HyperLiquidWSApiClient(
|
|
445
|
+
# account_type=HyperLiquidAccountType.TESTNET,
|
|
446
|
+
# api_key=HYPER_API_KEY,
|
|
447
|
+
# secret=HYPER_SECRET,
|
|
448
|
+
# handler=lambda msg: print(msg),
|
|
449
|
+
# task_manager=task_manager,
|
|
450
|
+
# clock=LiveClock(),
|
|
451
|
+
# enable_rate_limit=True,
|
|
452
|
+
# )
|
|
453
|
+
# oid = oid_to_cloid_hex(oidgen.oid)
|
|
454
|
+
# await ws_api_client.connect()
|
|
455
|
+
# await ws_api_client.place_order(
|
|
456
|
+
# id=oid,
|
|
457
|
+
# orders=[
|
|
458
|
+
# {
|
|
459
|
+
# "a": 4,
|
|
460
|
+
# "b": True,
|
|
461
|
+
# "p": "4500",
|
|
462
|
+
# "s": "0.003",
|
|
463
|
+
# "r": False,
|
|
464
|
+
# "t": {
|
|
465
|
+
# "limit": {
|
|
466
|
+
# "tif": "Gtc"
|
|
467
|
+
# }
|
|
468
|
+
# },
|
|
469
|
+
# "c": oid,
|
|
470
|
+
# }
|
|
471
|
+
# ]
|
|
472
|
+
# )
|
|
473
|
+
# await ws_api_client.cancel_order(
|
|
474
|
+
# id=UUID4().value, cancels=[{"a": 4, "o": 38086199157}]
|
|
475
|
+
# )
|
|
476
|
+
# await ws_api_client.cancel_orders_by_cloid(
|
|
477
|
+
# id="0x0000000000000000f3f0b3863b5d5b86",
|
|
478
|
+
# cancels=[{"asset": 4, "cloid": "0x0000000000000000f3f0b3863b5d5b86"}],
|
|
479
|
+
# )
|
|
480
|
+
# await task_manager.wait()
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# place order success
|
|
484
|
+
# {
|
|
485
|
+
# "channel": "post",
|
|
486
|
+
# "data": {
|
|
487
|
+
# "id": 1,
|
|
488
|
+
# "response": {
|
|
489
|
+
# "type": "action",
|
|
490
|
+
# "payload": {
|
|
491
|
+
# "status": "ok",
|
|
492
|
+
# "response": {"type": "cancel", "data": {"statuses": ["success"]}},
|
|
493
|
+
# },
|
|
494
|
+
# },
|
|
495
|
+
# },
|
|
496
|
+
# }
|
|
497
|
+
# # cancel order success
|
|
498
|
+
# {
|
|
499
|
+
# "channel": "post",
|
|
500
|
+
# "data": {
|
|
501
|
+
# "id": 1,
|
|
502
|
+
# "response": {
|
|
503
|
+
# "type": "action",
|
|
504
|
+
# "payload": {
|
|
505
|
+
# "status": "ok",
|
|
506
|
+
# "response": {
|
|
507
|
+
# "type": "order",
|
|
508
|
+
# "data": {
|
|
509
|
+
# "statuses": [
|
|
510
|
+
# {"error": "Order must have minimum value of $10. asset=4"}
|
|
511
|
+
# ]
|
|
512
|
+
# },
|
|
513
|
+
# },
|
|
514
|
+
# },
|
|
515
|
+
# },
|
|
516
|
+
# },
|
|
517
|
+
# }
|
|
518
|
+
# # place order failed
|
|
519
|
+
# {
|
|
520
|
+
# "channel": "post",
|
|
521
|
+
# "data": {
|
|
522
|
+
# "id": 1,
|
|
523
|
+
# "response": {
|
|
524
|
+
# "type": "action",
|
|
525
|
+
# "payload": {
|
|
526
|
+
# "status": "ok",
|
|
527
|
+
# "response": {
|
|
528
|
+
# "type": "order",
|
|
529
|
+
# "data": {
|
|
530
|
+
# "statuses": [
|
|
531
|
+
# {"error": "Order must have minimum value of $10. asset=4"}
|
|
532
|
+
# ]
|
|
533
|
+
# },
|
|
534
|
+
# },
|
|
535
|
+
# },
|
|
536
|
+
# },
|
|
537
|
+
# },
|
|
538
|
+
# }
|
|
539
|
+
# # place order success, you only need to get oid
|
|
540
|
+
# {
|
|
541
|
+
# "channel": "post",
|
|
542
|
+
# "data": {
|
|
543
|
+
# "id": 1,
|
|
544
|
+
# "response": {
|
|
545
|
+
# "type": "action",
|
|
546
|
+
# "payload": {
|
|
547
|
+
# "status": "ok",
|
|
548
|
+
# "response": {
|
|
549
|
+
# "type": "order",
|
|
550
|
+
# "data": {
|
|
551
|
+
# "statuses": [
|
|
552
|
+
# {
|
|
553
|
+
# "filled": {
|
|
554
|
+
# "totalSz": "0.003",
|
|
555
|
+
# "avgPx": "4432.9",
|
|
556
|
+
# "oid": 38086199157,
|
|
557
|
+
# }
|
|
558
|
+
# }
|
|
559
|
+
# ]
|
|
560
|
+
# },
|
|
561
|
+
# },
|
|
562
|
+
# },
|
|
563
|
+
# },
|
|
564
|
+
# },
|
|
565
|
+
# }
|
|
566
|
+
# # cancel order failed
|
|
567
|
+
# {
|
|
568
|
+
# "channel": "post",
|
|
569
|
+
# "data": {
|
|
570
|
+
# "id": 1,
|
|
571
|
+
# "response": {
|
|
572
|
+
# "type": "action",
|
|
573
|
+
# "payload": {
|
|
574
|
+
# "status": "ok",
|
|
575
|
+
# "response": {
|
|
576
|
+
# "type": "cancel",
|
|
577
|
+
# "data": {
|
|
578
|
+
# "statuses": [
|
|
579
|
+
# {
|
|
580
|
+
# "error": "Order was never placed, already canceled, or filled. asset=4"
|
|
581
|
+
# }
|
|
582
|
+
# ]
|
|
583
|
+
# },
|
|
584
|
+
# },
|
|
585
|
+
# },
|
|
586
|
+
# },
|
|
587
|
+
# },
|
|
588
|
+
# }
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# if __name__ == "__main__":
|
|
592
|
+
# asyncio.run(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from walrasquant.exchange.okx.constants import OkxAccountType
|
|
2
|
+
from walrasquant.exchange.okx.exchange import OkxExchangeManager
|
|
3
|
+
from walrasquant.exchange.okx.connector import OkxPublicConnector, OkxPrivateConnector
|
|
4
|
+
from walrasquant.exchange.okx.ems import OkxExecutionManagementSystem
|
|
5
|
+
from walrasquant.exchange.okx.oms import OkxOrderManagementSystem
|
|
6
|
+
from walrasquant.exchange.okx.factory import OkxFactory
|
|
7
|
+
|
|
8
|
+
# Auto-register factory on import
|
|
9
|
+
try:
|
|
10
|
+
from walrasquant.exchange.registry import register_factory
|
|
11
|
+
|
|
12
|
+
register_factory(OkxFactory())
|
|
13
|
+
except ImportError:
|
|
14
|
+
# Registry not available yet during bootstrap
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"OkxAccountType",
|
|
19
|
+
"OkxExchangeManager",
|
|
20
|
+
"OkxPublicConnector",
|
|
21
|
+
"OkxPrivateConnector",
|
|
22
|
+
"OkxExecutionManagementSystem",
|
|
23
|
+
"OkxOrderManagementSystem",
|
|
24
|
+
"OkxFactory",
|
|
25
|
+
]
|