async-hyperliquid 1.1.0__tar.gz → 1.2.0__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 (93) hide show
  1. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/CHANGELOG.md +11 -0
  2. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/PKG-INFO +2 -2
  3. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/README.md +1 -1
  4. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/README.md +24 -0
  5. async_hyperliquid-1.2.0/benchmarks/fast_cancel.py +178 -0
  6. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/howto/orders.rst +5 -0
  7. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/howto/orders.po +12 -0
  8. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/pyproject.toml +1 -1
  9. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/client.py +28 -8
  10. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/exchange.py +11 -1
  11. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/types/exchange.py +2 -0
  12. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/LICENSE +0 -0
  13. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/__init__.py +0 -0
  14. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/hotpath.py +0 -0
  15. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/__init__.py +0 -0
  16. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/models.py +0 -0
  17. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/pacing.py +0 -0
  18. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/preflight.py +0 -0
  19. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/providers.py +0 -0
  20. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/reporting.py +0 -0
  21. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/results.py +0 -0
  22. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/runner.py +0 -0
  23. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live/workload.py +0 -0
  24. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/live_exchange.py +0 -0
  25. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/results/20260805T192621Z/cancel-id-latency.png +0 -0
  26. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/results/20260805T192621Z/cancel-id-latency.svg +0 -0
  27. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/results/20260805T192621Z/report.json +0 -0
  28. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/results/20260805T192621Z/samples.csv +0 -0
  29. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/benchmarks/signing.py +0 -0
  30. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/_static/language-switcher.css +0 -0
  31. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/_templates/sidebar/language-switcher.html +0 -0
  32. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/coin-name-mapping.md +0 -0
  33. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/conf.py +0 -0
  34. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/howto/index.rst +0 -0
  35. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/howto/info-queries.rst +0 -0
  36. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/howto/lifecycle-reconciliation.rst +0 -0
  37. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/howto/markets.rst +0 -0
  38. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/howto/routing.rst +0 -0
  39. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/howto/transfers-administration.rst +0 -0
  40. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/index.rst +0 -0
  41. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/introduction/index.rst +0 -0
  42. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/introduction/installation.rst +0 -0
  43. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/introduction/quickstart.rst +0 -0
  44. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/coin-name-mapping.po +0 -0
  45. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/howto/index.po +0 -0
  46. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/howto/info-queries.po +0 -0
  47. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/howto/lifecycle-reconciliation.po +0 -0
  48. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/howto/markets.po +0 -0
  49. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/howto/routing.po +0 -0
  50. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/howto/transfers-administration.po +0 -0
  51. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/index.po +0 -0
  52. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/introduction/index.po +0 -0
  53. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/introduction/installation.po +0 -0
  54. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/introduction/quickstart.po +0 -0
  55. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/migration-0.5-to-1.0.po +0 -0
  56. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/project/about.po +0 -0
  57. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/project/benchmarks.po +0 -0
  58. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/project/index.po +0 -0
  59. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/project/license.po +0 -0
  60. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/reference/async-hyperliquid.po +0 -0
  61. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/reference/errors.po +0 -0
  62. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/reference/exchange-client.po +0 -0
  63. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/reference/index.po +0 -0
  64. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/reference/info-client.po +0 -0
  65. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/reference/response-types.po +0 -0
  66. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/locale/zh_CN/LC_MESSAGES/reference/types.po +0 -0
  67. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/migration-0.5-to-1.0.md +0 -0
  68. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/project/about.rst +0 -0
  69. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/project/benchmarks.md +0 -0
  70. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/project/index.rst +0 -0
  71. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/project/license.rst +0 -0
  72. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/reference/async-hyperliquid.rst +0 -0
  73. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/reference/errors.rst +0 -0
  74. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/reference/exchange-client.rst +0 -0
  75. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/reference/index.rst +0 -0
  76. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/reference/info-client.rst +0 -0
  77. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/reference/response-types.rst +0 -0
  78. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/docs/reference/types.rst +0 -0
  79. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/__init__.py +0 -0
  80. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/_internal/__init__.py +0 -0
  81. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/_internal/encoding.py +0 -0
  82. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/_internal/exchange.py +0 -0
  83. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/_internal/http.py +0 -0
  84. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/_internal/info.py +0 -0
  85. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/_internal/metadata.py +0 -0
  86. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/_internal/signing.py +0 -0
  87. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/constants.py +0 -0
  88. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/errors.py +0 -0
  89. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/info.py +0 -0
  90. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/py.typed +0 -0
  91. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/types/__init__.py +0 -0
  92. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/types/common.py +0 -0
  93. {async_hyperliquid-1.1.0 → async_hyperliquid-1.2.0}/src/async_hyperliquid/types/info.py +0 -0
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-11-03
9
+
10
+ ### Added
11
+
12
+ - Add `fast=True` to `cancel_order()`, `cancel_orders()`, `cancel_by_cloid()`, and
13
+ `cancel_orders_by_cloid()` to send Hyperliquid's fast-cancel `f` flag; the flag
14
+ is omitted when `fast=False`. The exchange rejects fast cancels of trigger
15
+ orders per status, so `fast` stays opt-in.
16
+ - Add testnet integration coverage for fast cancels and a diagnostic
17
+ `benchmarks/fast_cancel.py` comparing fast and non-fast cancel latency.
18
+
8
19
  ## [1.1.0] - 2026-09-14
9
20
 
10
21
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-hyperliquid
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Async Hyperliquid client using aiohttp
5
5
  Keywords: dex,hyperliquid,async,aiohttp,trading,cryptocurrency,defi
6
6
  Author: Yuki
@@ -36,7 +36,7 @@ Description-Content-Type: text/markdown
36
36
 
37
37
  [![CI](https://github.com/owlyfi/async-hyperliquid/actions/workflows/ci.yml/badge.svg)](https://github.com/owlyfi/async-hyperliquid/actions/workflows/ci.yml)
38
38
  [![Documentation Status](https://readthedocs.org/projects/async-hyperliquid/badge/?version=latest)](https://async-hyperliquid.readthedocs.io/en/latest/)
39
- [![PyPI](https://img.shields.io/pypi/v/async-hyperliquid.svg?v=1.1.0)](https://pypi.org/project/async-hyperliquid/)
39
+ [![PyPI](https://img.shields.io/pypi/v/async-hyperliquid.svg?v=1.2.0)](https://pypi.org/project/async-hyperliquid/)
40
40
 
41
41
  `async-hyperliquid` is a typed, asynchronous Python client for the Hyperliquid
42
42
  REST API. It provides credential-free market and account queries through
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/owlyfi/async-hyperliquid/actions/workflows/ci.yml/badge.svg)](https://github.com/owlyfi/async-hyperliquid/actions/workflows/ci.yml)
4
4
  [![Documentation Status](https://readthedocs.org/projects/async-hyperliquid/badge/?version=latest)](https://async-hyperliquid.readthedocs.io/en/latest/)
5
- [![PyPI](https://img.shields.io/pypi/v/async-hyperliquid.svg?v=1.1.0)](https://pypi.org/project/async-hyperliquid/)
5
+ [![PyPI](https://img.shields.io/pypi/v/async-hyperliquid.svg?v=1.2.0)](https://pypi.org/project/async-hyperliquid/)
6
6
 
7
7
  `async-hyperliquid` is a typed, asynchronous Python client for the Hyperliquid
8
8
  REST API. It provides credential-free market and account queries through
@@ -207,6 +207,30 @@ Artifacts: [sanitized JSON](results/20260805T192621Z/report.json), [sample CSV](
207
207
  These measurements describe one testnet run, not exchange capacity. Shared-IP traffic, geography, testnet load, dependency versions, and network conditions can change results.
208
208
  <!-- live-exchange-benchmark:detail:end -->
209
209
 
210
+ ## Fast-cancel benchmark
211
+
212
+ `benchmarks/fast_cancel.py` compares single-order `cancel_order()` latency with
213
+ `fast=False` and `fast=True` on testnet. It uses the same `.env.local`
214
+ credentials and safety checks as the live Exchange benchmark (testnet only,
215
+ `HL_SK` must derive `HL_AK`, roles validated before any order).
216
+
217
+ Each round places `--orders` resting BTC ALO buys at `mid * 0.9` (about 12 USDC
218
+ each) in one batch, then cancels them sequentially, alternating the two methods;
219
+ the method that goes first swaps every round. Only the cancel request is timed,
220
+ pacing sleeps excluded. Any non-success cancel aborts the run, and remaining
221
+ orders are cancelled on exit.
222
+
223
+ ```bash
224
+ uv run --frozen --group benchmark python benchmarks/fast_cancel.py \
225
+ --rounds 30 --warmups 3 --orders 10
226
+ ```
227
+
228
+ The output is a plain-text table of n, mean, p50, p90, p99, min and max in ms.
229
+ Hyperliquid currently documents no effect for `fast` other than rejecting
230
+ trigger orders, so the two methods are expected to match today; the benchmark
231
+ is a baseline for the future mempool-priority upgrade. It is diagnostic only
232
+ and does not produce publishable artifacts.
233
+
210
234
  ## Signing benchmark
211
235
 
212
236
  This benchmark compares Hyperliquid signing and order-payload construction in
@@ -0,0 +1,178 @@
1
+ """Compare single-order cancel latency with fast=False and fast=True on testnet.
2
+
3
+ Each round places ``--orders`` resting BTC ALO buys far below mid in one batch,
4
+ then cancels them one at a time, alternating fast=False / fast=True. The method
5
+ that goes first swaps every round. Only the cancel request itself is timed;
6
+ pacing sleeps are excluded. Any non-success cancel invalidates the run.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import asyncio
13
+ import math
14
+ import os
15
+ import statistics
16
+ import sys
17
+ from collections.abc import Sequence
18
+ from pathlib import Path
19
+ from time import perf_counter_ns
20
+
21
+ if __package__ in (None, ""):
22
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
23
+
24
+ from async_hyperliquid import AsyncHyperliquid
25
+ from async_hyperliquid.types import (
26
+ CancelOrder,
27
+ CancelOrderResponse,
28
+ Network,
29
+ PlaceOrderRequest,
30
+ TimeInForce,
31
+ limit_order_type,
32
+ )
33
+ from benchmarks.live.models import MIN_INTERVAL_NS, BenchmarkFailure
34
+ from benchmarks.live.pacing import WeightedPacer
35
+ from benchmarks.live.preflight import Credentials, validate_roles
36
+
37
+ COIN = "BTC"
38
+ NOTIONAL_USDC = 12
39
+ PRICE_RATIO = 0.9
40
+
41
+
42
+ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
43
+ parser = argparse.ArgumentParser(description=__doc__)
44
+ parser.add_argument("--rounds", type=int, default=30)
45
+ parser.add_argument("--warmups", type=int, default=3)
46
+ parser.add_argument("--orders", type=int, default=10, help="even, per round")
47
+ parser.add_argument("--interval-ms", type=int, default=250)
48
+ args = parser.parse_args(argv)
49
+ if args.rounds < 1 or args.warmups < 0:
50
+ parser.error("--rounds must be positive and --warmups non-negative")
51
+ if args.orders < 2 or args.orders % 2:
52
+ parser.error("--orders must be an even number >= 2")
53
+ if args.interval_ms * 1_000_000 < MIN_INTERVAL_NS:
54
+ parser.error(f"--interval-ms must be at least {MIN_INTERVAL_NS // 1_000_000}")
55
+ return args
56
+
57
+
58
+ def fast_plan(round_index: int, count: int) -> tuple[bool, ...]:
59
+ """Alternate fast flags; the first method swaps every round for fairness."""
60
+ first = round_index % 2 == 1
61
+ return tuple(first if i % 2 == 0 else not first for i in range(count))
62
+
63
+
64
+ def summarize(samples_ns: Sequence[int]) -> dict[str, float]:
65
+ ms = sorted(sample / 1_000_000 for sample in samples_ns)
66
+ cuts = statistics.quantiles(ms, n=100, method="inclusive")
67
+ return {
68
+ "n": len(ms),
69
+ "mean": statistics.fmean(ms),
70
+ "p50": statistics.median(ms),
71
+ "p90": cuts[89],
72
+ "p99": cuts[98],
73
+ "min": ms[0],
74
+ "max": ms[-1],
75
+ }
76
+
77
+
78
+ def format_table(samples: dict[bool, list[int]]) -> str:
79
+ columns = ("n", "mean", "p50", "p90", "p99", "min", "max")
80
+ lines = [f"{'method':<12}" + "".join(f"{name:>10}" for name in columns)]
81
+ for fast in (False, True):
82
+ stats = summarize(samples[fast])
83
+ cells = "".join(
84
+ f"{stats[name]:>10d}" if name == "n" else f"{stats[name]:>10.2f}"
85
+ for name in columns
86
+ )
87
+ lines.append(f"{f'fast={fast}':<12}" + cells)
88
+ return "\n".join(lines) + "\n(latency in ms)"
89
+
90
+
91
+ def _cancelled(response: CancelOrderResponse) -> bool:
92
+ return response["status"] == "ok" and response["response"]["data"]["statuses"] == [
93
+ "success"
94
+ ]
95
+
96
+
97
+ async def _place_round(
98
+ client: AsyncHyperliquid, count: int, size_decimals: int
99
+ ) -> list[int]:
100
+ px = await client.info.mid_price(COIN) * PRICE_RATIO
101
+ step = 10**size_decimals
102
+ sz = math.ceil(NOTIONAL_USDC / px * step) / step
103
+ order: PlaceOrderRequest = {
104
+ "coin": COIN,
105
+ "is_buy": True,
106
+ "sz": sz,
107
+ "px": px,
108
+ "is_market": False,
109
+ "order_type": limit_order_type(TimeInForce.ALO),
110
+ }
111
+ response = await client.place_orders([order.copy() for _ in range(count)])
112
+ if response["status"] != "ok":
113
+ raise BenchmarkFailure(f"placement failed: {response['response']}")
114
+ oids: list[int] = []
115
+ for status in response["response"]["data"]["statuses"]:
116
+ resting = status.get("resting") if isinstance(status, dict) else None
117
+ if not isinstance(resting, dict):
118
+ raise BenchmarkFailure(f"order did not rest: {status!r}")
119
+ oids.append(resting["oid"])
120
+ return oids
121
+
122
+
123
+ async def _run(args: argparse.Namespace, credentials: Credentials) -> str:
124
+ pacer = WeightedPacer(interval_ns=args.interval_ms * 1_000_000)
125
+ samples: dict[bool, list[int]] = {False: [], True: []}
126
+ async with AsyncHyperliquid(
127
+ credentials.master_address,
128
+ credentials.signing_key,
129
+ vault_address=credentials.subaccount_address,
130
+ network=Network.TESTNET,
131
+ ) as client:
132
+ await client.info.refresh_metadata()
133
+ validate_roles(
134
+ credentials.master_address,
135
+ await client.info.user_role(credentials.api_wallet_address),
136
+ await client.info.user_role(credentials.subaccount_address),
137
+ )
138
+ size_decimals = await client.info.size_decimals(COIN)
139
+ pending: list[int] = []
140
+ try:
141
+ for round_index in range(args.warmups + args.rounds):
142
+ await pacer.wait(weight=1)
143
+ pending = await _place_round(client, args.orders, size_decimals)
144
+ plan = fast_plan(round_index, len(pending))
145
+ for oid, fast in zip(tuple(pending), plan, strict=True):
146
+ await pacer.wait(weight=1)
147
+ start = perf_counter_ns()
148
+ response = await client.cancel_order(
149
+ CancelOrder(COIN, oid), fast=fast
150
+ )
151
+ elapsed = perf_counter_ns() - start
152
+ if not _cancelled(response):
153
+ raise BenchmarkFailure(f"cancel failed: {response!r}")
154
+ pending.remove(oid)
155
+ if round_index >= args.warmups:
156
+ samples[fast].append(elapsed)
157
+ finally:
158
+ if pending:
159
+ await client.cancel_orders(tuple(CancelOrder(COIN, o) for o in pending))
160
+ return format_table(samples)
161
+
162
+
163
+ def main(argv: Sequence[str] | None = None) -> int:
164
+ args = parse_args(argv)
165
+ from dotenv import load_dotenv
166
+
167
+ load_dotenv(Path(".env.local"), override=False)
168
+ try:
169
+ credentials = Credentials.from_environ(os.environ)
170
+ print(asyncio.run(_run(args, credentials)))
171
+ except BenchmarkFailure as error:
172
+ print(f"fast-cancel benchmark failed: {error}", file=sys.stderr)
173
+ return 1
174
+ return 0
175
+
176
+
177
+ if __name__ == "__main__":
178
+ raise SystemExit(main())
@@ -406,6 +406,11 @@ Use ``CancelOrder(coin, oid)`` with ``cancel_order()`` or ``cancel_orders()``.
406
406
  Use ``CancelByCloid(coin, cloid)`` with ``cancel_by_cloid()`` or
407
407
  ``cancel_orders_by_cloid()``. Empty batches are rejected locally.
408
408
 
409
+ All four cancel methods accept ``fast=True``, which sends Hyperliquid's ``f``
410
+ flag. The exchange rejects fast cancels that target trigger orders, and a future
411
+ network upgrade will prioritize fast cancels in the mempool. With the default
412
+ ``fast=False`` the flag is omitted from the signed action.
413
+
409
414
  ``ModifyOrderRequest`` identifies the order by integer order ID or ``Cloid`` and
410
415
  contains the complete replacement order. ``modify_order()`` returns a default
411
416
  acknowledgement; ``modify_orders()`` returns per-order statuses.
@@ -288,6 +288,18 @@ msgstr ""
288
288
  "搭配使用。将 ``CancelByCloid(coin, cloid)`` 与 ``cancel_by_cloid()`` 或 "
289
289
  "``cancel_orders_by_cloid()`` 搭配使用。空批次会在本地被拒绝。"
290
290
 
291
+ #: ../../howto/orders.rst:409
292
+ msgid ""
293
+ "All four cancel methods accept ``fast=True``, which sends Hyperliquid's "
294
+ "``f`` flag. The exchange rejects fast cancels that target trigger orders,"
295
+ " and a future network upgrade will prioritize fast cancels in the "
296
+ "mempool. With the default ``fast=False`` the flag is omitted from the "
297
+ "signed action."
298
+ msgstr ""
299
+ "四个撤单方法都接受 ``fast=True``,用于发送 Hyperliquid 的 ``f`` 标志。"
300
+ "交易所会拒绝针对触发单(trigger order)的快速撤单;未来的网络升级会在 mempool "
301
+ "中优先处理快速撤单。默认 ``fast=False`` 时,签名的 action 中不包含该标志。"
302
+
291
303
  #: ../../howto/orders.rst:409 2815403bb6ba4974a7f7a2680ee41f54
292
304
  msgid ""
293
305
  "``ModifyOrderRequest`` identifies the order by integer order ID or "
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "async-hyperliquid"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  requires-python = ">=3.12"
5
5
  description = "Async Hyperliquid client using aiohttp"
6
6
  authors = [{ name = "Yuki", email = "yuqi.lyle@gmail.com" }]
@@ -332,12 +332,22 @@ class AsyncHyperliquid:
332
332
  )
333
333
 
334
334
  async def cancel_order(
335
- self, order: CancelOrder, *, expires_after: int | None = None
335
+ self,
336
+ order: CancelOrder,
337
+ *,
338
+ fast: bool = False,
339
+ expires_after: int | None = None,
336
340
  ) -> CancelOrderResponse:
337
- return await self.cancel_orders((order,), expires_after=expires_after)
341
+ return await self.cancel_orders(
342
+ (order,), fast=fast, expires_after=expires_after
343
+ )
338
344
 
339
345
  async def cancel_orders(
340
- self, orders: Sequence[CancelOrder], *, expires_after: int | None = None
346
+ self,
347
+ orders: Sequence[CancelOrder],
348
+ *,
349
+ fast: bool = False,
350
+ expires_after: int | None = None,
341
351
  ) -> CancelOrderResponse:
342
352
  commands = tuple(orders)
343
353
  if not commands:
@@ -350,16 +360,26 @@ class AsyncHyperliquid:
350
360
  for order, market in zip(commands, markets, strict=True)
351
361
  )
352
362
  return await self._exchange._submit_cancels(
353
- cancels, expires_after=expires_after
363
+ cancels, fast=fast, expires_after=expires_after
354
364
  )
355
365
 
356
366
  async def cancel_by_cloid(
357
- self, order: CancelByCloid, *, expires_after: int | None = None
367
+ self,
368
+ order: CancelByCloid,
369
+ *,
370
+ fast: bool = False,
371
+ expires_after: int | None = None,
358
372
  ) -> CancelOrderResponse:
359
- return await self.cancel_orders_by_cloid((order,), expires_after=expires_after)
373
+ return await self.cancel_orders_by_cloid(
374
+ (order,), fast=fast, expires_after=expires_after
375
+ )
360
376
 
361
377
  async def cancel_orders_by_cloid(
362
- self, orders: Sequence[CancelByCloid], *, expires_after: int | None = None
378
+ self,
379
+ orders: Sequence[CancelByCloid],
380
+ *,
381
+ fast: bool = False,
382
+ expires_after: int | None = None,
363
383
  ) -> CancelOrderResponse:
364
384
  commands = tuple(orders)
365
385
  if not commands:
@@ -372,7 +392,7 @@ class AsyncHyperliquid:
372
392
  for order, market in zip(commands, markets, strict=True)
373
393
  )
374
394
  return await self._exchange._submit_cloid_cancels(
375
- cancels, expires_after=expires_after
395
+ cancels, fast=fast, expires_after=expires_after
376
396
  )
377
397
 
378
398
  @staticmethod
@@ -195,24 +195,34 @@ class ExchangeClient:
195
195
  return await self._submit_action(action, "order", expires_after=expires_after)
196
196
 
197
197
  async def _submit_cancels(
198
- self, cancels: Sequence[EncodedCancel], *, expires_after: int | None = None
198
+ self,
199
+ cancels: Sequence[EncodedCancel],
200
+ *,
201
+ fast: bool = False,
202
+ expires_after: int | None = None,
199
203
  ) -> CancelOrderResponse:
200
204
  encoded = list(cancels)
201
205
  if not encoded:
202
206
  raise ValueError("orders must not be empty")
203
207
  action = CancelAction(type="cancel", cancels=encoded)
208
+ if fast:
209
+ # Hyperliquid rejects actions hashed with f=false, so only send f=true.
210
+ action["f"] = True
204
211
  return await self._submit_action(action, "cancel", expires_after=expires_after)
205
212
 
206
213
  async def _submit_cloid_cancels(
207
214
  self,
208
215
  cancels: Sequence[EncodedCancelByCloid],
209
216
  *,
217
+ fast: bool = False,
210
218
  expires_after: int | None = None,
211
219
  ) -> CancelOrderResponse:
212
220
  encoded = list(cancels)
213
221
  if not encoded:
214
222
  raise ValueError("orders must not be empty")
215
223
  action = CancelByCloidAction(type="cancelByCloid", cancels=encoded)
224
+ if fast:
225
+ action["f"] = True
216
226
  return await self._submit_action(action, "cancel", expires_after=expires_after)
217
227
 
218
228
  async def _post_envelope(
@@ -144,6 +144,7 @@ class EncodedCancel(TypedDict):
144
144
  class CancelAction(TypedDict):
145
145
  type: Literal["cancel"]
146
146
  cancels: list[EncodedCancel]
147
+ f: NotRequired[Literal[True]]
147
148
 
148
149
 
149
150
  class EncodedCancelByCloid(TypedDict):
@@ -154,6 +155,7 @@ class EncodedCancelByCloid(TypedDict):
154
155
  class CancelByCloidAction(TypedDict):
155
156
  type: Literal["cancelByCloid"]
156
157
  cancels: list[EncodedCancelByCloid]
158
+ f: NotRequired[Literal[True]]
157
159
 
158
160
 
159
161
  class EncodedModify(TypedDict):