pmwallets 的 PnL 管线遇到算不准的仓位,默认做法是排除:无法定价、NegRisk 转换解释不了、生命周期事件配不上对,这些 (钱包, 市场) 不进胜率和排名,按原因单独计数披露。这个做法的代价是,管线自己出 bug 时,结果看起来也只是「排除多了一点」,不会报错。09-26 这一天连续查出四处这样的问题,记在这里。
自动兑换的仓位被算成 0
那天用浏览器插件检查钱包,发现有盈利的仓位在卡片上显示为 0。
沿着自动兑换路径检查,才找到原因。09-26 的修复提交记录,Polymarket 从 08-28 起使用自动兑换合约(0xa120…),市场结算后把赢单份额从钱包转走再兑换。在当时的账本里,这段钱包侧流动按 ERC-1155 直接转账估值,计入现金流;调查窗口中约 66% 的兑换额经过这条路径。
估值在 stats_values 计算。09-21 的 bebf1a7 将 tr 和 cvc 物化,却把它们排在 values 前面,两者 LEFT JOIN 到本轮空表,v 为 NULL,标成 unpricedTransfer / unpricedConv。受影响的直接转账和 NegRisk 转换让整个 (钱包, 市场) 被排除。
修法是把 values 挪回 points 之后(0c19fb5),再加一个 test/pnl-step-order.test.ts:直接读源码,按执行顺序检查每一步读到的 stats_* 表都已经被更早的步骤写过。自动兑换合约同时加进 SYSTEM_ADDRESSES,否则修好之后它会作为一个钱包上榜。
顺序改回来之后,tr 开始 join 完整的估值,30 天窗口单条语句 22.95 GiB 被杀,又按 cityHash64(tokenId) 分了 8 批,两侧都带批次谓词。生产上用只读 SELECT 试跑,第 0 批 6.28 GiB / 74 秒。
给排除加一个上限
回头看,这个 bug 期间每一轮被管线原因排除的份额都在 76–77%,修复后第一轮是 1.97%。数字一直摆在那里,只是没有任何东西去看它。
于是加了发布前的「管线原因排除占比」校验(b9c04ba)。分子分母随 run 写进 pnl_runs.preflight.faultExcluded,排除异常增多时拒绝发布。后续复审把清淡市场的 weakMark 从故障分子中移出;--force 越过校验的 run 标成 forced,不进 *_latest。
校验加上之后,PnL 集成测试在 beforeAll 失败。合成账本主要由排除用例组成,也触发了发布限制,后来补入正常仓位,让测试能继续验证各个排除场景。
0 金额转账
第二个问题在 NegRisk。路由合约代为调用时,stakeholder 可能是路由,不能直接当成用户。归因先按事件列出期望 token、数量和方向,再到同一笔交易中找实现全部期望腿的钱包。解释不了的流动标 adapterConversion,排除。
路由在赎回时会附带一些 0 金额的转账。它们永远对不上任何期望腿,于是整个 condition 被当成解释不了。一轮 30 天的计算里有 117 万笔这样的转账,牵连 49,120 个钱包。修法是 NegRisk 流动只看 amount > 0,同时加了一条集成用例:同一笔交易里非零的未解释流动仍然要排除。
同时发现 backfill-negrisk 从来没跑过。4,792 个 NegRisk 市场里有 3,660 个市场结构未知,归因无从谈起。两处一起修完,7 天窗口里因 adapterConversion 排除的买入额从 $1,720 万降到 $300 万,涉及钱包从 11,130 个降到 190 个。
回填从每秒约 5,400 块降到 550 块,原先估计 1–2 小时,实际用了 7 小时。09-27 增加 backfill-negrisk --staging,长扫描先写暂存表,最后用 promote-negrisk-staging 并入。先并条件准备,再并市场结构,避免中断后留下缺准备事件的市场结构。并入仍可能改变正在运行的统计指纹,让该轮拒发,但不必整个扫描期间都停 stats。命令保留暂存表,核对每个事件键已进入目标表,便于重跑。
partialHistory 判得太宽
第三个问题是 partialHistory。账本有覆盖起点 coverageStart,一个市场如果在起点之前就创建了,钱包在起点时的持仓我们不知道,期初余额不可信,这个市场对该钱包排除。
判断「市场何时创建」用的是 firstBlock,而它只取了两张 token 注册表。有一个头部钱包交易过 27,171 个市场,只有 95 个出现在注册表里,其余全部被判成 partialHistory。这些市场里有 18,937 个是在覆盖起点之后才创建的,链上有它们的 ConditionPreparation 事件(存在 condition_preps),历史是完整的。
firstBlock 加上市场创建事件之后,7 天窗口里被 partialHistory 排除的买入额从 $7.51 亿(85%)降到 $1.56 亿(18%)。
weakMark 不再整条排除
第四处是估值口径调整。成交不足以形成强标记价时,退回最近成交价并标 weakMark。原来这类市场整条排除,09-26 的提交改为计入盈亏,保留标记供披露。
回填期间每轮拒发
同一天还修了一个和回填有关的发布失败。09-25 起 30d 和 7d 连续失败:balances(读转账)、fpc(读成交)和三处 resolutions 都读「≤ b1 的全部历史」,而当时正在往 coverageStart 以下回填历史,一轮计算中途,期初持仓和价格就被改了,发布指纹每轮都对不上。
流水读取加上 coverageStart 下限,避免本轮读到计算中途写入的更早区块。指纹同步收窄到本轮实际读取范围。注册表按全部读取列的整行内容去重计数,再合并 live/backfill 登记,不能只按事件键计数;NegRisk 结构也限定读取范围。test/pnl-step-order.test.ts 还检查流水读取是否带下限。
这天之后 CLAUDE.md 多了一条硬规则:回填与发布可以同时进行,回填期间拒发算回归。
pmwallets 工程笔记(3/7)