模拟器:2026-09-28T16:55:54Z → 17:05:43Z,267 笔交易,真实净利 −$376.26。报告器输出的净利:$+741,670.47。 差额来自 2 笔交易,来自 1 个被解析错的池价,和真实市价差 372 倍。 直接成因两条:均值回归策略的出口绑在「下一笔成交」上(59% 同块进出),以及整条链路上没有任何一道 px 合理性校验。 同一份代码里还有第三个缺陷:WS 订阅字段名写错,行情修订从未抵达——但它不是这个 74 万的成因(价格经 REST 兜底一直有效),只是让 mid 始终停留在慢通道上。 全部事实点在本轮对现场文件复核过,复核命令与实测值见文末附录,可重跑。
1. 选题
主标题
一个 372 倍的价格,把 7 天模拟交易的净利从 -376 美元刷成了 +74 万
(42 字符,≤70 达标。数字具象型 + 反差型;结果在前,成因在后。)
description
HyperEVM MEV 纸面交易模拟器跑了 10 分钟、267 笔,真实净利 −$376.26,报告器却输出 +$741,670.47。差额来自 2 笔把 88.71 的市价当成 33,000.11 的伪造成交;直接成因是均值回归策略的出口绑在「下一笔成交」上,以及缺失的价格合理性校验。同代码另有一个不影响该数字的缺陷:WS 订阅字段名写错,行情修订从未抵达。
体裁:深度分析 / 拆解(以「报告数字离谱 → 逐层定位 → 2 条直接成因 + 1 条无关缺陷」推进)
为什么这个选题值得写
近 7 天本机的高信息密度变更里,这条最符合「具体到能复现」的标准:
- 有可引用的现场数字(267 笔、−$376.26、+$741,670.47、372 倍)
- 有明确的代码位置(paper.js:46 / 55 / 56 /
simS2) - 有可重跑的验证命令(见附录)
- 主题是「仪表盘数字骗人」这类普遍存在的工程问题,不依赖 MEV 领域知识也能读懂
与前几轮 content 产出刻意错开
| 轮次 | 主题 | 误判类型 |
|---|---|---|
| content-20260927-211550 | 账号身份误判 | 实体归属 |
| content-20260928-004757 | mmtls 诊断方向误判 | 逆向方向 |
| content-20260928-020756 | fleet 调度闸门自身的三个 bug | 调度基础设施 |
| content-20260928-021448 | 微信图片库物理层复核 | 数据资产完整性 |
| 本轮 | 模拟交易报告器的数字自欺 | 度量层 / 指标可信度 |
前几轮都在回答「这个东西到底是不是它声称的那个东西」;本轮回答的是「这个数字到底是不是它声称的那个数字」。度量层可信度是本系列尚未覆盖的角度。
2. 正文草稿
报告说赚了 74 万,真实是亏了 376 块
先看两个数字,它们出自同一批数据、同一分钟:
| |
| |
同一个 trades.jsonl,同一时刻读。一个说净赚 74 万、盈亏比 1450;另一个说净亏 376 块、S2 策略胜率 1.3%。
这不是四舍五入的差别,是符号的差别。
这是榜单上唯一一个「模拟器声称自己赢了、其实是全输」的案例,而且它本来会被自动发进日报。
那 74 万从哪来:两笔把 88 块当成 3 万 3 的成交
把 267 条记录按净利绝对值排序,前两名:
| |
两条的 entry / exit 里都出现了同一个数字:33000.10650784375。
同一时间窗内 HYPE 的实时市价是多少?直接查 Hyperliquid 的 REST 接口:
| |
33000.1065 ÷ 88.713 = 372.0。 这个价格不是市场给的,是解析器算错的。
这两笔伪造成交贡献了多少?fake gross sum = $742,048.94。全表净利 +$741,670.47。去掉这两笔,剩下 265 笔合计 −$376.26。
也就是说:整个「盈利 74 万」的结论,100% 由这 2 笔制造,且方向完全相反。
缺陷一(与 74 万无因果):WS 订阅字段名写错,行情修订从未抵达
先讲一个不是成因的缺陷,因为它最容易被误当成成因。
paper.js 的 connectWs() 里,订阅语句是这样写的(paper.js:46):
| |
而实际连上去发同一条消息,Hyperliquid 直接退回错误:
| |
把 orderBook 换成 l2Book,立刻有数据:
| |
哪怕订阅名改对了,下一层还是错的——paper.js:55-56 的处理逻辑是:
| |
真实 l2Book 消息里,levels 是 [bids, asks] 两个数组(每条是 {px, sz, n} 对象数组),不是 {b, a} 对象。所以修复不是改一个字段名,而是订阅名 + 解析结构两处。
后果:WebSocket 通道全程零有效数据,state.obMid 永远拿不到值。
边界:价格来源其实没断,断的是「显示」
这里我要先否掉一个看起来更严重的假设。
看到心跳日志里连续 9 条 mid=?,第一反应是「价格输入断了,策略在裸奔」。但往下查,这个结论只对一半。
paper.js 的 pollLoop() 里有兜底:mid 为空时调用 REST allMids()。而 REST 通道是通的:
| |
那么 mid=? 从哪来?出在打印上,不在策略上。allMids 返回的 @107 是字符串 "88.713",而心跳打印表达式写的是 state.obMid?.toFixed?.(4) ?? '?':
| |
字符串没有 toFixed 方法,可选链短路成 ?。每一条 mid=? 的心跳,实际上都拿着一个有效市价,只是没能打出来。
这个区分很重要:如果结论停在「价格断了」,修错地方;真实情况是策略一直有价可用,WS 修订更新这条慢通道只是没生效。「日志里看不到」不等于「数据不存在」——这是本次最容易被误判的一点。
成因一:S2 的出口选错,59% 的交易同块进出
价格有了,那 33000 这种离谱值是怎么进到成交记录里的?看设均值回归策略的 simS2。
它的设计意图是:池价偏离 mid 超过阈值就入场,等价格回归后离场。但离场条件写成了「同池的下一笔成交」——不管中间隔了多久、价格回了没回。
统计 holdBlks 分布:
| |
156 笔 S2 交易里,92 笔在同一区块内开仓又平仓,持仓块数的中位数是 0。 但均值是 10.65、最长一笔扛了 137 个区块——这是个双峰分布:要么立刻出,要么拖很久。没有一档是策略原本设想的「等价格回归」的那个时间尺度。
这不是均值回归,是把「两个相邻 swap」当成了一次「回归」。
后果是费用被反复摩擦:
| |
同块组的毛利是 −$2.54(意味着连方向都没对),费用却照收 $201.20。费用项 size*(feePct/100)*2 + gasUsd*2 与持仓时长无关,同块进出照样付两次 AMM 费 + 两次 gas。毛利接近零乘以高频,等于把费用乘了几百遍。
而那个 33000 的价格,正因为出口选在「deviation 超阈的下一笔成交」上,完全不受任何合理性约束——一个解析异常值可以和任意正常的"下一笔"配对成一次「回归」。
成因二:没有合理性校验,所以 372 倍的价差被当成套利机会
回到最上游。池价 px 来自 eth_getLogs 拉回的 Uniswap V3 Swap 事件,按 a0/a1 的符号与 decimals 还原:
| |
这条路径上没有任何一步校验 px 是否落在合理区间。Number(a1)/D1 算出来的值,不代表池子的真实价格:Uniswap V3 的 Swap 事件只带 amount0 / amount1 / sqrtPriceX96 / liquidity / tick,价格应当从 sqrtPriceX96 还原,而代码直接拿转账量做除法——只有在这笔 swap 恰好不动价格时,两者才相等。偏离越大,|px/mid-1| 越大,越容易同时触发 S1 的套利判定和 S2 的入场。异常值不是被过滤掉,而是被当成「最大的机会」优先交易。
至于那个 33000.10650784375 具体是怎么算出来的,我没能定位到根因:eth_getLogs 对该区块的历史查询返回空(区块 47131225 与 47131232 我去拉了,但 RPC 没给我原始 data),无法逐字段复算。
这里只陈述已经验实的两件事:一是这个价格与同时刻真实市价差 372 倍,二是在生产它的那段代码里,不存在任何一道会拦住它的检查。机制待定,缺口确凿。
把这三节收拢一下。真正造成那 74 万的,只有后两条:
- S2 出口绑「下一笔成交」→ 任意异常值都能凑成一次「回归」(直接成因)
- 无 px 合理性校验 → 异常值被当成套利机会入场(直接成因)
- WS 修订没生效 → mid 只从 REST 慢通道取(真实缺陷,但与本例无因果:mid 一直是有效的 88.7)
换句话说:就算现在就修好 WS 订阅,那个 33000 的价格照样会入场、照样会被记成一笔 37 万的盈利。 挡不住它的是第 2 条,不是第 3 条。
这个数字本来会去哪里
如果这条流水线按原计划跑完 7 天,mev_report.py 会这样播报:
| |
代入本次数据:
| |
一个净亏的模拟,会以「净$+741670」的标题、info 级别,进入日报收件箱。 级别判定只看 net >= 0,不看盈亏比是否荒唐、单笔是否占了全表净利的 100%。
这里有个值得记住的教训:当上游指标本身失真时,基于该指标的健康分级会反向工作——赚得越假,级别越安静。profitFactor: 1457.30 本该是个刺眼的信号,但没有任何一行代码检查它。
改动优先级(尚未实施)
按「堵住本次漏口 → 修复并存缺陷 → 加固下游报告」排序,都是小改动:
- 给 px 加区间校验(最小改动、最高收益)。在
pollLoop还原 px 后加一道Math.abs(px/mid - 1) > 0.05直接丢弃;任何偏离 5% 的池价,在这个交易对上都只可能是解析错误。 - 修 WS 订阅与解析。
type从orderBook改l2Book;levels从.b/.a改成数组下标[0]/[1],元素取.px。 - S2 出口加时间约束。移除或收紧
holdBlks==0的同块平仓——同块进出在均值回归语义下不成立。 - 报告器加健全性断言。在
mev_report.py加一条:单笔|net|超过全表|net|中位数的 N 倍就当异常值剔除或标红;profitFactor > 100直接升级为warn。
收束
回到最早的那个发现。
跑这条模拟的初衷,是用真金白银之前的零成本环境验证「S1 taker 搬砖 / S2 均值回归」两个策略到底能不能赚钱。第一轮 10 分钟的数据已经给出了答案,而且不是模拟器播报的那个答案:
| |
S1 胜率高、净利接近零,说明「偏离阈值 0.05%」对 prjxA/nest 这两个 0.05% 费率的池子刚好在盈亏平衡线上;S2 则是负期望,费用项吃掉了一切。
如果没有在报告器上多看一眼,这本会是一篇「7 天模拟净赚 74 万」的结论——发布出去,然后在下一次真实交易里被市场纠正。7 天模拟的全部价值,都押在「报告数字是真的」这一条上;而这条恰恰是整条流水线里唯一没被校验过的地方。
附录:事实点复核记录
复核时间:2026-09-29 01:00–01:10 CST
复核对象:/home/li/mev-monitor/(非 git 仓库,现场文件)
重要前提:mev-paper.service 是常驻进程(systemd Restart=always),trades.jsonl 在我写稿期间持续增长。本文所有数字锁定在一份冻结快照上,快照文件 /tmp/mev_final.jsonl(267 笔,末笔时间 2026-09-28T17:05:43.160Z)。复跑时数字会变,但结构性结论不变——下面每条都给验证方法。
| # | 事实点 | 实测值 | 来源 / 复核命令 |
|---|---|---|---|
| 1 | 服务在跑、且是常驻 | active (running) since 2026-09-29 00:54:34 CST;ExecStart=/home/li/.local/bin/node paper.js;Restart=always | systemctl --user status mev-paper;cat mev-paper.service |
| 2 | 快照规模与时间窗 | 267 笔,16:55:54.244Z → 17:05:43.160Z(9 分 49 秒,600 区块 ≈ 0.98 s/块) | python3 -c "import json;...;print(min/max ts, max-min blk)" |
| 3 | 报告器输出净利(对冻结快照跑) | n: 267,net: 741670.47,profitFactor: '1457.30',winPct: '29.6' | 把 267 行冻结集复制为 trades.jsonl 后跑 node report.js |
| 4 | 真实净利(剔除 2 笔离群) | −$376.26(265 笔) | 过滤 abs(net)>1000 后求和 |
| 5 | 2 笔伪造成交的 gross 合计 | $742,048.94 | 取 gross > 1000 两条求和 |
| 6 | 伪造价格与真实市价之比 | 33000.10650784375 / 88.713 = 372.0 | node -e "fetch(info,allMids)" 取 @107 |
| 7 | REST allMids 通道正常 | has @107: true;value @107: 88.713 | 同上;注意返回是字符串 |
| 8 | WS 订阅字段名错误 | channel=error ... "type\":\"orderBook\"... | 发 {type:'orderBook',coin:'@107'} 给 wss://api.hyperliquid.xyz/ws |
| 9 | 正确订阅名可拿到盘口 | BID=88.724 ASK=88.739 mid=88.7315 | 同上,改发 {type:'l2Book'} |
| 10 | WS 处理层结构不匹配 | 代码读 levels.b[0].px;真实 l2Book 为 levels[bids[],asks[]],元素 {px,sz,n} | paper.js:55-56;wsprobe4.mjs 打印 keys |
| 11 | mid=? 是显示 bug 非输入断供 | "88.713".toFixed?.(4) ?? '?' → ?;88.713.toFixed(4) → 88.7130 | node -e 复现。判据用「有没有打出过数字」而非计数:grep -cE 'mid=[0-9]' paper-stdout.log = 0(全程无一条心跳打出过数值);grep -c 'mid=?' 随进程增长,写作时 9、复核时 25 |
| 12 | S2 同块进出占比与持仓分布 | holdBlks==0:92/156 = 59%;全分布 mean=10.65 median=0 max=137 | 按 holdBlks 分组计数/统计 |
| 13 | 同块组毛利与费用 | gross −$2.54 / fees $201.20 / net −$203.74 | 同上分组求和 |
| 14 | 分策略真实盈亏 | S1: n=111 win=67.6% net=+$3.90;S2: n=154 win=1.3% net=−$380.17 | 剔除离群后按 strat 分组 |
| 15 | 费率配置(解释池间差异) | prjxA/nest = 0.05%,kitt/prjxB = 0.30%;obTakerFeePct=0.045 | config.json |
| 16 | 日报会播报什么 | title = "MEV模拟 X天:267笔 胜率29.6% 净$+741670",level = "info" | mev_report.py 第 4 步代入 |
| 17 | 该报告已注册进调度 | mev_report → /usr/bin/python3 /home/li/mev-monitor/mev_report.py,flock=/tmp/cron-mev-report.lock | git show 4ed2477 -- scripts/windmill_registry.json |
一处自我纠错(写进正文第 4 节)
初查时看到 9 条心跳全是 mid=?,第一反应是「WS 断了、策略没有价格输入」。往下追发现只对一半:WS 修订通道确实从未生效(#8、#10),但 pollLoop 有 REST allMids 兜底且该通道正常(#7);mid=? 是打印层的字符串 toFixed 调用失败(#11),不是数据缺失。结论修正为「慢通道可用、修订通道失效、日志显示失真」三者并存。
未复核/存疑
- 33000.10650784375 的精确成因未定位。本轮试拉了
blk 47131225/47131232的 prjxA 池eth_getLogs,RPC 返回空(未能取回原始data),因此无法逐字段复算。文中正文第 6 节只陈述两件已验实的事:该价格与同时刻真实市价差 372 倍;生产它的代码段里没有任何合理性检查。机制待定,缺口确凿——不写成已验证结论。 - 由此产生的连带不确定性:上述两笔的
gross(合计 $742,048.94)是错误输入的算术结果,而非真实的交易机会。文章对「74 万从哪来」的归因到「2 笔离群 + 1 个错价」为止,不再往下推测具体解析路径。 paper.js启动时 stderr 有 3 行/usr/bin/env: 'node': No such file or directory,而当前服务运行正常、/usr/bin/node不存在。可能是历史上曾用 shebang/env node方式启动留下;未影响当前运行,未深究。- 快照仅 267 笔 / 10 分钟,任何「策略长期期望」的结论都不成立。文中所有策略评估均限定在这个窗口内。
复跑方式
| |
