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,348 @@
1
+ import msgspec
2
+ import eth_account
3
+ from eth_account.signers.local import LocalAccount
4
+ from eth_account.messages import encode_typed_data
5
+ from typing import Dict, Any, List, Literal, cast
6
+ from urllib.parse import urljoin
7
+ import asyncio
8
+ import aiohttp
9
+ from Crypto.Hash import keccak
10
+
11
+ from walrasquant.base.api_client import ApiClient
12
+ from walrasquant.exchange.hyperliquid.constants import (
13
+ HyperLiquidAccountType,
14
+ HyperLiquidRateLimiter,
15
+ HyperLiquidOrderRequest,
16
+ HyperLiquidOrderCancelRequest,
17
+ HyperLiquidCloidCancelRequest,
18
+ )
19
+ from walrasquant.exchange.hyperliquid.schema import (
20
+ HyperLiquidOrderResponse,
21
+ HyperLiquidCancelResponse,
22
+ HyperLiquidUserOrder,
23
+ HyperLiquidUserPerpsSummary,
24
+ HyperLiquidKline,
25
+ HyperLiquidTrade,
26
+ HyperLiquidOrderBook,
27
+ HyperLiquidTicker,
28
+ HyperLiquidUserSpotSummary,
29
+ )
30
+ from walrasquant.exchange.hyperliquid.error import HyperLiquidHttpError
31
+ from walrasquant.core.nautilius_core import LiveClock
32
+
33
+
34
+ class HyperLiquidApiClient(ApiClient):
35
+ """REST API client for Hyperliquid exchange"""
36
+
37
+ _limiter: HyperLiquidRateLimiter
38
+
39
+ def __init__(
40
+ self,
41
+ clock: LiveClock,
42
+ api_key: str | None = None,
43
+ secret: str | None = None,
44
+ timeout: int = 10,
45
+ testnet: bool = False,
46
+ enable_rate_limit: bool = True,
47
+ ):
48
+ super().__init__(
49
+ clock=clock,
50
+ api_key=api_key,
51
+ secret=secret,
52
+ timeout=timeout,
53
+ rate_limiter=cast(Any, HyperLiquidRateLimiter(enable_rate_limit)),
54
+ )
55
+
56
+ self._account_type = (
57
+ HyperLiquidAccountType.TESTNET
58
+ if testnet
59
+ else HyperLiquidAccountType.MAINNET
60
+ )
61
+ if secret:
62
+ self._eth_account: LocalAccount = eth_account.Account.from_key(secret)
63
+ self._base_url = self._account_type.rest_url
64
+ self._testnet = testnet
65
+ self._headers = {
66
+ "Content-Type": "application/json",
67
+ # "User-Agent": "NexusTrader/1.0",
68
+ }
69
+
70
+ # Decoders for different response types
71
+ self._msg_decoder = msgspec.json.Decoder()
72
+ self._msg_encoder = msgspec.json.Encoder()
73
+ self._order_response_decoder = msgspec.json.Decoder(HyperLiquidOrderResponse)
74
+ self._cancel_response_decoder = msgspec.json.Decoder(HyperLiquidCancelResponse)
75
+ self._user_perps_summary_decoder = msgspec.json.Decoder(
76
+ HyperLiquidUserPerpsSummary
77
+ )
78
+ self._user_spot_summary_decoder = msgspec.json.Decoder(
79
+ HyperLiquidUserSpotSummary
80
+ )
81
+
82
+ self._kline_decoder = msgspec.json.Decoder(list[HyperLiquidKline])
83
+ self._trade_decoder = msgspec.json.Decoder(HyperLiquidTrade)
84
+ self._orderbook_decoder = msgspec.json.Decoder(HyperLiquidOrderBook)
85
+ self._ticker_decoder = msgspec.json.Decoder(HyperLiquidTicker)
86
+ self._user_order_decoder = msgspec.json.Decoder(list[HyperLiquidUserOrder])
87
+
88
+ def _get_rate_limit_cost(self, cost: int = 1, length: int = 0) -> int:
89
+ """Get rate limit cost for an operation
90
+
91
+ Please refer to https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/rate-limits-and-user-limits
92
+ """
93
+ return cost + length // 40
94
+
95
+ async def _fetch(
96
+ self,
97
+ method: str,
98
+ base_url: str,
99
+ endpoint: str,
100
+ payload: Dict[str, Any],
101
+ ) -> bytes:
102
+ """Asynchronous HTTP request"""
103
+ await self._init_session()
104
+ session = self._session
105
+ if session is None:
106
+ raise ValueError("HTTP session initialization failed")
107
+
108
+ url = urljoin(base_url, endpoint)
109
+ data = msgspec.json.encode(payload)
110
+
111
+ self._log.debug(f"Request: {method} {url}")
112
+
113
+ try:
114
+ async with session.request(
115
+ method=method,
116
+ url=url,
117
+ headers=self._headers,
118
+ data=data,
119
+ ) as response:
120
+ raw = await response.read()
121
+
122
+ if response.status >= 400:
123
+ raise HyperLiquidHttpError(
124
+ status_code=response.status,
125
+ message=raw.decode() if isinstance(raw, bytes) else str(raw),
126
+ headers=dict(response.headers),
127
+ )
128
+
129
+ return raw
130
+ except asyncio.TimeoutError as e:
131
+ self._log.error(f"Timeout {method} {url} {e}")
132
+ raise
133
+ except aiohttp.ClientError as e:
134
+ self._log.error(f"Request Error {method} {url} {e}")
135
+ raise
136
+ except Exception as e:
137
+ self._log.error(f"Error {method} {url} {e}")
138
+ raise
139
+
140
+ # Market Data Endpoints
141
+ async def get_user_perps_summary(self) -> HyperLiquidUserPerpsSummary:
142
+ """Get user perps summary"""
143
+ endpoint = "/info"
144
+ api_key = self._api_key
145
+ base_url = self._base_url
146
+ if api_key is None or base_url is None:
147
+ raise ValueError("api_key and base_url are required")
148
+ payload = {"type": "clearinghouseState", "user": self._api_key, "dex": ""}
149
+ await self._limiter.info_limit(cost=2)
150
+ raw = await self._fetch("POST", base_url, endpoint, payload)
151
+ return self._user_perps_summary_decoder.decode(raw)
152
+
153
+ async def get_user_spot_summary(self) -> HyperLiquidUserSpotSummary:
154
+ """Get user spot summary"""
155
+ endpoint = "/info"
156
+ api_key = self._api_key
157
+ base_url = self._base_url
158
+ if api_key is None or base_url is None:
159
+ raise ValueError("api_key and base_url are required")
160
+ payload = {"type": "spotClearinghouseState", "user": api_key}
161
+ await self._limiter.info_limit(cost=2)
162
+ raw = await self._fetch("POST", base_url, endpoint, payload)
163
+ return self._user_spot_summary_decoder.decode(raw)
164
+
165
+ async def get_order_status(self, oid: str | int) -> dict[str, Any]:
166
+ """
167
+ Query order status by exchange oid or client order id (cloid).
168
+ """
169
+ endpoint = "/info"
170
+ api_key = self._api_key
171
+ base_url = self._base_url
172
+ if api_key is None or base_url is None:
173
+ raise ValueError("api_key and base_url are required")
174
+ payload = {"type": "orderStatus", "user": api_key, "oid": oid}
175
+ await self._limiter.info_limit(cost=2)
176
+ raw = await self._fetch("POST", base_url, endpoint, payload)
177
+ return self._msg_decoder.decode(raw)
178
+
179
+ async def get_klines(
180
+ self,
181
+ coin: str,
182
+ interval: str,
183
+ startTime: int | None = None,
184
+ endTime: int | None = None,
185
+ ) -> List[HyperLiquidKline]:
186
+ """
187
+ Get kline/candlestick data
188
+ https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candle-snapshot
189
+ """
190
+ endpoint = "/info"
191
+ base_url = self._base_url
192
+ if base_url is None:
193
+ raise ValueError("base_url is required")
194
+ req: dict[str, str | int] = {
195
+ "coin": coin,
196
+ "interval": interval,
197
+ }
198
+ if startTime is not None:
199
+ req["startTime"] = startTime
200
+ if endTime is not None:
201
+ req["endTime"] = endTime
202
+ payload = {"type": "candleSnapshot", "req": req}
203
+
204
+ await self._limiter.info_limit(cost=20)
205
+ raw = await self._fetch("POST", base_url, endpoint, payload)
206
+ return self._kline_decoder.decode(raw)
207
+
208
+ def _construct_phantom_agent(
209
+ self, hash: bytes, is_testnet: bool = False
210
+ ) -> Dict[str, Any]:
211
+ return {"source": "b" if is_testnet else "a", "connectionId": hash}
212
+
213
+ def _action_hash(
214
+ self, action: Dict[str, Any], nonce: int, vaultAddress: str | None = None
215
+ ) -> bytes:
216
+ data = msgspec.msgpack.encode(action)
217
+ data += nonce.to_bytes(8, "big")
218
+ if vaultAddress is None:
219
+ data += b"\x00"
220
+ else:
221
+ data += b"\x01"
222
+ data += bytes.fromhex(
223
+ vaultAddress[2:] if vaultAddress.startswith("0x") else vaultAddress
224
+ )
225
+ return keccak.new(digest_bits=256, data=data).digest()
226
+
227
+ def _sign_l1_action(
228
+ self, action: Dict[str, Any], nonce: int, vaultAddress: str | None = None
229
+ ) -> dict[str, str | int]:
230
+ if not hasattr(self, "_eth_account"):
231
+ raise ValueError("secret is required for signed HyperLiquid actions")
232
+ hash = self._action_hash(action, nonce, vaultAddress)
233
+ phantom_agent = self._construct_phantom_agent(hash, is_testnet=self._testnet)
234
+ encoded_data = encode_typed_data(
235
+ full_message={
236
+ "domain": {
237
+ "chainId": 1337,
238
+ "name": "Exchange",
239
+ "verifyingContract": "0x0000000000000000000000000000000000000000",
240
+ "version": "1",
241
+ },
242
+ "types": {
243
+ "Agent": [
244
+ {"name": "source", "type": "string"},
245
+ {"name": "connectionId", "type": "bytes32"},
246
+ ],
247
+ "EIP712Domain": [
248
+ {"name": "name", "type": "string"},
249
+ {"name": "version", "type": "string"},
250
+ {"name": "chainId", "type": "uint256"},
251
+ {"name": "verifyingContract", "type": "address"},
252
+ ],
253
+ },
254
+ "primaryType": "Agent",
255
+ "message": phantom_agent,
256
+ }
257
+ )
258
+ signed = self._eth_account.sign_message(encoded_data)
259
+ return {
260
+ "r": f"{signed.r:#x}",
261
+ "s": f"{signed.s:#x}",
262
+ "v": signed.v,
263
+ }
264
+
265
+ # Trading Endpoints
266
+ async def place_orders(
267
+ self,
268
+ orders: List[HyperLiquidOrderRequest],
269
+ grouping: Literal["na", "normalTpsl", "positionTpsl"] = "na",
270
+ ) -> HyperLiquidOrderResponse:
271
+ nounce = self._clock.timestamp_ms()
272
+ base_url = self._base_url
273
+ if base_url is None:
274
+ raise ValueError("base_url is required")
275
+ orderAction = {
276
+ "type": "order",
277
+ "orders": orders,
278
+ "grouping": grouping,
279
+ }
280
+ signature = self._sign_l1_action(orderAction, nounce)
281
+ cost = self._get_rate_limit_cost(cost=1, length=len(orders))
282
+ await self._limiter.exchange_limit(cost=cost)
283
+ res = await self._fetch(
284
+ "POST",
285
+ base_url,
286
+ "/exchange",
287
+ {
288
+ "action": orderAction,
289
+ "nonce": nounce,
290
+ "signature": signature,
291
+ },
292
+ )
293
+ order = self._order_response_decoder.decode(res)
294
+ return order
295
+
296
+ async def cancel_orders(
297
+ self,
298
+ cancels: List[HyperLiquidOrderCancelRequest],
299
+ ):
300
+ nounce = self._clock.timestamp_ms()
301
+ base_url = self._base_url
302
+ if base_url is None:
303
+ raise ValueError("base_url is required")
304
+ orderAction = {
305
+ "type": "cancel",
306
+ "cancels": cancels,
307
+ }
308
+ signature = self._sign_l1_action(orderAction, nounce)
309
+ # cost = self._get_rate_limit_cost(cost=1, length=len(cancels))
310
+ # await self._limiter.exchange_limit(cost=cost)
311
+ res = await self._fetch(
312
+ "POST",
313
+ base_url,
314
+ "/exchange",
315
+ {
316
+ "action": orderAction,
317
+ "nonce": nounce,
318
+ "signature": signature,
319
+ },
320
+ )
321
+ return self._cancel_response_decoder.decode(res)
322
+
323
+ async def cancel_orders_by_cloid(
324
+ self,
325
+ cancels: List[HyperLiquidCloidCancelRequest],
326
+ ):
327
+ nounce = self._clock.timestamp_ms()
328
+ base_url = self._base_url
329
+ if base_url is None:
330
+ raise ValueError("base_url is required")
331
+ orderAction = {
332
+ "type": "cancelByCloid",
333
+ "cancels": cancels,
334
+ }
335
+ signature = self._sign_l1_action(orderAction, nounce)
336
+ # cost = self._get_rate_limit_cost(cost=1, length=len(cancels))
337
+ # await self._limiter.exchange_limit(cost=cost)
338
+ res = await self._fetch(
339
+ "POST",
340
+ base_url,
341
+ "/exchange",
342
+ {
343
+ "action": orderAction,
344
+ "nonce": nounce,
345
+ "signature": signature,
346
+ },
347
+ )
348
+ return self._cancel_response_decoder.decode(res)