Hyperliquid 下单报 Order has invalid price:低价币价格精度算错
现象:主币能成交,小币全失败
策略在 testnet 跑了两周,BTC、ETH 的下单一直正常。加入一批低价币之后,所有小币种的单子都被拒:
1 | {"status":"err","response":"Order has invalid price."} |
不是余额不足,不是杠杆超限,就是价格不合法。而且失败是选择性的——同一个策略、同一套代码,价格在 1 以上的币全部通过,0.0x 区间的币全部被拒。
这个模式本身就指向了答案:问题跟「价格怎么格式化」有关,跟下单逻辑无关。
根因:价格被舍入成了 0
先看当时的格式化代码:
1 | px = round(price, 2) |
简单粗暴,两位小数。对 BTC(60000 以上)毫无影响,对价格 0.001234 的币:
1 | round(0.001234, 2) |
价格变成了 0.0。交易所收到一个价格为 0 的订单,返回 Order has invalid price 完全合理——这不是交易所苛刻,是客户端送了个垃圾数据。
但为什么会写死两位小数?因为最初的代码从交易所返回的元数据里取「价格精度」字段:
1 | px_decimals = asset_meta["pxDecimals"] # 想象中应该有这个字段 |
而 Hyperliquid 的 meta 接口返回的 universe 里**只有 szDecimals,没有 pxDecimals**。取不到就 KeyError,外面套了个 try/except 兜底成默认值 2——错误被吞掉了,问题被藏了两个月。
Hyperliquid 的真实精度规则
价格精度不是一个独立字段,而是由两条约束共同决定(永续合约):
- 最多 5 位有效数字
- 小数位数最多
6 - szDecimals
两条取更严格的那个。
举几个例子:
| 价格 | szDecimals | 允许小数位 | 结果 |
|---|---|---|---|
| 62345.7 | 5 | 1 | 62346(5 位有效数字) |
| 3.14159 | 4 | 2 | 3.14 |
| 0.0012345 | 0 | 6 | 0.001234(小数位被 cap 到 6) |
| 0.00001234 | 0 | 6 | 0.000012 |
注意最后一行:价格太小时,6 - szDecimals 这条约束比 5 位有效数字更严格,会把有效数字压到 2 位。精度不够时只能放弃一部分有效数字,否则下单必失败。
修复:按规则推导精度
1 | import math |
szDecimals 从哪来?POST /info 带 {"type":"meta"},返回的 universe 数组每一项都有 szDecimals:
1 | meta = post_info({"type": "meta"}) |
顺带暴露的第二个问题:精度直接决定最小下单量
修好价格之后,小币还是有一部分失败,报的是另一类错误——Order has invalid size。
原因相同:下单数量也有精度要求,由 szDecimals 直接给出。数量 round(size, 2) 对 szDecimals=0 的币(只能整数)没问题,但对 szDecimals=4 的币会把 0.00005 舍成 0.0,同样变成无效值。
更麻烦的是最小名义金额。Hyperliquid 要求单笔订单名义价值不低于 10 USDC。低价币按固定金额计算数量时:
1 | size = 10 / 0.00001234 ≈ 810,000 枚 |
这时候仓位规模远超预期。所以下单前必须同时校验三个值:
1 | def validate_order(px: float, sz: float, sz_decimals: int, min_notional=10.0) -> str | None: |
在真正发单前跑一遍,错误在本地下单前就暴露,而不是等交易所拒绝。
为什么这个 bug 藏了两个月
复盘一下,有三层原因叠加:
第一层,pxDecimals 字段确实不存在,但代码用 try/except 兜底而不是报错。异常被吞了,程序继续跑,只是精度错了。
第二层,主币占总成交量的大头,日常测试基本都在主币上跑。低价币只在扩展标的池时才被引入,时机很晚。
第三层,交易所的报错信息 Order has invalid price 没有说明为什么无效。它不会告诉你「你的价格是 0」。如果报错里带上收到的价格,定位时间会从两天缩到两分钟。
第三点值得单独记:下单前把格式化后的字符串打日志。这不是可有可无的调试信息,而是唯一能事后定位精度问题的证据。现在每次发单都会记录:
1 | [order] BTC px=62346 sz=0.001 → px_str=62346 sz_str=0.001 |
出问题时,一眼就能看出是价格错了还是数量错了。
收口
低价币下单失败,九成是精度问题,排查顺序:
- 打印格式化后的价格字符串,看是不是
0或0.00 - **查
szDecimals**,确认6 - szDecimals这条约束 - 验证有效数字,5 位是硬上限
- 校验名义价值,≥ 10 USDC 是另一道门槛
根本原则是:精度不能靠猜,也不能靠兜底。取不到精度信息就应当让程序失败,而不是悄悄用一个默认值继续跑——默认值在 99% 的场景下是对的,恰好会让人永远发现不了那 1%。
这次修复的完整背景在 记录一次 Hyperliquid Testnet 自动交易亏损原因核对,同期的风控加固(白名单 fail-closed、脏价格守卫)见 记录一次 Hyperliquid 链上-DB 漂移告警与 A2 扫描。