async-hyperliquid 0.4.5__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.5 → async_hyperliquid-0.4.6}/PKG-INFO +1 -1
  2. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/pyproject.toml +1 -1
  3. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/core.py +180 -35
  4. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/info.py +53 -6
  5. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/orders.py +62 -12
  6. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/signing.py +8 -5
  7. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/LICENSE +0 -0
  8. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/README.md +0 -0
  9. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/__init__.py +0 -0
  10. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/__init__.py +0 -0
  11. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/_async_hyperliquid/actions.py +0 -0
  12. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/async_api.py +0 -0
  13. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/async_hyperliquid.py +0 -0
  14. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/exchange.py +0 -0
  15. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/info.py +0 -0
  16. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/__init__.py +0 -0
  17. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/constants.py +0 -0
  18. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/decorators.py +0 -0
  19. {async_hyperliquid-0.4.5 → async_hyperliquid-0.4.6}/src/async_hyperliquid/utils/miscs.py +0 -0
  20. {async_hyperliquid-0.4.5 → 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.5
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.5"
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,58 @@ 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)
187
+ coin_names.setdefault(name, asset_name)
188
+ coin_names.setdefault(quote_name, quote_name)
156
189
 
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)
190
+ asset_sz_decimals[asset] = base_info["szDecimals"]
191
+ spot_tokens[asset_name] = base_info
192
+ spot_tokens.setdefault(quote_name, quote_info)
160
193
 
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
- }
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("@")}
165
196
 
166
- async def init_metas(self) -> None:
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:
167
239
  # TODO: Add HIP-4 outcome meta initialization from the spot info
168
240
  # `outcomeMeta` endpoint once outcomes move beyond testnet-only rollout.
169
241
  # Outcome asset IDs do not follow the current perp/spot offset scheme:
@@ -179,17 +251,36 @@ class AsyncHyperliquidCore(AsyncAPI):
179
251
  meta_task, spot_meta_task, all_dex_names_task
180
252
  )
181
253
 
182
- self._init_perp_meta(meta, 0)
183
- 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
+ )
184
273
 
274
+ dex_index_by_name = {
275
+ name: idx for idx, name in enumerate(all_dex_names) if name is not None
276
+ }
185
277
  dex_meta_tasks = []
186
278
  dex_indices = []
187
279
  for dex in self.perp_dexs:
188
280
  if dex == "":
189
281
  continue
190
- try:
191
- idx = all_dex_names.index(dex)
192
- except ValueError:
282
+ idx = dex_index_by_name.get(dex)
283
+ if idx is None:
193
284
  continue
194
285
 
195
286
  if idx > 0:
@@ -200,9 +291,49 @@ class AsyncHyperliquidCore(AsyncAPI):
200
291
  dex_metas = await asyncio.gather(*dex_meta_tasks)
201
292
  for idx, dex_meta in zip(dex_indices, dex_metas):
202
293
  dex_asset_offset = PERP_DEX_OFFSET + (idx - 1) * 10000
203
- 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)
204
334
 
205
- self._update_coin_symbols()
335
+ async def init_metas(self) -> None:
336
+ await self._run_meta_refresh(only_if_missing=False)
206
337
 
207
338
  async def get_metas(self, perp_only: bool = False) -> Metas:
208
339
  metas: Metas = {"perp": {}, "spot": [], "dexs": {}} # type: ignore
@@ -244,31 +375,45 @@ class AsyncHyperliquidCore(AsyncAPI):
244
375
  return names
245
376
 
246
377
  async def get_coin_name(self, coin: str) -> str:
247
- if not hasattr(self, "coin_names") or coin not in self.coin_names:
248
- await self.init_metas()
378
+ coin_name = self._lookup_cached_coin_name(coin)
379
+ if coin_name is not None:
380
+ return coin_name
249
381
 
250
- 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:
251
388
  raise ValueError(f"Coin {coin} not found")
252
389
 
253
- return self.coin_names[coin]
390
+ return coin_name
254
391
 
255
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
+
256
397
  coin_name = await self.get_coin_name(coin)
257
- if coin_name not in self.coin_assets:
398
+ asset = self.coin_assets.get(coin_name)
399
+ if asset is None:
258
400
  raise ValueError(f"Coin {coin}({coin_name}) not found")
259
401
 
260
- return self.coin_assets[coin_name]
402
+ return asset
261
403
 
262
404
  async def get_coin_symbol(self, coin: str) -> str:
263
405
  coin_name = await self.get_coin_name(coin)
264
406
  return self.coin_symbols[coin_name]
265
407
 
266
408
  async def get_coin_sz_decimals(self, coin: str) -> int:
267
- coin_name = await self.get_coin_name(coin)
268
- if coin_name not in self.coin_assets:
269
- raise ValueError(f"Coin {coin}({coin_name}) not found")
270
-
271
- 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")
272
417
  return self.asset_sz_decimals[asset]
273
418
 
274
419
  async def get_token_info(self, coin: str) -> SpotTokenMeta:
@@ -27,22 +27,55 @@ from .core import AsyncHyperliquidCore
27
27
 
28
28
 
29
29
  class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
30
- async def _get_perp_mark_price(self, coin_name: str, dex: str = "") -> float:
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:
31
56
  meta_ctx = await self.info.get_perp_meta_ctx(dex)
32
57
  meta = cast(PerpMeta, meta_ctx[0])
33
58
  asset_ctxs = cast(list[PerpMetaCtxItem], meta_ctx[1])
34
59
 
60
+ if asset_index is not None and 0 <= asset_index < len(asset_ctxs):
61
+ return float(asset_ctxs[asset_index]["markPx"])
62
+
35
63
  for idx, info in enumerate(meta["universe"]):
36
64
  if info["name"] == coin_name:
37
65
  return float(asset_ctxs[idx]["markPx"])
38
66
 
39
67
  raise ValueError(f"Coin {coin_name} not found in perp dex '{dex}'")
40
68
 
41
- async def _get_spot_mark_price(self, coin_name: str) -> float:
69
+ async def _get_spot_mark_price(
70
+ self, coin_name: str, asset_index: int | None = None
71
+ ) -> float:
42
72
  meta_ctx = await self.info.get_spot_meta_ctx()
43
73
  meta = cast(SpotMeta, meta_ctx[0])
44
74
  asset_ctxs = cast(list[SpotMetaCtxItem], meta_ctx[1])
45
75
 
76
+ if asset_index is not None and 0 <= asset_index < len(asset_ctxs):
77
+ return float(asset_ctxs[asset_index]["markPx"])
78
+
46
79
  for info in meta["universe"]:
47
80
  if info["name"] == coin_name:
48
81
  asset_index = info["index"]
@@ -52,18 +85,32 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
52
85
 
53
86
  async def get_mark_price(self, coin: str) -> float:
54
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
+ )
55
94
  return await self._get_perp_mark_price(coin, get_coin_dex(coin))
56
95
 
57
96
  coin_name = await self.get_coin_name(coin)
58
- if coin_name not in self.coin_assets:
97
+ asset = self._lookup_cached_asset_id(coin_name)
98
+ if asset is None:
59
99
  raise ValueError(f"Coin {coin}({coin_name}) not found")
60
100
 
61
- asset = self.coin_assets[coin_name]
62
101
  is_spot_asset = SPOT_OFFSET <= asset < PERP_DEX_OFFSET
63
102
  if is_spot_asset:
64
- return await self._get_spot_mark_price(coin_name)
103
+ return await self._get_spot_mark_price(
104
+ coin_name, asset_index=asset - SPOT_OFFSET
105
+ )
65
106
 
66
- return await self._get_perp_mark_price(coin_name, get_coin_dex(coin_name))
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
+ )
67
114
 
68
115
  async def get_market_price(self, coin: str) -> float:
69
116
  warnings.warn(
@@ -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": [
@@ -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: