订单跟踪系统BUG38
订单跟踪系统 — 复杂度审计报告
审计范围:src/trading/executor.py · src/trading/websocket_order_manager.py
验证方式:静态分析 + 测试网实测(scripts/verify_order_tracking.py --coin ICP --size 10)
一、审计结论
发现 5 项复杂度问题,根源均为对 Hyperliquid API 响应结构的错误假设。
测试网验证推翻了两个核心前提:
| 假设 | 实测结果 |
|---|---|
query_order_by_oid 返回 avgPx / totalSz |
从不返回,两字段对任意状态均缺失 |
userFills 比 orderUpdates 晚到 |
同时到达(实测差值 0ms) |
直接后果:
_backfill_order_price/_verify_fill_completeness写入的字段永远是 0,是有网络开销的空操作_close_limit_leg_timeout的remaining恒等于原始仓位量,平仓数量计算始终错误has_fill_price字段与filled_size > 0语义完全重合,是冗余状态
二、问题清单
问题 A — 两个死函数(executor.py)
函数:_backfill_order_price(L698–724)、`_verify_fill_completeness`(L726–766)
死因:两函数均调用 resp.get("avgPx") 和 resp.get("totalSz"),
而 query_order_by_oid 的响应对 filled 和 canceled 均无这两个字段。
每次调用只产生网络开销,不产生任何效果。
调用点(共 3 处,均为死代码):
_track_limit_order:if/else has_fill_price分叉,两个分支分别调用两函数_close_limit_leg_timeout:else分支调用_backfill_order_pricelimit_open:调用_backfill_order_price
净删除:~70 行 + 3 处调用
问题 B — _close_limit_leg_timeout 解析错误(executor.py)
位置:~L787–818
错误:
filled_sz = self._safe_pos_float(resp.get("totalSz")) # → 永远 0
fill_px = self._safe_pos_float(resp.get("avgPx")) # → 永远 0
totalSz / avgPx 不在响应顶层,正确结构为 resp["order"]["origSz"] 和 resp["order"]["sz"]。
后果:filled_sz 恒为 0 → remaining = position_size(全量)→ 超时平仓总是按全量下单。
remaining == 0 的分支(L810–818)永远不会到达,是不可达代码。
正确解析:
inner = resp.get("order", {})
orig_sz = self._safe_pos_float(inner.get("origSz"))
remain_sz = self._safe_pos_float(inner.get("sz"))
filled_sz = max(0.0, orig_sz - remain_sz) if orig_sz > 0 else 0.0
fill_px = self._safe_pos_float(inner.get("limitPx")) # avgPx 不可用,limitPx 最优近似
问题 C — has_fill_price 冗余字段(websocket_order_manager.py)
字段:OrderTracking.has_fill_price: bool = False(L48)
冗余原因:has_fill_price 的语义等价于 filled_size > 0。
filled_size 在每次 fill 事件时累计,有 fill 则必然 > 0,无 fill 则必然 == 0。
两个字段表达同一个事实,无需单独维护。
引用点(共 4 处需处理):
OrderTracking字段声明(L48):删除_resolve()内赋值(~L259):删除_on_order_filled_event内赋值(~L378):删除_on_order_status_event内条件判断(~L319):替换为filled_size > 0
保留
_grace_timer:验证显示正常路径两事件同时到达、timer 不启动。
但若userFillsWS 消息丢失,timer 是防止订单卡在 PENDING(直至 600s 超时)的唯一保障。
风险高于收益,不删除。
问题 D — _parse_order_response 解析错误(websocket_order_manager.py)
位置:~L537–568
错误:
avg_px = _pos_float(resp.get("avgPx") or resp.get("avg_px")) # → 永远 0
total_sz = _pos_float(resp.get("totalSz") or resp.get("total_sz")) # → 永远 0
后果:_http_check 通过 HTTP 检测到已成交/已撤销订单时,
resolve 写入 avg_price=0、filled_size=0,价格信息完全丢失。
正确解析:
inner = resp.get("order", {})
total_sz = max(0.0, _pos_float(inner.get("origSz")) - _pos_float(inner.get("sz")))
avg_px = _pos_float(inner.get("limitPx")) # avgPx 不可用,limitPx 是最优近似
问题 E — _http_check 魔法字符串(websocket_order_manager.py)
位置:~L493–530
问题:函数返回 "open" / "resolved" / "stale" / "busy" / "error" 5 个字符串,
调用方用字符串比较做分支,类型不安全,可读性差,且 "stale" 语义含糊。
改动:返回值改为 Optional[bool]
| 旧返回值 | 语义 | 新返回值 |
|---|---|---|
"resolved" / "stale" |
已处理 | True |
"open" |
仍挂单,应 TIMEOUT | False |
"busy" / "error" |
跳过本轮 | None |
签名:def _http_check(self, oid: int) -> bool | None:
调用方模式(2 处统一):
result = self._http_check(oid)
if result is True:
return
if result is False:
self._resolve(oid, tracking, OrderStatus.TIMEOUT)
return
# None → error/busy,继续重试逻辑
三、修改汇总
| 文件 | 修复项 | 操作 | 净变化 |
|---|---|---|---|
src/trading/executor.py |
A | 删除 2 函数 + 3 调用点 | −~70 行 |
src/trading/executor.py |
B | 修复 _close_limit_leg_timeout 解析 |
~0 行(改写) |
src/trading/websocket_order_manager.py |
C | 删除 has_fill_price 字段及 4 处引用 |
−4 行 |
src/trading/websocket_order_manager.py |
D | 修复 _parse_order_response 解析 |
~0 行(改写) |
src/trading/websocket_order_manager.py |
E | _http_check 返回类型改为 Optional[bool] |
~0 行(改写) |
总净删除:约 85 行,无新增抽象层。
四、执行顺序
- Fix A:先删函数体,再删 3 处调用点(避免遗漏)
- Fix B:独立改写
_close_limit_leg_timeout内解析逻辑 - Fix C:搜索
has_fill_price全部引用,逐一处理 - Fix D:改写
_parse_order_response两行 - Fix E:改返回值签名 + 统一 2 处调用方
五、验证方法
python scripts/verify_order_tracking.py --coin ICP --size 10
| 验证点 | 预期结果 |
|---|---|
| Fix A 完成后 | grep -r "_backfill_order_price|_verify_fill_completeness" src/ 结果为空 |
| Fix B 完成后 | Verify B 输出 filled = origSz - sz = 0.0(无成交挂单) |
| Fix D 完成后 | HTTP 路径 resolve 时日志中 size/price 非 0 |
| Fix E 完成后 | grep "_http_check" src/ 调用方无裸字符串比较 |
附:API 响应结构说明
query_order_by_oid → query_order_status 解包后的结构:
{
"order": {
"origSz": "10.0",
"sz": "10.0",
"limitPx": "2.0542",
"side": "B",
"coin": "ICP",
"oid": 48961810081
},
"status": "canceled"
}
不存在的字段:avgPx、totalSz(仅在下单即时响应 exchange.order() 中存在)
成交量计算:filled = origSz - sz(取消时两者相等,成交 = 0;部分成交再取消时两者不等)