async-hyperliquid 0.4.4__tar.gz → 0.4.6__tar.gz

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 (20) hide show
  1. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/PKG-INFO +1 -1
  2. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/pyproject.toml +1 -1
  3. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/core.py +190 -38
  4. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/info.py +91 -4
  5. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/orders.py +62 -12
  6. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/info.py +2 -2
  7. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/signing.py +8 -5
  8. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/LICENSE +0 -0
  9. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/README.md +0 -0
  10. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/__init__.py +0 -0
  11. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/__init__.py +0 -0
  12. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/actions.py +0 -0
  13. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/async_api.py +0 -0
  14. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/async_hyperliquid.py +0 -0
  15. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/exchange.py +0 -0
  16. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/__init__.py +0 -0
  17. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/constants.py +0 -0
  18. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/decorators.py +0 -0
  19. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/miscs.py +0 -0
  20. {async_hyperliquid-0.4.4 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: async-hyperliquid
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Async Hyperliquid client using aiohttp
5
5
  Keywords: dex,hyperliquid,async,aiohttp,trading,cryptocurrency,defi
6
6
  Author: Yuki
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "async-hyperliquid"
3
- version = "0.4.4"
3
+ version = "0.4.6"
4
4
  description = "Async Hyperliquid client using aiohttp"
5
5
  authors = [{ name = "Yuki", email = "yuqi.lyle@gmail.com" }]
6
6
  readme = "README.md"
@@ -34,6 +34,9 @@ class AsyncHyperliquidCore(AsyncAPI):
34
34
  coin_symbols: dict[str, str]
35
35
  asset_sz_decimals: dict[int, int]
36
36
  spot_tokens: dict[str, SpotTokenMeta]
37
+ _meta_init_lock: asyncio.Lock
38
+ _meta_init_task: asyncio.Task[None] | None
39
+ _metas_initialized: bool
37
40
 
38
41
  perp_dexs: list[str]
39
42
 
@@ -79,6 +82,9 @@ class AsyncHyperliquidCore(AsyncAPI):
79
82
  self.coin_symbols = {}
80
83
  self.asset_sz_decimals = {}
81
84
  self.spot_tokens = {}
85
+ self._meta_init_lock = asyncio.Lock()
86
+ self._meta_init_task = None
87
+ self._metas_initialized = False
82
88
 
83
89
  self.vault = vault
84
90
  self.expires: int | None = None
@@ -124,23 +130,50 @@ class AsyncHyperliquidCore(AsyncAPI):
124
130
 
125
131
  self.evm_exchange = EVMExchange(rpc_url, private_key)
126
132
 
127
- def _init_perp_meta(self, meta: PerpMeta, offset: int) -> None:
133
+ def _init_perp_meta(
134
+ self,
135
+ meta: PerpMeta,
136
+ offset: int,
137
+ *,
138
+ coin_assets: dict[str, int] | None = None,
139
+ coin_names: dict[str, str] | None = None,
140
+ asset_sz_decimals: dict[int, int] | None = None,
141
+ ) -> None:
142
+ coin_assets = self.coin_assets if coin_assets is None else coin_assets
143
+ coin_names = self.coin_names if coin_names is None else coin_names
144
+ asset_sz_decimals = (
145
+ self.asset_sz_decimals if asset_sz_decimals is None else asset_sz_decimals
146
+ )
128
147
  for asset, info in enumerate(meta["universe"]):
129
148
  asset += offset
130
149
  asset_name = info["name"]
131
- self.coin_assets[asset_name] = asset
132
- self.coin_names[asset_name] = asset_name
133
- self.asset_sz_decimals[asset] = info["szDecimals"]
150
+ coin_assets[asset_name] = asset
151
+ coin_names[asset_name] = asset_name
152
+ asset_sz_decimals[asset] = info["szDecimals"]
134
153
 
135
- def _init_spot_meta(self, meta: SpotMeta) -> None:
154
+ def _init_spot_meta(
155
+ self,
156
+ meta: SpotMeta,
157
+ *,
158
+ coin_assets: dict[str, int] | None = None,
159
+ coin_names: dict[str, str] | None = None,
160
+ asset_sz_decimals: dict[int, int] | None = None,
161
+ spot_tokens: dict[str, SpotTokenMeta] | None = None,
162
+ ) -> None:
163
+ coin_assets = self.coin_assets if coin_assets is None else coin_assets
164
+ coin_names = self.coin_names if coin_names is None else coin_names
165
+ asset_sz_decimals = (
166
+ self.asset_sz_decimals if asset_sz_decimals is None else asset_sz_decimals
167
+ )
168
+ spot_tokens = self.spot_tokens if spot_tokens is None else spot_tokens
136
169
  tokens = meta["tokens"]
137
170
  total_tokens = len(tokens)
138
171
  for info in meta["universe"]:
139
172
  asset = info["index"] + SPOT_OFFSET
140
173
  asset_name = info["name"]
141
174
 
142
- self.coin_assets[asset_name] = asset
143
- self.coin_names[asset_name] = asset_name
175
+ coin_assets[asset_name] = asset
176
+ coin_names[asset_name] = asset_name
144
177
 
145
178
  base, quote = info["tokens"]
146
179
  if not 0 <= base < total_tokens or not 0 <= quote < total_tokens:
@@ -151,19 +184,65 @@ class AsyncHyperliquidCore(AsyncAPI):
151
184
  quote_info = tokens[quote]
152
185
  quote_name = quote_info["name"]
153
186
  name = f"{base_name}/{quote_name}"
154
- self.coin_names.setdefault(name, asset_name)
155
- self.coin_names.setdefault(quote_name, quote_name)
156
-
157
- self.asset_sz_decimals[asset] = base_info["szDecimals"]
158
- self.spot_tokens[asset_name] = base_info
159
- self.spot_tokens.setdefault(quote_name, quote_info)
160
-
161
- def _update_coin_symbols(self) -> None:
162
- self.coin_symbols = {
163
- v: k for k, v in self.coin_names.items() if not k.startswith("@")
164
- }
165
-
166
- async def init_metas(self) -> None:
187
+ coin_names.setdefault(name, asset_name)
188
+ coin_names.setdefault(quote_name, quote_name)
189
+
190
+ asset_sz_decimals[asset] = base_info["szDecimals"]
191
+ spot_tokens[asset_name] = base_info
192
+ spot_tokens.setdefault(quote_name, quote_info)
193
+
194
+ def _build_coin_symbols(self, coin_names: dict[str, str]) -> dict[str, str]:
195
+ return {v: k for k, v in coin_names.items() if not k.startswith("@")}
196
+
197
+ def _get_meta_init_lock(self) -> asyncio.Lock:
198
+ lock = getattr(self, "_meta_init_lock", None)
199
+ if lock is None:
200
+ lock = asyncio.Lock()
201
+ self._meta_init_lock = lock
202
+ return lock
203
+
204
+ def _lookup_cached_coin_name(self, coin: str) -> str | None:
205
+ coin_names = getattr(self, "coin_names", {})
206
+ if coin in coin_names:
207
+ return coin_names[coin]
208
+
209
+ coin_assets = getattr(self, "coin_assets", {})
210
+ if coin in coin_assets:
211
+ return coin
212
+
213
+ return None
214
+
215
+ def _lookup_cached_asset_id(self, coin: str) -> int | None:
216
+ coin_assets = getattr(self, "coin_assets", {})
217
+ asset = coin_assets.get(coin)
218
+ if asset is not None:
219
+ return asset
220
+
221
+ coin_name = getattr(self, "coin_names", {}).get(coin)
222
+ if coin_name is None:
223
+ return None
224
+
225
+ return coin_assets.get(coin_name)
226
+
227
+ def _lookup_cached_asset(self, coin: str) -> tuple[str, int] | None:
228
+ asset = self._lookup_cached_asset_id(coin)
229
+ if asset is None:
230
+ return None
231
+
232
+ coin_name = self._lookup_cached_coin_name(coin)
233
+ if coin_name is None:
234
+ return None
235
+
236
+ return coin_name, asset
237
+
238
+ async def _refresh_metas(self) -> None:
239
+ # TODO: Add HIP-4 outcome meta initialization from the spot info
240
+ # `outcomeMeta` endpoint once outcomes move beyond testnet-only rollout.
241
+ # Outcome asset IDs do not follow the current perp/spot offset scheme:
242
+ # `encoding = 10 * outcome + side`, coin names use `#{encoding}`, token
243
+ # names use `+{encoding}`, and `asset_id = 100_000_000 + encoding`, so
244
+ # this path will need dedicated outcome mappings instead of reusing the
245
+ # existing perp/spot logic.
167
246
  meta_task = self.info.get_perp_meta()
168
247
  spot_meta_task = self.info.get_spot_meta()
169
248
  all_dex_names_task = self.get_all_dex_name()
@@ -172,17 +251,36 @@ class AsyncHyperliquidCore(AsyncAPI):
172
251
  meta_task, spot_meta_task, all_dex_names_task
173
252
  )
174
253
 
175
- self._init_perp_meta(meta, 0)
176
- self._init_spot_meta(spot_meta)
254
+ coin_assets: dict[str, int] = {}
255
+ coin_names: dict[str, str] = {}
256
+ asset_sz_decimals: dict[int, int] = {}
257
+ spot_tokens: dict[str, SpotTokenMeta] = {}
258
+
259
+ self._init_perp_meta(
260
+ meta,
261
+ 0,
262
+ coin_assets=coin_assets,
263
+ coin_names=coin_names,
264
+ asset_sz_decimals=asset_sz_decimals,
265
+ )
266
+ self._init_spot_meta(
267
+ spot_meta,
268
+ coin_assets=coin_assets,
269
+ coin_names=coin_names,
270
+ asset_sz_decimals=asset_sz_decimals,
271
+ spot_tokens=spot_tokens,
272
+ )
177
273
 
274
+ dex_index_by_name = {
275
+ name: idx for idx, name in enumerate(all_dex_names) if name is not None
276
+ }
178
277
  dex_meta_tasks = []
179
278
  dex_indices = []
180
279
  for dex in self.perp_dexs:
181
280
  if dex == "":
182
281
  continue
183
- try:
184
- idx = all_dex_names.index(dex)
185
- except ValueError:
282
+ idx = dex_index_by_name.get(dex)
283
+ if idx is None:
186
284
  continue
187
285
 
188
286
  if idx > 0:
@@ -193,9 +291,49 @@ class AsyncHyperliquidCore(AsyncAPI):
193
291
  dex_metas = await asyncio.gather(*dex_meta_tasks)
194
292
  for idx, dex_meta in zip(dex_indices, dex_metas):
195
293
  dex_asset_offset = PERP_DEX_OFFSET + (idx - 1) * 10000
196
- self._init_perp_meta(dex_meta, dex_asset_offset)
294
+ self._init_perp_meta(
295
+ dex_meta,
296
+ dex_asset_offset,
297
+ coin_assets=coin_assets,
298
+ coin_names=coin_names,
299
+ asset_sz_decimals=asset_sz_decimals,
300
+ )
301
+
302
+ self.coin_assets = coin_assets
303
+ self.coin_names = coin_names
304
+ self.asset_sz_decimals = asset_sz_decimals
305
+ self.spot_tokens = spot_tokens
306
+ self.coin_symbols = self._build_coin_symbols(coin_names)
307
+ self._metas_initialized = True
308
+
309
+ async def _run_meta_refresh(self, *, only_if_missing: bool) -> None:
310
+ if only_if_missing and getattr(self, "_metas_initialized", False):
311
+ return
312
+
313
+ task = getattr(self, "_meta_init_task", None)
314
+ if task is not None and not task.done():
315
+ await task
316
+ return
317
+
318
+ async with self._get_meta_init_lock():
319
+ task = getattr(self, "_meta_init_task", None)
320
+ if task is None or task.done():
321
+ if only_if_missing and getattr(self, "_metas_initialized", False):
322
+ return
323
+ task = asyncio.create_task(self._refresh_metas())
324
+ self._meta_init_task = task
325
+
326
+ try:
327
+ await task
328
+ finally:
329
+ if getattr(self, "_meta_init_task", None) is task and task.done():
330
+ self._meta_init_task = None
331
+
332
+ async def _ensure_metas_initialized(self) -> None:
333
+ await self._run_meta_refresh(only_if_missing=True)
197
334
 
198
- self._update_coin_symbols()
335
+ async def init_metas(self) -> None:
336
+ await self._run_meta_refresh(only_if_missing=False)
199
337
 
200
338
  async def get_metas(self, perp_only: bool = False) -> Metas:
201
339
  metas: Metas = {"perp": {}, "spot": [], "dexs": {}} # type: ignore
@@ -237,31 +375,45 @@ class AsyncHyperliquidCore(AsyncAPI):
237
375
  return names
238
376
 
239
377
  async def get_coin_name(self, coin: str) -> str:
240
- if not hasattr(self, "coin_names") or coin not in self.coin_names:
241
- await self.init_metas()
378
+ coin_name = self._lookup_cached_coin_name(coin)
379
+ if coin_name is not None:
380
+ return coin_name
242
381
 
243
- if coin not in self.coin_names:
382
+ if getattr(self, "_metas_initialized", False):
383
+ await self.init_metas()
384
+ else:
385
+ await self._ensure_metas_initialized()
386
+ coin_name = self._lookup_cached_coin_name(coin)
387
+ if coin_name is None:
244
388
  raise ValueError(f"Coin {coin} not found")
245
389
 
246
- return self.coin_names[coin]
390
+ return coin_name
247
391
 
248
392
  async def get_coin_asset(self, coin: str) -> int:
393
+ cached = self._lookup_cached_asset(coin)
394
+ if cached is not None:
395
+ return cached[1]
396
+
249
397
  coin_name = await self.get_coin_name(coin)
250
- if coin_name not in self.coin_assets:
398
+ asset = self.coin_assets.get(coin_name)
399
+ if asset is None:
251
400
  raise ValueError(f"Coin {coin}({coin_name}) not found")
252
401
 
253
- return self.coin_assets[coin_name]
402
+ return asset
254
403
 
255
404
  async def get_coin_symbol(self, coin: str) -> str:
256
405
  coin_name = await self.get_coin_name(coin)
257
406
  return self.coin_symbols[coin_name]
258
407
 
259
408
  async def get_coin_sz_decimals(self, coin: str) -> int:
260
- coin_name = await self.get_coin_name(coin)
261
- if coin_name not in self.coin_assets:
262
- raise ValueError(f"Coin {coin}({coin_name}) not found")
263
-
264
- asset = self.coin_assets[coin_name]
409
+ cached = self._lookup_cached_asset(coin)
410
+ if cached is not None:
411
+ _, asset = cached
412
+ else:
413
+ coin_name = await self.get_coin_name(coin)
414
+ asset = self.coin_assets.get(coin_name)
415
+ if asset is None:
416
+ raise ValueError(f"Coin {coin}({coin_name}) not found")
265
417
  return self.asset_sz_decimals[asset]
266
418
 
267
419
  async def get_token_info(self, coin: str) -> SpotTokenMeta:
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  import warnings
3
- from typing import Literal
3
+ from typing import Literal, cast
4
4
 
5
5
  from async_hyperliquid.utils.constants import ONE_HOUR_MS, PERP_DEX_OFFSET, SPOT_OFFSET
6
6
  from async_hyperliquid.utils.miscs import get_coin_dex, get_timestamp_ms
@@ -9,9 +9,13 @@ from async_hyperliquid.utils.types import (
9
9
  AccountState,
10
10
  ClearinghouseState,
11
11
  OrderWithStatus,
12
+ PerpMeta,
13
+ PerpMetaCtxItem,
12
14
  Portfolio,
13
15
  Position,
16
+ SpotMeta,
14
17
  SpotClearinghouseState,
18
+ SpotMetaCtxItem,
15
19
  UserDeposit,
16
20
  UserNonFundingDelta,
17
21
  UserOpenOrders,
@@ -23,13 +27,96 @@ from .core import AsyncHyperliquidCore
23
27
 
24
28
 
25
29
  class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
30
+ def _get_cached_perp_ctx_index(self, coin_name: str) -> tuple[str, int] | None:
31
+ asset = self._lookup_cached_asset_id(coin_name)
32
+ if asset is None:
33
+ return None
34
+
35
+ if asset < SPOT_OFFSET:
36
+ return get_coin_dex(coin_name), asset
37
+
38
+ if asset >= PERP_DEX_OFFSET:
39
+ return get_coin_dex(coin_name), (asset - PERP_DEX_OFFSET) % 10000
40
+
41
+ return None
42
+
43
+ def _get_cached_spot_ctx_index(self, coin_name: str) -> int | None:
44
+ asset = self._lookup_cached_asset_id(coin_name)
45
+ if asset is None:
46
+ return None
47
+
48
+ if SPOT_OFFSET <= asset < PERP_DEX_OFFSET:
49
+ return asset - SPOT_OFFSET
50
+
51
+ return None
52
+
53
+ async def _get_perp_mark_price(
54
+ self, coin_name: str, dex: str = "", asset_index: int | None = None
55
+ ) -> float:
56
+ meta_ctx = await self.info.get_perp_meta_ctx(dex)
57
+ meta = cast(PerpMeta, meta_ctx[0])
58
+ asset_ctxs = cast(list[PerpMetaCtxItem], meta_ctx[1])
59
+
60
+ if asset_index is not None and 0 <= asset_index < len(asset_ctxs):
61
+ return float(asset_ctxs[asset_index]["markPx"])
62
+
63
+ for idx, info in enumerate(meta["universe"]):
64
+ if info["name"] == coin_name:
65
+ return float(asset_ctxs[idx]["markPx"])
66
+
67
+ raise ValueError(f"Coin {coin_name} not found in perp dex '{dex}'")
68
+
69
+ async def _get_spot_mark_price(
70
+ self, coin_name: str, asset_index: int | None = None
71
+ ) -> float:
72
+ meta_ctx = await self.info.get_spot_meta_ctx()
73
+ meta = cast(SpotMeta, meta_ctx[0])
74
+ asset_ctxs = cast(list[SpotMetaCtxItem], meta_ctx[1])
75
+
76
+ if asset_index is not None and 0 <= asset_index < len(asset_ctxs):
77
+ return float(asset_ctxs[asset_index]["markPx"])
78
+
79
+ for info in meta["universe"]:
80
+ if info["name"] == coin_name:
81
+ asset_index = info["index"]
82
+ return float(asset_ctxs[asset_index]["markPx"])
83
+
84
+ raise ValueError(f"Coin {coin_name} not found in spot metadata")
85
+
86
+ async def get_mark_price(self, coin: str) -> float:
87
+ if ":" in coin:
88
+ cached = self._get_cached_perp_ctx_index(coin)
89
+ if cached is not None:
90
+ dex, asset_index = cached
91
+ return await self._get_perp_mark_price(
92
+ coin, dex, asset_index=asset_index
93
+ )
94
+ return await self._get_perp_mark_price(coin, get_coin_dex(coin))
95
+
96
+ coin_name = await self.get_coin_name(coin)
97
+ asset = self._lookup_cached_asset_id(coin_name)
98
+ if asset is None:
99
+ raise ValueError(f"Coin {coin}({coin_name}) not found")
100
+
101
+ is_spot_asset = SPOT_OFFSET <= asset < PERP_DEX_OFFSET
102
+ if is_spot_asset:
103
+ return await self._get_spot_mark_price(
104
+ coin_name, asset_index=asset - SPOT_OFFSET
105
+ )
106
+
107
+ return await self._get_perp_mark_price(
108
+ coin_name,
109
+ get_coin_dex(coin_name),
110
+ asset_index=(
111
+ asset if asset < SPOT_OFFSET else (asset - PERP_DEX_OFFSET) % 10000
112
+ ),
113
+ )
114
+
26
115
  async def get_market_price(self, coin: str) -> float:
27
116
  warnings.warn(
28
117
  "get_market_price is deprecated and will remove in the future, use get_mid_price instead"
29
118
  )
30
- coin_name = await self.get_coin_name(coin)
31
- market_prices = await self.get_all_market_prices()
32
- return market_prices[coin_name]
119
+ return await self.get_mark_price(coin)
33
120
 
34
121
  async def get_all_market_prices(
35
122
  self, market: Literal["spot", "perp", "all"] = "all"
@@ -20,14 +20,33 @@ from .info import AsyncHyperliquidInfoClient
20
20
 
21
21
 
22
22
  class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
23
+ def _round_sz_px_cached(
24
+ self, coin: str, sz: float, px: float
25
+ ) -> tuple[int, float, float | int] | None:
26
+ asset = self._lookup_cached_asset_id(coin)
27
+ if asset is None:
28
+ return None
29
+
30
+ sz_decimals = self.asset_sz_decimals.get(asset)
31
+ if sz_decimals is None:
32
+ return None
33
+
34
+ is_spot = SPOT_OFFSET <= asset < PERP_DEX_OFFSET
35
+ px_decimals = (6 if not is_spot else 8) - sz_decimals
36
+ return asset, round_float(sz, sz_decimals), round_px(px, px_decimals)
37
+
23
38
  async def _round_sz_px(self, coin: str, sz: float, px: float):
39
+ cached = self._round_sz_px_cached(coin, sz, px)
40
+ if cached is not None:
41
+ return cached
42
+
24
43
  coin_name = await self.get_coin_name(coin)
25
- if coin_name not in self.coin_assets:
44
+ asset = self.coin_assets.get(coin_name)
45
+ if asset is None:
26
46
  raise ValueError(f"Coin {coin}({coin_name}) not found")
27
47
 
28
- asset = self.coin_assets[coin_name]
29
48
  sz_decimals = self.asset_sz_decimals[asset]
30
- is_spot = asset >= SPOT_OFFSET and asset < PERP_DEX_OFFSET
49
+ is_spot = SPOT_OFFSET <= asset < PERP_DEX_OFFSET
31
50
  px_decimals = (6 if not is_spot else 8) - sz_decimals
32
51
  return asset, round_float(sz, sz_decimals), round_px(px, px_decimals)
33
52
 
@@ -155,12 +174,21 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
155
174
  return await self.place_orders(reqs, grouping=grouping, builder=builder)
156
175
 
157
176
  async def _get_batch_limit_orders(self, orders: BatchPlaceOrderRequest):
177
+ rounded_orders = [
178
+ self._round_sz_px_cached(o["coin"], o["sz"], o["px"]) for o in orders
179
+ ]
180
+ if all(rounded_order is not None for rounded_order in rounded_orders):
181
+ return [
182
+ {**order, "asset": asset, "sz": sz, "px": px}
183
+ for order, (asset, sz, px) in zip(orders, rounded_orders, strict=True)
184
+ ]
185
+
158
186
  rounded_orders = await asyncio.gather(
159
187
  *(self._round_sz_px(o["coin"], o["sz"], o["px"]) for o in orders)
160
188
  )
161
189
  return [
162
190
  {**order, "asset": asset, "sz": sz, "px": px}
163
- for order, (asset, sz, px) in zip(orders, rounded_orders)
191
+ for order, (asset, sz, px) in zip(orders, rounded_orders, strict=True)
164
192
  ]
165
193
 
166
194
  async def _get_batch_market_orders(
@@ -175,15 +203,25 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
175
203
  slippage_factor = (1 + slippage) if order["is_buy"] else (1 - slippage)
176
204
  quoted_prices.append(market_price * slippage_factor)
177
205
 
206
+ rounded_orders = [
207
+ self._round_sz_px_cached(order["coin"], order["sz"], quoted_price)
208
+ for order, quoted_price in zip(orders, quoted_prices, strict=True)
209
+ ]
210
+ if all(rounded_order is not None for rounded_order in rounded_orders):
211
+ return [
212
+ {**order, "asset": asset, "sz": sz, "px": px, "order_type": order_type}
213
+ for order, (asset, sz, px) in zip(orders, rounded_orders, strict=True)
214
+ ]
215
+
178
216
  rounded_orders = await asyncio.gather(
179
217
  *(
180
218
  self._round_sz_px(order["coin"], order["sz"], quoted_price)
181
- for order, quoted_price in zip(orders, quoted_prices)
219
+ for order, quoted_price in zip(orders, quoted_prices, strict=True)
182
220
  )
183
221
  )
184
222
  return [
185
223
  {**order, "asset": asset, "sz": sz, "px": px, "order_type": order_type}
186
- for order, (asset, sz, px) in zip(orders, rounded_orders)
224
+ for order, (asset, sz, px) in zip(orders, rounded_orders, strict=True)
187
225
  ]
188
226
 
189
227
  async def cancel_order(self, coin: str, oid: int):
@@ -193,9 +231,15 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
193
231
  return await self.cancel_orders(cancels)
194
232
 
195
233
  async def cancel_orders(self, cancels: BatchCancelRequest):
196
- assets = await asyncio.gather(
197
- *(self.get_coin_asset(coin) for coin, _ in cancels)
198
- )
234
+ assets = [
235
+ asset
236
+ for coin, _ in cancels
237
+ if (asset := self._lookup_cached_asset_id(coin)) is not None
238
+ ]
239
+ if len(assets) != len(cancels):
240
+ assets = await asyncio.gather(
241
+ *(self.get_coin_asset(coin) for coin, _ in cancels)
242
+ )
199
243
  action = {
200
244
  "type": "cancel",
201
245
  "cancels": [
@@ -212,9 +256,15 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
212
256
  return await self.batch_cancel_by_cloid([(coin, cloid)])
213
257
 
214
258
  async def batch_cancel_by_cloid(self, cancels: list[tuple[str, Cloid]]):
215
- assets = await asyncio.gather(
216
- *(self.get_coin_asset(coin) for coin, _ in cancels)
217
- )
259
+ assets = [
260
+ asset
261
+ for coin, _ in cancels
262
+ if (asset := self._lookup_cached_asset_id(coin)) is not None
263
+ ]
264
+ if len(assets) != len(cancels):
265
+ assets = await asyncio.gather(
266
+ *(self.get_coin_asset(coin) for coin, _ in cancels)
267
+ )
218
268
  action = {
219
269
  "type": "cancelByCloid",
220
270
  "cancels": [
@@ -190,8 +190,8 @@ class InfoAPI(AsyncAPI):
190
190
  payload = {"type": "meta", "dex": dex}
191
191
  return await self.post(payload)
192
192
 
193
- async def get_perp_meta_ctx(self) -> PerpMetaCtx:
194
- payload = {"type": "metaAndAssetCtxs"}
193
+ async def get_perp_meta_ctx(self, dex: str = "") -> PerpMetaCtx:
194
+ payload = {"type": "metaAndAssetCtxs", "dex": dex}
195
195
  return await self.post(payload)
196
196
 
197
197
  async def get_perp_dexs(self) -> PerpDexs:
@@ -1,5 +1,5 @@
1
+ import math
1
2
  from typing import Any, List
2
- from decimal import Decimal
3
3
 
4
4
  import msgpack
5
5
  from eth_utils.crypto import keccak
@@ -174,14 +174,17 @@ def sign_action(
174
174
 
175
175
 
176
176
  def round_float(x: float) -> str:
177
+ if not math.isfinite(x):
178
+ raise ValueError("round_float requires finite number", x)
179
+
177
180
  rounded = f"{x:.8f}"
178
181
  if abs(float(rounded) - x) >= 1e-12:
179
182
  raise ValueError("round_float causes rounding", x)
180
183
 
181
- if rounded == "-0":
182
- rounded = "0"
183
- normalized = Decimal(rounded).normalize()
184
- return f"{normalized:f}"
184
+ trimmed = rounded.rstrip("0").rstrip(".")
185
+ if trimmed in {"", "-0"}:
186
+ return "0"
187
+ return trimmed
185
188
 
186
189
 
187
190
  def ensure_order_type(order_type: OrderType) -> OrderType: