记录一次测试盲区连环排查(2026-10-15)
记录一次测试盲区连环排查(2026-10-15)
恒定量是等价写法的天然盲区——同一个“看起来对”的实现,在恒定输入下与错误实现无法区分。
今天没跑什么新策略,几乎一整天都在跟测试盲区较劲。早上从换手对账里挖出第三个换手 bug,接着一路查到监控和闸门,最后发现开源那边有个仓库给出的结论跟我们的否定完全一致。
一、BUG-3:首日持仓被当成首日空仓
自查 A2 时用纯经济口径对账,场景 [1,-1,1,-1,1,-1](连续每日翻向)引擎 tos 合计 9.000,经济口径 11.000。根因在 _prev.iloc[0] = 0.0,把第一行的上一仓位强制写成 0,等于假设第 0 天空仓。sig_eff 丢掉了第 0 行,因为 ret 也从第 1 行开始,sig.shift(1)*R 表示第 1 天吃第 0 天的仓位收益。一旦首日持仓 + 次日翻向,引擎只算 |-1 - 0| = 1,经济口径应该是 |-1 - 1|(平多) + |1|(首日建仓) = 3,全期 9 vs 11。
修法:_prev.iloc[0] = sig.iloc[0],再 tos.iloc[0] += sig.iloc[0].abs().sum() 补计首日建仓。修复后 6 个场景全部一致,连续每日翻向从 9 变 11。
第一版测试 test_first_entry_cost_is_charged 用的是恒定持仓(0.5 每天不变),恰好是两种写法等价的那个场景。测试用例本身有盲区。 新增参数化对账测试 test_turnover_matches_economic_fills,覆盖 5 种权重形态,直接与不依赖引擎的第一性原理口径 _econ_turnover() 对账。注入验证:把修法回退成 _prev.iloc[0]=0.0,weights3 确实失败。
二、skipna 与相对路径:两个老熟人
今天回头看过往 bug 记录,发现 BUG-1 和 BUG-2 都是同一类“静默失效”。BUG-1 是 tos = sig_eff.diff().abs().sum(axis=1).fillna(...),diff() 首行 NaN 被 .sum(axis=1) 默认 skipna=True 求成 0.0,fillna 无事可填,建仓成本从未计入。这与 all([]) 返回 True 完全同类:pandas 的 skipna 默认把缺失静默变成 0。 修法改成以空仓为基准 _prev = sig_eff.shift(1); _prev.iloc[0] = 0.0,属性测试 全程满仓只换手一次(首日) 从 FAIL 变 PASS。
BUG-2 是 watchdog 的 count_processes 用绝对路径关键词匹配不上相对路径启动的进程。实测注入一份相对路径副本后,实际 2 个进程,count_processes(绝对路径keyword) 返回 1,watchdog 报“✅ 正常”,重复永不暴露。修法把采集类 keyword 改通配 *xxx.py,smart_money_trader 保持绝对路径不动。修复后实测实际 2 ⇒ watchdog 计数 2。
另外,我此前告诉用户“watchdog 的存活判定是 [>=1] 不是 [==1]”——这是错的。 代码里有 EXPECTED_COUNT = {"smart_money_trader": 2} 与 elif count > want: 的重复检测分支,真正问题不是判定式而是匹配不上。
三、dvol_monitor 的 min_periods 陷阱
下午补 dvol_monitor 测试(WorkBuddy 第七轮缺口清单排第二),抓到 BUG-8:min_periods = max(30, win // 3),当 win<30 时 min_periods > window,pandas 直接抛 ValueError: min_periods 30 must be <= window 20,被 main() 的 try/except 吞成 state="ERROR" → overall 落成 INSUFFICIENT → 退出码 0。操作者传 --win 20 看到的是“样本不足”,不是“你传了非法参数”,且不会收到任何告警。
修法:mp = min(int(win), max(30, int(win) // 3)),任何 win 都不抛。默认 win=180 不受影响,属潜伏问题。
新增 tests/test_dvol_monitor.py 14 条,覆盖 fetch_dvol 的解包、rq 的 shift(1) 前视防护、perm_p 的退化输入、evaluate 状态机等。注入验证:去掉 rq 的 shift(1),test_rq_shifts_one_bar_to_avoid_lookahead 确实失败。
我在这个文件上犯了 4 个测试侧错误,其中两条值得记:造数据和解析时间戳本身就是被测逻辑的一部分,写错会让正确的实现看起来是错的。 比如 pd.Timestamp(BASE + n*DAY) 把毫秒当纳秒,全部解析成 1970-01-01。
四、市场状态闸门的 fail-open 告警漏洞
补闸门测试时又抓到 BUG-7:fail-open 状态永不告警。原代码 unknown = det.startswith("fail-open"),v_key = None if unknown else False,而 _regime_log_state 初始值也是 None,于是首次 fail-open 时 None is not None 为 False,不打日志;此后状态不变,永远不再打。API 长期故障时操作者永远看不到任何告警,闸门静默退化成“永远放行”且无痕迹。
修法:三态用互不相同的哨兵值,_regime_log_state = {"v": "__unset__"},v_key = "unknown" if unknown else False。
附带发现:我的第一版 fixture 写死 T._regime_log_state = {"v": "__unset__"},把模块真实初始值 None 覆盖掉了,注入 BUG-7 后测试仍全绿。改回 {"v": None} 后注入立刻被抓。fixture 重置的必须是生产代码的真实初始值,而不是你觉得“干净”的值。
五、开源核查与协议 v2:外部独立否定
快傍晚时看 open-source review 的第二批,三个仓库都有 LICENSE,其中 richkuo/go-trader 是唯一达到机构级严谨度的:预注册 + 哈希冻结,成本模型 commission_pct 0.00045、slippage_pct 0.0005、stress slippage_multiplier 2.0,资金费来自 Hyperliquid fundingHistory 且文件带哈希,判定纪律包括“资金费不完整的窗口不许作为通过依据”。33 个候选结果:0 个通过,31 fail,2 inconclusive。
对比我们自己的协议 v2,五个候选全部 fail,最有说服力的是配对超额表:所有 NW t 落在 −2.4 ~ +1.0 之间,没有一个达到显著;Bonferroni×24 后全部 p=1.0。候选的绝对收益都是正的(最高 +122%),但配对超额全部不显著——它们赚钱的方式就是持有。 两个完全独立的系统得到同一结论,这个结论不是本项目的样本缺陷或方法错误。
遗留
尚未修/未覆盖的:入场簇数用“换手>0的天数”冒充、not_concentrated 分母是天不是笔、load_data() 仍 0 测试——它决定了所有数字的输入,一旦读错数据,上面 75 条测试全部照常通过。下一步先把 load_data() 补上,再把 go-trader 那 9 类规则移植里的剩余项逐条过掉。