walrasquant-lib 0.4.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. walrasquant/__init__.py +7 -0
  2. walrasquant/aggregation.py +449 -0
  3. walrasquant/backends/__init__.py +5 -0
  4. walrasquant/backends/db.py +109 -0
  5. walrasquant/backends/db_memory.py +61 -0
  6. walrasquant/backends/db_postgresql.py +321 -0
  7. walrasquant/backends/db_sqlite.py +310 -0
  8. walrasquant/base/__init__.py +24 -0
  9. walrasquant/base/api_client.py +46 -0
  10. walrasquant/base/connector.py +863 -0
  11. walrasquant/base/ems.py +794 -0
  12. walrasquant/base/exchange.py +213 -0
  13. walrasquant/base/oms.py +428 -0
  14. walrasquant/base/retry.py +220 -0
  15. walrasquant/base/sms.py +545 -0
  16. walrasquant/base/ws_client.py +408 -0
  17. walrasquant/config.py +284 -0
  18. walrasquant/constants.py +413 -0
  19. walrasquant/core/__init__.py +0 -0
  20. walrasquant/core/cache.py +688 -0
  21. walrasquant/core/clock.py +59 -0
  22. walrasquant/core/connection.py +41 -0
  23. walrasquant/core/entity.py +504 -0
  24. walrasquant/core/nautilius_core.py +103 -0
  25. walrasquant/core/registry.py +41 -0
  26. walrasquant/engine.py +745 -0
  27. walrasquant/error.py +34 -0
  28. walrasquant/exchange/__init__.py +13 -0
  29. walrasquant/exchange/base_factory.py +172 -0
  30. walrasquant/exchange/binance/__init__.py +30 -0
  31. walrasquant/exchange/binance/connector.py +1093 -0
  32. walrasquant/exchange/binance/constants.py +934 -0
  33. walrasquant/exchange/binance/ems.py +140 -0
  34. walrasquant/exchange/binance/error.py +48 -0
  35. walrasquant/exchange/binance/exchange.py +144 -0
  36. walrasquant/exchange/binance/factory.py +115 -0
  37. walrasquant/exchange/binance/oms.py +1807 -0
  38. walrasquant/exchange/binance/rest_api.py +1653 -0
  39. walrasquant/exchange/binance/schema.py +1063 -0
  40. walrasquant/exchange/binance/websockets.py +389 -0
  41. walrasquant/exchange/bitget/__init__.py +28 -0
  42. walrasquant/exchange/bitget/connector.py +578 -0
  43. walrasquant/exchange/bitget/constants.py +392 -0
  44. walrasquant/exchange/bitget/ems.py +202 -0
  45. walrasquant/exchange/bitget/error.py +36 -0
  46. walrasquant/exchange/bitget/exchange.py +128 -0
  47. walrasquant/exchange/bitget/factory.py +135 -0
  48. walrasquant/exchange/bitget/oms.py +1619 -0
  49. walrasquant/exchange/bitget/rest_api.py +610 -0
  50. walrasquant/exchange/bitget/schema.py +885 -0
  51. walrasquant/exchange/bitget/websockets.py +753 -0
  52. walrasquant/exchange/bybit/__init__.py +32 -0
  53. walrasquant/exchange/bybit/connector.py +819 -0
  54. walrasquant/exchange/bybit/constants.py +479 -0
  55. walrasquant/exchange/bybit/ems.py +93 -0
  56. walrasquant/exchange/bybit/error.py +36 -0
  57. walrasquant/exchange/bybit/exchange.py +108 -0
  58. walrasquant/exchange/bybit/factory.py +128 -0
  59. walrasquant/exchange/bybit/oms.py +1195 -0
  60. walrasquant/exchange/bybit/rest_api.py +570 -0
  61. walrasquant/exchange/bybit/schema.py +867 -0
  62. walrasquant/exchange/bybit/websockets.py +307 -0
  63. walrasquant/exchange/hyperliquid/__init__.py +28 -0
  64. walrasquant/exchange/hyperliquid/connector.py +370 -0
  65. walrasquant/exchange/hyperliquid/constants.py +371 -0
  66. walrasquant/exchange/hyperliquid/ems.py +156 -0
  67. walrasquant/exchange/hyperliquid/error.py +48 -0
  68. walrasquant/exchange/hyperliquid/exchange.py +120 -0
  69. walrasquant/exchange/hyperliquid/factory.py +135 -0
  70. walrasquant/exchange/hyperliquid/oms.py +1081 -0
  71. walrasquant/exchange/hyperliquid/rest_api.py +348 -0
  72. walrasquant/exchange/hyperliquid/schema.py +583 -0
  73. walrasquant/exchange/hyperliquid/websockets.py +592 -0
  74. walrasquant/exchange/okx/__init__.py +25 -0
  75. walrasquant/exchange/okx/connector.py +931 -0
  76. walrasquant/exchange/okx/constants.py +518 -0
  77. walrasquant/exchange/okx/ems.py +144 -0
  78. walrasquant/exchange/okx/error.py +66 -0
  79. walrasquant/exchange/okx/exchange.py +102 -0
  80. walrasquant/exchange/okx/factory.py +138 -0
  81. walrasquant/exchange/okx/oms.py +1199 -0
  82. walrasquant/exchange/okx/rest_api.py +799 -0
  83. walrasquant/exchange/okx/schema.py +1449 -0
  84. walrasquant/exchange/okx/websockets.py +420 -0
  85. walrasquant/exchange/registry.py +201 -0
  86. walrasquant/execution/__init__.py +24 -0
  87. walrasquant/execution/algorithm.py +968 -0
  88. walrasquant/execution/algorithms/__init__.py +3 -0
  89. walrasquant/execution/algorithms/twap.py +392 -0
  90. walrasquant/execution/config.py +34 -0
  91. walrasquant/execution/constants.py +27 -0
  92. walrasquant/execution/schema.py +62 -0
  93. walrasquant/indicator.py +382 -0
  94. walrasquant/push.py +77 -0
  95. walrasquant/schema.py +755 -0
  96. walrasquant/strategy.py +1805 -0
  97. walrasquant/tools/__init__.py +0 -0
  98. walrasquant/tools/pm2_wrapper.py +1016 -0
  99. walrasquant/web/__init__.py +26 -0
  100. walrasquant/web/app.py +157 -0
  101. walrasquant/web/server.py +92 -0
  102. walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
  103. walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
  104. walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
  105. walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,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()