你刚想出一个方案,接下来该做的不是验证它,是专门去推翻它。 这两件事的差别不是态度,是默认值: 验证的默认是「成立,除非发现问题」,对抗验证的默认是 「不成立,除非它扛住了攻击」。
你有一个想法,然后去找支持它的证据 —— 这是最自然的做法,也是最不可靠的:
| 你在做 | 实际结果 |
|---|---|
| 找支持的证据 | 一定找得到(数据够多的话总有) |
| 找到了就更相信 | 信心增长和真实性无关 |
| 遇到反例就找解释 | 解释也一定找得到 |
问题在于:「找得到支持证据」这件事,对真方案和假方案都成立, 所以它不携带信息。能区分真假的只有一件事: 你认真去杀它,而它没死。
关键是不要同时扮演提案人和审查人。我们的做法是让审查方带着明确任务: 目标不是「评估这个方案」,是「找出它为什么不成立」。
| 规则 | 说明 |
|---|---|
| 默认它是错的 | 举证责任在提案方,不在审查方 |
| 「说不通」就算被推翻 | 不需要证明它错,只需要指出它没被证明对 |
| 审查方不负责修 | 提出问题就够了,修是提案方的事 |
| 多个角度分开攻 | 正确性、成本、执行、口径 —— 不同角度看到的问题不同 |
第二行是这套东西的核心,也是最难接受的一条: 默认无罪 vs 默认有罪,用在这里必须是后者。 因为一个没被证明的方案,和一个错的方案, 在花钱这件事上后果完全一样。
我们有一条真钱线,上线前做过一轮对抗审查,抓到 13 个缺陷,全部修完才上线。 其中两条是致命的:
| 缺陷 | 后果 | |
|---|---|---|
| ① | 代码里调用的撤单方法在开发库里根本不存在, 而错误被一个宽泛的异常捕获吞掉了 | 这个钱包唯一的撤单防线永远不会执行,而且不报错 |
| ② | 已处理记录只存在内存里 | 重启后会对同一个窗重复下单; 崩溃循环时可叠加多笔 |
两条都有同一个特征:正常运行时完全看不出来。 第一条要等到真的需要撤单时才暴露,第二条要等到重启时才暴露 —— 而那两个时刻恰好都是你最不希望出问题的时刻。
① 凡是「自动重启」的程序,每个状态都要问一句「重启后它还在吗」。 内存里的状态在重启后归零,而程序会当作什么都没发生过继续跑。
② 宽泛的异常捕获会把「方法不存在」这种硬错误变成静默失败。 一个永远不报错的防线,和没有防线是一样的 —— 但你会以为自己有。
③ 资金地板的「未知」两个方向都不能错。 未知当成 0 会假停机;未知当成正常会盲跑。在途资金必须显式建模。
不只是代码。我们有过一个自己提的交易方案, 被对抗验证推翻,最后一分钱没花就归档了。
这类审查问的问题很朴素,但很难糊弄过去:
| 问题 | 它挡住什么 |
|---|---|
| 这个数的分母是什么? | 把子集比率说成全样本比率 |
| 这个因果源里写着,还是你接的? | 把合理的推测当成记录 |
| 你切了多少个子集才挑出这个? | 在噪声里挑最大的那个 |
| 这个近似的偏差方向是什么? | 把乐观上界当成实际值 |
| 换个口径还成立吗? | 结论其实来自量尺而非数据 |
最后一条我们栽得最惨:一个「怎么切都在」的漂亮规律, 最后发现整个是标签造出来的假象。 凡是「换什么条件都还在」的发现,都该先怀疑它根本不在数据里,而在量尺里 —— 全过程在 你的回测赢了,可能只是尺子错了。
| 做法 | 说明 |
|---|---|
| 隔一天再看 | 时间差能部分模拟「不是同一个人」 |
| 先写「它会怎么死」 | 见 预先认领的死法 |
| 把方案讲给不懂的人听 | 讲不清楚的地方通常就是没想清楚的地方 |
| 强制自己写三条反驳 | 写不出三条,说明还没认真想过它会怎么错 |
| 先算成本再看收益 | 大量方案在算完成本那一步就死了 |
「找得到支持证据」对真假方案都成立,所以它不携带信息。 唯一携带信息的是:你认真去杀它,而它没死。 在花钱这件事上,没被证明的方案和错的方案后果一样 —— 所以默认值必须是「不成立」。