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,307 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
import asyncio
|
|
3
|
+
import picows
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, List
|
|
6
|
+
|
|
7
|
+
from walrasquant.base import WSClient
|
|
8
|
+
from walrasquant.core.entity import TaskManager
|
|
9
|
+
from walrasquant.core.nautilius_core import LiveClock, hmac_signature
|
|
10
|
+
from walrasquant.exchange.bybit.schema import (
|
|
11
|
+
BybitWsMessageGeneral,
|
|
12
|
+
BybitWsApiGeneralMsg,
|
|
13
|
+
)
|
|
14
|
+
from walrasquant.exchange.bybit.constants import (
|
|
15
|
+
BybitAccountType,
|
|
16
|
+
BybitKlineInterval,
|
|
17
|
+
BybitRateLimiter,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_REQ_ID_MASK = (1 << 48) - 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def user_pong_callback(self, frame: picows.WSFrame) -> bool:
|
|
24
|
+
if frame.msg_type != picows.WSMsgType.TEXT:
|
|
25
|
+
self._log.debug(
|
|
26
|
+
f"Received non-text frame for pong callback. ws_frame: {self._decode_frame(frame)}"
|
|
27
|
+
)
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
raw = frame.get_payload_as_bytes()
|
|
31
|
+
try:
|
|
32
|
+
message = msgspec.json.decode(raw, type=BybitWsMessageGeneral)
|
|
33
|
+
self._log.debug(f"Received pong message: {message}")
|
|
34
|
+
return message.is_pong
|
|
35
|
+
except msgspec.DecodeError:
|
|
36
|
+
self._log.error(
|
|
37
|
+
f"Failed to decode pong message. ws_frame: {self._decode_frame(frame)}"
|
|
38
|
+
)
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def user_api_pong_callback(self, frame: picows.WSFrame) -> bool:
|
|
43
|
+
if frame.msg_type != picows.WSMsgType.TEXT:
|
|
44
|
+
self._log.debug(
|
|
45
|
+
f"Received non-text frame for pong callback. ws_frame: {self._decode_frame(frame)}"
|
|
46
|
+
)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
raw = frame.get_payload_as_bytes()
|
|
50
|
+
try:
|
|
51
|
+
message = msgspec.json.decode(raw, type=BybitWsApiGeneralMsg)
|
|
52
|
+
self._log.debug(f"Received pong message: {message}")
|
|
53
|
+
return message.is_pong
|
|
54
|
+
except msgspec.DecodeError:
|
|
55
|
+
self._log.error(
|
|
56
|
+
f"Failed to decode pong message. ws_frame: {self._decode_frame(frame)}"
|
|
57
|
+
)
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BybitWSClient(WSClient):
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
account_type: BybitAccountType,
|
|
65
|
+
handler: Callable[..., Any],
|
|
66
|
+
task_manager: TaskManager,
|
|
67
|
+
clock: LiveClock,
|
|
68
|
+
api_key: str | None = None,
|
|
69
|
+
secret: str | None = None,
|
|
70
|
+
custom_url: str | None = None,
|
|
71
|
+
max_subscriptions_per_client: int | None = None,
|
|
72
|
+
max_clients: int | None = None,
|
|
73
|
+
):
|
|
74
|
+
self._account_type = account_type
|
|
75
|
+
self._api_key = api_key
|
|
76
|
+
self._secret = secret
|
|
77
|
+
|
|
78
|
+
if self.is_private:
|
|
79
|
+
url = account_type.ws_private_url
|
|
80
|
+
else:
|
|
81
|
+
url = account_type.ws_public_url
|
|
82
|
+
if custom_url:
|
|
83
|
+
url = custom_url
|
|
84
|
+
# Bybit: do not exceed 500 requests per 5 minutes
|
|
85
|
+
super().__init__(
|
|
86
|
+
url,
|
|
87
|
+
handler=handler,
|
|
88
|
+
task_manager=task_manager,
|
|
89
|
+
clock=clock,
|
|
90
|
+
ping_idle_timeout=10,
|
|
91
|
+
ping_reply_timeout=2,
|
|
92
|
+
specific_ping_msg=msgspec.json.encode({"op": "ping"}),
|
|
93
|
+
auto_ping_strategy="ping_periodically",
|
|
94
|
+
user_pong_callback=user_pong_callback,
|
|
95
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
96
|
+
max_clients=max_clients,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def is_private(self):
|
|
101
|
+
return self._api_key is not None or self._secret is not None
|
|
102
|
+
|
|
103
|
+
def _generate_signature(self):
|
|
104
|
+
expires = self._clock.timestamp_ms() + 1_000
|
|
105
|
+
signature = hmac_signature(self._secret, f"GET/realtime{expires}") # type: ignore
|
|
106
|
+
return signature, expires
|
|
107
|
+
|
|
108
|
+
def _get_auth_payload(self):
|
|
109
|
+
signature, expires = self._generate_signature()
|
|
110
|
+
return {"op": "auth", "args": [self._api_key, expires, signature]}
|
|
111
|
+
|
|
112
|
+
async def _auth(self, client_id: int | None = None):
|
|
113
|
+
self.send(self._get_auth_payload(), client_id=client_id)
|
|
114
|
+
await asyncio.sleep(5)
|
|
115
|
+
|
|
116
|
+
def _send_payload(
|
|
117
|
+
self,
|
|
118
|
+
params: List[str],
|
|
119
|
+
chunk_size: int = 100,
|
|
120
|
+
op: str = "subscribe",
|
|
121
|
+
client_id: int | None = None,
|
|
122
|
+
):
|
|
123
|
+
# Split params into chunks of 100 if length exceeds 100
|
|
124
|
+
params_chunks = [
|
|
125
|
+
params[i : i + chunk_size] for i in range(0, len(params), chunk_size)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
for chunk in params_chunks:
|
|
129
|
+
payload = {"op": op, "args": chunk}
|
|
130
|
+
self.send(payload, client_id=client_id)
|
|
131
|
+
|
|
132
|
+
def _subscribe(self, topics: List[str]):
|
|
133
|
+
assigned = self._register_subscriptions(topics)
|
|
134
|
+
if not assigned:
|
|
135
|
+
return
|
|
136
|
+
for client_id, client_topics in assigned.items():
|
|
137
|
+
for topic in client_topics:
|
|
138
|
+
self._log.debug(f"Subscribing to {topic}...")
|
|
139
|
+
if self._is_client_connected(client_id):
|
|
140
|
+
self._send_payload(client_topics, op="subscribe", client_id=client_id)
|
|
141
|
+
|
|
142
|
+
def _unsubscribe(self, topics: List[str]):
|
|
143
|
+
removed = self._unregister_subscriptions(topics)
|
|
144
|
+
if not removed:
|
|
145
|
+
return
|
|
146
|
+
for client_id, client_topics in removed.items():
|
|
147
|
+
for topic in client_topics:
|
|
148
|
+
self._log.debug(f"Unsubscribing from {topic}...")
|
|
149
|
+
self._send_payload(client_topics, op="unsubscribe", client_id=client_id)
|
|
150
|
+
|
|
151
|
+
def subscribe_order_book(self, symbols: List[str], depth: int):
|
|
152
|
+
"""subscribe to orderbook"""
|
|
153
|
+
topics = [f"orderbook.{depth}.{symbol}" for symbol in symbols]
|
|
154
|
+
self._subscribe(topics)
|
|
155
|
+
|
|
156
|
+
def subscribe_trade(self, symbols: List[str]):
|
|
157
|
+
"""subscribe to trade"""
|
|
158
|
+
topics = [f"publicTrade.{symbol}" for symbol in symbols]
|
|
159
|
+
self._subscribe(topics)
|
|
160
|
+
|
|
161
|
+
def subscribe_ticker(self, symbols: List[str]):
|
|
162
|
+
"""subscribe to ticker"""
|
|
163
|
+
topics = [f"tickers.{symbol}" for symbol in symbols]
|
|
164
|
+
self._subscribe(topics)
|
|
165
|
+
|
|
166
|
+
def subscribe_kline(self, symbols: List[str], interval: BybitKlineInterval):
|
|
167
|
+
"""subscribe to kline"""
|
|
168
|
+
topics = [f"kline.{interval.value}.{symbol}" for symbol in symbols]
|
|
169
|
+
self._subscribe(topics)
|
|
170
|
+
|
|
171
|
+
def unsubscribe_order_book(self, symbols: List[str], depth: int):
|
|
172
|
+
"""unsubscribe from orderbook"""
|
|
173
|
+
topics = [f"orderbook.{depth}.{symbol}" for symbol in symbols]
|
|
174
|
+
self._unsubscribe(topics)
|
|
175
|
+
|
|
176
|
+
def unsubscribe_trade(self, symbols: List[str]):
|
|
177
|
+
"""unsubscribe from trade"""
|
|
178
|
+
topics = [f"publicTrade.{symbol}" for symbol in symbols]
|
|
179
|
+
self._unsubscribe(topics)
|
|
180
|
+
|
|
181
|
+
def unsubscribe_ticker(self, symbols: List[str]):
|
|
182
|
+
"""unsubscribe from ticker"""
|
|
183
|
+
topics = [f"tickers.{symbol}" for symbol in symbols]
|
|
184
|
+
self._unsubscribe(topics)
|
|
185
|
+
|
|
186
|
+
def unsubscribe_kline(self, symbols: List[str], interval: BybitKlineInterval):
|
|
187
|
+
"""unsubscribe from kline"""
|
|
188
|
+
topics = [f"kline.{interval.value}.{symbol}" for symbol in symbols]
|
|
189
|
+
self._unsubscribe(topics)
|
|
190
|
+
|
|
191
|
+
async def _resubscribe_for_client(self, client_id: int, subscriptions: List[str]):
|
|
192
|
+
if not subscriptions:
|
|
193
|
+
return
|
|
194
|
+
if self.is_private:
|
|
195
|
+
await self._auth(client_id=client_id)
|
|
196
|
+
self._send_payload(subscriptions, client_id=client_id)
|
|
197
|
+
|
|
198
|
+
def subscribe_order(self, topic: str = "order"):
|
|
199
|
+
"""subscribe to order"""
|
|
200
|
+
self._subscribe([topic])
|
|
201
|
+
|
|
202
|
+
def subscribe_position(self, topic: str = "position"):
|
|
203
|
+
"""subscribe to position"""
|
|
204
|
+
self._subscribe([topic])
|
|
205
|
+
|
|
206
|
+
def subscribe_wallet(self, topic: str = "wallet"):
|
|
207
|
+
"""subscribe to wallet"""
|
|
208
|
+
self._subscribe([topic])
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class BybitWSApiClient(WSClient):
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
account_type: BybitAccountType,
|
|
215
|
+
api_key: str,
|
|
216
|
+
secret: str,
|
|
217
|
+
handler: Callable[..., Any],
|
|
218
|
+
task_manager: TaskManager,
|
|
219
|
+
clock: LiveClock,
|
|
220
|
+
enable_rate_limit: bool,
|
|
221
|
+
):
|
|
222
|
+
self._api_key = api_key
|
|
223
|
+
self._secret = secret
|
|
224
|
+
self._account_type = account_type
|
|
225
|
+
|
|
226
|
+
url = account_type.ws_api_url
|
|
227
|
+
self._limiter = BybitRateLimiter(
|
|
228
|
+
enable_rate_limit=enable_rate_limit,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
super().__init__(
|
|
232
|
+
url,
|
|
233
|
+
handler=handler,
|
|
234
|
+
task_manager=task_manager,
|
|
235
|
+
clock=clock,
|
|
236
|
+
ping_idle_timeout=5,
|
|
237
|
+
ping_reply_timeout=2,
|
|
238
|
+
specific_ping_msg=msgspec.json.encode({"op": "ping"}),
|
|
239
|
+
user_pong_callback=user_api_pong_callback,
|
|
240
|
+
)
|
|
241
|
+
self._req_id_seq = clock.timestamp_ms() & _REQ_ID_MASK
|
|
242
|
+
|
|
243
|
+
def _generate_signature(self):
|
|
244
|
+
expires = self._clock.timestamp_ms() + 1_000
|
|
245
|
+
signature = hmac_signature(self._secret, f"GET/realtime{expires}")
|
|
246
|
+
return signature, expires
|
|
247
|
+
|
|
248
|
+
def _get_auth_payload(self):
|
|
249
|
+
signature, expires = self._generate_signature()
|
|
250
|
+
return {"op": "auth", "args": [self._api_key, expires, signature]}
|
|
251
|
+
|
|
252
|
+
async def _auth(self, client_id: int | None = None):
|
|
253
|
+
self.send(self._get_auth_payload(), client_id=client_id)
|
|
254
|
+
await asyncio.sleep(5)
|
|
255
|
+
|
|
256
|
+
def _next_req_id(self, prefix: str, oid: str) -> str:
|
|
257
|
+
self._req_id_seq = (self._req_id_seq + 1) & _REQ_ID_MASK
|
|
258
|
+
return f"{prefix}{oid}.{self._req_id_seq:012x}"
|
|
259
|
+
|
|
260
|
+
def _submit(self, reqId: str, op: str, args: list[dict]):
|
|
261
|
+
payload = {
|
|
262
|
+
"reqId": reqId,
|
|
263
|
+
"header": {
|
|
264
|
+
"X-BAPI-TIMESTAMP": self._clock.timestamp_ms(),
|
|
265
|
+
},
|
|
266
|
+
"op": op,
|
|
267
|
+
"args": args,
|
|
268
|
+
}
|
|
269
|
+
self.send(payload)
|
|
270
|
+
|
|
271
|
+
async def create_order(
|
|
272
|
+
self,
|
|
273
|
+
id: str,
|
|
274
|
+
symbol: str,
|
|
275
|
+
side: str,
|
|
276
|
+
orderType: str,
|
|
277
|
+
qty: str,
|
|
278
|
+
category: str,
|
|
279
|
+
**kwargs,
|
|
280
|
+
):
|
|
281
|
+
arg = {
|
|
282
|
+
"symbol": symbol,
|
|
283
|
+
"side": side,
|
|
284
|
+
"orderType": orderType,
|
|
285
|
+
"qty": qty,
|
|
286
|
+
"category": category,
|
|
287
|
+
**kwargs,
|
|
288
|
+
}
|
|
289
|
+
op = "order.create"
|
|
290
|
+
await self._limiter.order_limit(category=category, endpoint=op)
|
|
291
|
+
self._submit(reqId=self._next_req_id("n", id), op=op, args=[arg])
|
|
292
|
+
|
|
293
|
+
async def cancel_order(
|
|
294
|
+
self, id: str, symbol: str, orderLinkId: str, category: str, **kwargs
|
|
295
|
+
):
|
|
296
|
+
arg = {
|
|
297
|
+
"symbol": symbol,
|
|
298
|
+
"orderLinkId": orderLinkId,
|
|
299
|
+
"category": category,
|
|
300
|
+
**kwargs,
|
|
301
|
+
}
|
|
302
|
+
op = "order.cancel"
|
|
303
|
+
# await self._limiter.order_limit(category=category, endpoint=op)
|
|
304
|
+
self._submit(reqId=self._next_req_id("c", id), op=op, args=[arg])
|
|
305
|
+
|
|
306
|
+
async def _resubscribe_for_client(self, client_id: int, subscriptions: List[str]):
|
|
307
|
+
await self._auth(client_id=client_id)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from walrasquant.exchange.hyperliquid.exchange import HyperLiquidExchangeManager
|
|
2
|
+
from walrasquant.exchange.hyperliquid.constants import HyperLiquidAccountType
|
|
3
|
+
from walrasquant.exchange.hyperliquid.connector import (
|
|
4
|
+
HyperLiquidPublicConnector,
|
|
5
|
+
HyperLiquidPrivateConnector,
|
|
6
|
+
)
|
|
7
|
+
from walrasquant.exchange.hyperliquid.oms import HyperLiquidOrderManagementSystem
|
|
8
|
+
from walrasquant.exchange.hyperliquid.ems import HyperLiquidExecutionManagementSystem
|
|
9
|
+
from walrasquant.exchange.hyperliquid.factory import HyperLiquidFactory
|
|
10
|
+
|
|
11
|
+
# Auto-register factory on import
|
|
12
|
+
try:
|
|
13
|
+
from walrasquant.exchange.registry import register_factory
|
|
14
|
+
|
|
15
|
+
register_factory(HyperLiquidFactory())
|
|
16
|
+
except ImportError:
|
|
17
|
+
# Registry not available yet during bootstrap
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"HyperLiquidExchangeManager",
|
|
22
|
+
"HyperLiquidAccountType",
|
|
23
|
+
"HyperLiquidPublicConnector",
|
|
24
|
+
"HyperLiquidPrivateConnector",
|
|
25
|
+
"HyperLiquidOrderManagementSystem",
|
|
26
|
+
"HyperLiquidExecutionManagementSystem",
|
|
27
|
+
"HyperLiquidFactory",
|
|
28
|
+
]
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
from walrasquant.constants import (
|
|
4
|
+
KlineInterval,
|
|
5
|
+
BookLevel,
|
|
6
|
+
OrderSide,
|
|
7
|
+
)
|
|
8
|
+
from walrasquant.config import OrderQueryConfig
|
|
9
|
+
from walrasquant.core.nautilius_core import (
|
|
10
|
+
MessageBus,
|
|
11
|
+
LiveClock,
|
|
12
|
+
)
|
|
13
|
+
from walrasquant.base import PublicConnector, PrivateConnector
|
|
14
|
+
from walrasquant.core.registry import OrderRegistry
|
|
15
|
+
from walrasquant.core.entity import TaskManager
|
|
16
|
+
from walrasquant.core.cache import AsyncCache
|
|
17
|
+
from walrasquant.schema import (
|
|
18
|
+
KlineList,
|
|
19
|
+
Ticker,
|
|
20
|
+
BookL1,
|
|
21
|
+
Trade,
|
|
22
|
+
Kline,
|
|
23
|
+
)
|
|
24
|
+
from walrasquant.exchange.hyperliquid.schema import (
|
|
25
|
+
HyperLiquidMarket,
|
|
26
|
+
HyperLiquidWsMessageGeneral,
|
|
27
|
+
HyperLiquidWsBboMsg,
|
|
28
|
+
HyperLiquidWsTradeMsg,
|
|
29
|
+
HyperLiquidWsCandleMsg,
|
|
30
|
+
)
|
|
31
|
+
from walrasquant.exchange.hyperliquid.oms import HyperLiquidOrderManagementSystem
|
|
32
|
+
from walrasquant.exchange.hyperliquid.exchange import HyperLiquidExchangeManager
|
|
33
|
+
from walrasquant.exchange.hyperliquid.websockets import HyperLiquidWSClient
|
|
34
|
+
from walrasquant.exchange.hyperliquid.constants import (
|
|
35
|
+
HyperLiquidAccountType,
|
|
36
|
+
HyperLiquidEnumParser,
|
|
37
|
+
)
|
|
38
|
+
from walrasquant.exchange.hyperliquid.rest_api import HyperLiquidApiClient
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HyperLiquidPublicConnector(PublicConnector):
|
|
42
|
+
_ws_client: HyperLiquidWSClient
|
|
43
|
+
_account_type: HyperLiquidAccountType
|
|
44
|
+
_market: Dict[str, HyperLiquidMarket]
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
account_type: HyperLiquidAccountType,
|
|
49
|
+
exchange: HyperLiquidExchangeManager,
|
|
50
|
+
msgbus: MessageBus,
|
|
51
|
+
clock: LiveClock,
|
|
52
|
+
task_manager: TaskManager,
|
|
53
|
+
custom_url: str | None = None,
|
|
54
|
+
enable_rate_limit: bool = True,
|
|
55
|
+
max_subscriptions_per_client: int | None = None,
|
|
56
|
+
max_clients: int | None = None,
|
|
57
|
+
):
|
|
58
|
+
super().__init__(
|
|
59
|
+
account_type=account_type,
|
|
60
|
+
market=exchange.market,
|
|
61
|
+
market_id=exchange.market_id,
|
|
62
|
+
exchange_id=exchange.exchange_id,
|
|
63
|
+
ws_client=HyperLiquidWSClient(
|
|
64
|
+
account_type=account_type,
|
|
65
|
+
handler=self._ws_msg_handler,
|
|
66
|
+
task_manager=task_manager,
|
|
67
|
+
clock=clock,
|
|
68
|
+
custom_url=custom_url,
|
|
69
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
70
|
+
max_clients=max_clients,
|
|
71
|
+
),
|
|
72
|
+
api_client=HyperLiquidApiClient(
|
|
73
|
+
clock=clock,
|
|
74
|
+
testnet=account_type.is_testnet,
|
|
75
|
+
enable_rate_limit=enable_rate_limit,
|
|
76
|
+
),
|
|
77
|
+
msgbus=msgbus,
|
|
78
|
+
clock=clock,
|
|
79
|
+
task_manager=task_manager,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self._ws_msg_general_decoder = msgspec.json.Decoder(HyperLiquidWsMessageGeneral)
|
|
83
|
+
self._ws_msg_bbo_decoder = msgspec.json.Decoder(HyperLiquidWsBboMsg)
|
|
84
|
+
self._ws_msg_trade_decoder = msgspec.json.Decoder(HyperLiquidWsTradeMsg)
|
|
85
|
+
self._ws_msg_candle_decoder = msgspec.json.Decoder(HyperLiquidWsCandleMsg)
|
|
86
|
+
|
|
87
|
+
async def request_klines(
|
|
88
|
+
self,
|
|
89
|
+
symbol: str,
|
|
90
|
+
interval: KlineInterval,
|
|
91
|
+
limit: int | None = None,
|
|
92
|
+
start_time: int | None = None,
|
|
93
|
+
end_time: int | None = None,
|
|
94
|
+
) -> KlineList:
|
|
95
|
+
"""Request klines"""
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
async def request_ticker(
|
|
99
|
+
self,
|
|
100
|
+
symbol: str,
|
|
101
|
+
) -> Ticker:
|
|
102
|
+
"""Request 24hr ticker data"""
|
|
103
|
+
raise NotImplementedError
|
|
104
|
+
|
|
105
|
+
async def request_all_tickers(
|
|
106
|
+
self,
|
|
107
|
+
) -> Dict[str, Ticker]:
|
|
108
|
+
"""Request 24hr ticker data for multiple symbols"""
|
|
109
|
+
raise NotImplementedError
|
|
110
|
+
|
|
111
|
+
async def request_index_klines(
|
|
112
|
+
self,
|
|
113
|
+
symbol: str,
|
|
114
|
+
interval: KlineInterval,
|
|
115
|
+
limit: int | None = None,
|
|
116
|
+
start_time: int | None = None,
|
|
117
|
+
end_time: int | None = None,
|
|
118
|
+
) -> KlineList:
|
|
119
|
+
"""Request index klines"""
|
|
120
|
+
raise NotImplementedError
|
|
121
|
+
|
|
122
|
+
def _ws_msg_handler(self, raw: bytes):
|
|
123
|
+
try:
|
|
124
|
+
ws_msg: HyperLiquidWsMessageGeneral = self._ws_msg_general_decoder.decode(
|
|
125
|
+
raw
|
|
126
|
+
)
|
|
127
|
+
# if ws_msg.channel == "pong":
|
|
128
|
+
# self._ws_client._transport.notify_user_specific_pong_received()
|
|
129
|
+
# self._log.debug("Pong received")
|
|
130
|
+
# return
|
|
131
|
+
|
|
132
|
+
if ws_msg.channel == "bbo":
|
|
133
|
+
self._parse_bbo_update(raw)
|
|
134
|
+
elif ws_msg.channel == "trades":
|
|
135
|
+
self._parse_trades_update(raw)
|
|
136
|
+
elif ws_msg.channel == "candle":
|
|
137
|
+
self._parse_candle_update(raw)
|
|
138
|
+
except msgspec.DecodeError as e:
|
|
139
|
+
self._log.error(f"Failed to decode WebSocket message: {e}")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
def _parse_candle_update(self, raw: bytes):
|
|
143
|
+
candle_msg: HyperLiquidWsCandleMsg = self._ws_msg_candle_decoder.decode(raw)
|
|
144
|
+
candle_data = candle_msg.data
|
|
145
|
+
id = candle_data.s
|
|
146
|
+
symbol = self._market_id[id]
|
|
147
|
+
interval = HyperLiquidEnumParser.parse_kline_interval(candle_data.i)
|
|
148
|
+
ts = self._clock.timestamp_ms()
|
|
149
|
+
kline = Kline(
|
|
150
|
+
exchange=self._exchange_id,
|
|
151
|
+
symbol=symbol,
|
|
152
|
+
start=candle_data.t,
|
|
153
|
+
timestamp=ts,
|
|
154
|
+
open=float(candle_data.o),
|
|
155
|
+
high=float(candle_data.h),
|
|
156
|
+
low=float(candle_data.l),
|
|
157
|
+
close=float(candle_data.c),
|
|
158
|
+
volume=float(candle_data.v),
|
|
159
|
+
interval=interval,
|
|
160
|
+
confirm=False, # NOTE: you should define how to handle confirmation in HyperLiquid
|
|
161
|
+
)
|
|
162
|
+
self._msgbus.publish(topic="kline", msg=kline)
|
|
163
|
+
|
|
164
|
+
def _parse_trades_update(self, raw: bytes):
|
|
165
|
+
trade_msg: HyperLiquidWsTradeMsg = self._ws_msg_trade_decoder.decode(raw)
|
|
166
|
+
for data in trade_msg.data:
|
|
167
|
+
id = data.coin
|
|
168
|
+
symbol = self._market_id[id]
|
|
169
|
+
trade = Trade(
|
|
170
|
+
exchange=self._exchange_id,
|
|
171
|
+
symbol=symbol,
|
|
172
|
+
timestamp=data.time,
|
|
173
|
+
price=float(data.px),
|
|
174
|
+
size=float(data.sz),
|
|
175
|
+
side=OrderSide.BUY if data.side.is_buy else OrderSide.SELL,
|
|
176
|
+
)
|
|
177
|
+
self._msgbus.publish(topic="trade", msg=trade)
|
|
178
|
+
|
|
179
|
+
def _parse_bbo_update(self, raw: bytes):
|
|
180
|
+
bbo_msg: HyperLiquidWsBboMsg = self._ws_msg_bbo_decoder.decode(raw)
|
|
181
|
+
id = bbo_msg.data.coin
|
|
182
|
+
symbol = self._market_id[id]
|
|
183
|
+
bid_data = bbo_msg.data.bbo[0]
|
|
184
|
+
ask_data = bbo_msg.data.bbo[1]
|
|
185
|
+
bookl1 = BookL1(
|
|
186
|
+
exchange=self._exchange_id,
|
|
187
|
+
symbol=symbol,
|
|
188
|
+
timestamp=bbo_msg.data.time,
|
|
189
|
+
bid=float(bid_data.px),
|
|
190
|
+
bid_size=float(bid_data.sz),
|
|
191
|
+
ask=float(ask_data.px),
|
|
192
|
+
ask_size=float(ask_data.sz),
|
|
193
|
+
)
|
|
194
|
+
self._msgbus.publish(topic="bookl1", msg=bookl1)
|
|
195
|
+
|
|
196
|
+
def subscribe_bookl1(self, symbol: str | List[str]):
|
|
197
|
+
symbol = [symbol] if isinstance(symbol, str) else symbol
|
|
198
|
+
symbols = []
|
|
199
|
+
for sym in symbol:
|
|
200
|
+
market = self._market.get(sym)
|
|
201
|
+
if not market:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"Market {sym} not found in exchange {self._exchange_id}"
|
|
204
|
+
)
|
|
205
|
+
symbols.append(market.baseName if market.swap else market.id)
|
|
206
|
+
self._ws_client.subscribe_bbo(symbols)
|
|
207
|
+
|
|
208
|
+
def unsubscribe_bookl1(self, symbol: str | List[str]):
|
|
209
|
+
symbol = [symbol] if isinstance(symbol, str) else symbol
|
|
210
|
+
symbols = []
|
|
211
|
+
for sym in symbol:
|
|
212
|
+
market = self._market.get(sym)
|
|
213
|
+
if not market:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"Market {sym} not found in exchange {self._exchange_id}"
|
|
216
|
+
)
|
|
217
|
+
symbols.append(market.baseName if market.swap else market.id)
|
|
218
|
+
self._ws_client.unsubscribe_bbo(symbols)
|
|
219
|
+
|
|
220
|
+
def subscribe_trade(self, symbol):
|
|
221
|
+
symbol = [symbol] if isinstance(symbol, str) else symbol
|
|
222
|
+
symbols = []
|
|
223
|
+
for sym in symbol:
|
|
224
|
+
market = self._market.get(sym)
|
|
225
|
+
if not market:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"Market {sym} not found in exchange {self._exchange_id}"
|
|
228
|
+
)
|
|
229
|
+
symbols.append(market.baseName if market.swap else market.id)
|
|
230
|
+
self._ws_client.subscribe_trades(symbols)
|
|
231
|
+
|
|
232
|
+
def unsubscribe_trade(self, symbol):
|
|
233
|
+
symbol = [symbol] if isinstance(symbol, str) else symbol
|
|
234
|
+
symbols = []
|
|
235
|
+
for sym in symbol:
|
|
236
|
+
market = self._market.get(sym)
|
|
237
|
+
if not market:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"Market {sym} not found in exchange {self._exchange_id}"
|
|
240
|
+
)
|
|
241
|
+
symbols.append(market.baseName if market.swap else market.id)
|
|
242
|
+
self._ws_client.unsubscribe_trades(symbols)
|
|
243
|
+
|
|
244
|
+
def subscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
|
|
245
|
+
"""Subscribe to the kline data"""
|
|
246
|
+
symbol = [symbol] if isinstance(symbol, str) else symbol
|
|
247
|
+
symbols = []
|
|
248
|
+
for sym in symbol:
|
|
249
|
+
market = self._market.get(sym)
|
|
250
|
+
if not market:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
f"Market {sym} not found in exchange {self._exchange_id}"
|
|
253
|
+
)
|
|
254
|
+
symbols.append(market.baseName if market.swap else market.id)
|
|
255
|
+
hyper_interval = HyperLiquidEnumParser.to_hyperliquid_kline_interval(interval)
|
|
256
|
+
self._ws_client.subscribe_candle(symbols, hyper_interval)
|
|
257
|
+
|
|
258
|
+
def unsubscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
|
|
259
|
+
"""Unsubscribe from the kline data"""
|
|
260
|
+
symbol = [symbol] if isinstance(symbol, str) else symbol
|
|
261
|
+
symbols = []
|
|
262
|
+
for sym in symbol:
|
|
263
|
+
market = self._market.get(sym)
|
|
264
|
+
if not market:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"Market {sym} not found in exchange {self._exchange_id}"
|
|
267
|
+
)
|
|
268
|
+
symbols.append(market.baseName if market.swap else market.id)
|
|
269
|
+
hyper_interval = HyperLiquidEnumParser.to_hyperliquid_kline_interval(interval)
|
|
270
|
+
self._ws_client.unsubscribe_candle(symbols, hyper_interval)
|
|
271
|
+
|
|
272
|
+
def subscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
|
|
273
|
+
"""Subscribe to the bookl2 data"""
|
|
274
|
+
raise NotImplementedError
|
|
275
|
+
|
|
276
|
+
def unsubscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
|
|
277
|
+
"""Unsubscribe from the bookl2 data"""
|
|
278
|
+
raise NotImplementedError
|
|
279
|
+
|
|
280
|
+
def subscribe_funding_rate(self, symbol: str | List[str]):
|
|
281
|
+
"""Subscribe to the funding rate data"""
|
|
282
|
+
raise NotImplementedError
|
|
283
|
+
|
|
284
|
+
def unsubscribe_funding_rate(self, symbol: str | List[str]):
|
|
285
|
+
"""Unsubscribe from the funding rate data"""
|
|
286
|
+
raise NotImplementedError
|
|
287
|
+
|
|
288
|
+
def subscribe_index_price(self, symbol: str | List[str]):
|
|
289
|
+
"""Subscribe to the index price data"""
|
|
290
|
+
raise NotImplementedError
|
|
291
|
+
|
|
292
|
+
def unsubscribe_index_price(self, symbol: str | List[str]):
|
|
293
|
+
"""Unsubscribe from the index price data"""
|
|
294
|
+
raise NotImplementedError
|
|
295
|
+
|
|
296
|
+
def subscribe_mark_price(self, symbol: str | List[str]):
|
|
297
|
+
"""Subscribe to the mark price data"""
|
|
298
|
+
raise NotImplementedError
|
|
299
|
+
|
|
300
|
+
def unsubscribe_mark_price(self, symbol: str | List[str]):
|
|
301
|
+
"""Unsubscribe from the mark price data"""
|
|
302
|
+
raise NotImplementedError
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class HyperLiquidPrivateConnector(PrivateConnector):
|
|
306
|
+
_api_client: HyperLiquidApiClient
|
|
307
|
+
_market: Dict[str, HyperLiquidMarket]
|
|
308
|
+
_oms: HyperLiquidOrderManagementSystem
|
|
309
|
+
|
|
310
|
+
def __init__(
|
|
311
|
+
self,
|
|
312
|
+
account_type: HyperLiquidAccountType,
|
|
313
|
+
exchange: HyperLiquidExchangeManager,
|
|
314
|
+
cache: AsyncCache,
|
|
315
|
+
registry: OrderRegistry,
|
|
316
|
+
clock: LiveClock,
|
|
317
|
+
msgbus: MessageBus,
|
|
318
|
+
task_manager: TaskManager,
|
|
319
|
+
order_query_config: OrderQueryConfig,
|
|
320
|
+
enable_rate_limit: bool = True,
|
|
321
|
+
max_subscriptions_per_client: int | None = None,
|
|
322
|
+
max_clients: int | None = None,
|
|
323
|
+
**kwargs,
|
|
324
|
+
):
|
|
325
|
+
if not exchange.api_key or not exchange.secret:
|
|
326
|
+
raise ValueError(
|
|
327
|
+
"API key and secret must be provided for private connector"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
api_client = HyperLiquidApiClient(
|
|
331
|
+
clock=clock,
|
|
332
|
+
api_key=exchange.api_key,
|
|
333
|
+
secret=exchange.secret,
|
|
334
|
+
testnet=account_type.is_testnet,
|
|
335
|
+
enable_rate_limit=enable_rate_limit,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
oms = HyperLiquidOrderManagementSystem(
|
|
339
|
+
account_type=account_type,
|
|
340
|
+
api_key=exchange.api_key,
|
|
341
|
+
secret=exchange.secret,
|
|
342
|
+
market=exchange.market,
|
|
343
|
+
market_id=exchange.market_id,
|
|
344
|
+
registry=registry,
|
|
345
|
+
cache=cache,
|
|
346
|
+
api_client=api_client,
|
|
347
|
+
exchange_id=exchange.exchange_id,
|
|
348
|
+
clock=clock,
|
|
349
|
+
msgbus=msgbus,
|
|
350
|
+
task_manager=task_manager,
|
|
351
|
+
max_slippage=kwargs.get("max_slippage", 0.02),
|
|
352
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
353
|
+
max_clients=max_clients,
|
|
354
|
+
order_query_config=order_query_config,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
super().__init__(
|
|
358
|
+
account_type=account_type,
|
|
359
|
+
market=exchange.market,
|
|
360
|
+
api_client=api_client,
|
|
361
|
+
task_manager=task_manager,
|
|
362
|
+
oms=oms,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
async def connect(self):
|
|
366
|
+
"""Connect to the exchange"""
|
|
367
|
+
self._oms._ws_client.subscribe_order_updates()
|
|
368
|
+
self._oms._ws_client.subscribe_user_events()
|
|
369
|
+
await self._oms._ws_client.connect()
|
|
370
|
+
await self._oms._ws_api_client.connect()
|