我怎么骗了自己两个月
但真正值得写下来的不是那些结论 —— 是我怎么会一次又一次地相信它们。 下面每一条都对应一个我实际踩过的坑,不是「应该注意」的清单。
一、前视偏差:最容易骗到自己的一类
我有两条策略一开始跑出来是强正的。查下去,两条都是代码里的前视。
坑 1:用「该分钟均值」当挂单价
我的分钟网格 MD[i,t] 是第 t 分钟中价的均值。
它包含第 45–60 秒的信息 —— 而我在第 0–45 秒撮合。
决策用到了决策之后的价格。
| 修正前 | 修正后 | |
|---|---|---|
| 随机方向 | +5.22bp/天 | −18.48bp/天 |
| 信号方向 | +8.65bp/天 | −11.80bp/天 |
| 正号标的 | 33/45 | 0/41 |
坑 2:信号本身用了未来
fac[t] 是第 t 分钟的因子收益 —— 要到该分钟结束才知道。
而我在该分钟开头下单。改用 fac[t−1] 后,
taker 版从 +4.65bp 变成 −2.70bp。
均值尤其危险,因为它看起来无辜 —— 你不会觉得「取个平均」有什么问题。
可操作的检测:前视的特征是「收益随前视量单调变化」。 加一个
--batch-ms N 参数扫 0/5/10/15/30 秒,看结果会不会动。
会动就是有前视。
还有一个高发模式值得单独记:「批量刷新最新状态 + 遍历历史事件」。
任何 refresh_all_latest() 后面跟着
for event in past_events 的代码都要警惕 ——
你在用「现在」的状态去处理「过去」的事件。
修法是把状态更新和事件按发生时间归并成一条流顺序回放。
二、幽灵成交:回测亏钱的头号假来源
我一开始用「价格触及/穿越我的限价 ⇒ 成交」做回测。
后来有了一个干净样本 —— 「他确实挂着单」的那些时刻 —— 拿它校准:
| 情形 | 发生次数 | 他实际成交的比例 |
|---|---|---|
| 价格穿过他的限价 | 38,230 | 3.2% |
| 价格正好打在他的限价上 | 5,222 | 41.3% |
朴素模型造出了 12.45 倍的幽灵成交。而这些幽灵不是中性的 —— 它们系统性有毒:
- 幽灵成交的 60 分钟 markout:−0.54bp、逆向比例 50.9%
- 穿透超过 16bp 的幽灵:−21.81bp、逆向 74.0%(穿透越深越毒)
换句话说,我用一个错误的成交假设,给自己造出了恰好「看起来合理」的结果。
三、口径:同一个东西的六个版本
「口径」这个词在我的文档里出现了 68 次,是最高频的一个。举两个实例:
费率
同一个「他的费率」,我在不同文档里并存过 0.14 / 0.269 / 0.29bp 三个数。 其中 0.269 出自早期仅 3,881 笔被动 + 265 笔主动的小样本, 后来用 23,135 笔实测是 0.14 —— 而且 p10 = p90 完全一致, 说明是常数费率不是逐笔滑点。小样本那个数大约是真实值的两倍。
我们自己的费率更乱,有 六个版本存活在不同文档里。
markout
同一个策略,两把尺子量出来差 6 倍:
- 干净 mid 口径:+0.90bp
- 官方 mid_px 口径:+5.50bp
纪律:口径必须全项目锁定成一个数,并声明权威排序。 口径不锁定的时候,所有 ✅ / ❌ 都是废话。
四、事后挑窗口:最贵的一个错误
我有一张三天的逐日表:09-01 −15.2 / 09-02 −2.2 / 09-03 +14.5(单位 bp)。
然后我把 +14.5 这一天写进了策略规格,作为「策略成型后的水平值」, 并据此得出「每美元名义收益率高于目标地址」的结论。
我用一套完全独立的实现复算(更保守的撮合:必须严格穿价;真实 oracle tick; 不假设 oracle 平价出场),五个 1.6 天块得到 −0.04 / +0.03 / +2.43 / +1.98 / +3.63 bp —— 两套实现是一致的,都在零附近。分歧只在解读:我把三天里最好的一天当成了水平值。
同类错误还有一次:我用「已知为正的 3 个窗口」验证一个配置, 3/3 全正,看起来很稳。后来跑完整的 8 个窗口:3/8、中位 −109.3bp。
永远不要只报你挑中的窗口。
五、自证式检验:比不检验更糟
我做过一次「价格审计」,报出 99.66% 命中率,非常漂亮。但那是同义反复 —— 我验的是「他自己的成交价是否落在 1 分钟 K 线的 [low, high] 内」, 而 K 线的 high/low 本来就是从成交里取的极值。99.66% 是数学必然。
真正该验的(我自己算出的 mid、距 mid 距离、markout 基准是否落在真实 K 线内) 一次都没做。
还有一个更隐蔽的变体:用自己写的笔记当「实测值」的出处。
我把自己的 strategy-v1.md 当权威引了五轮,
而那上面的数字本身就是错的 —— 那些「他的实测 RATIOS」其实是我自己写的笔记。
六、统计:不是所有「显著」都算数
样本量差 2 倍时的「两半同号」是弱证据
n 差 2 倍的两个独立正相关样本,本来就有约 3/4 的概率凑出同号。 报两半同号时必须同时给二项 p 值和两半幅度差。
把「币」当独立观测会低估方差
我有一条「卖侧特别强(+2.32bp)」的结论。用币聚类 bootstrap 压了 2,000 次之后, 置信区间 [−4.33, +0.20] —— 含 0。剔掉买侧少于 60 笔的币后从 +2.32 腰斩到 +1.135bp。
比较配置前先算统计功效
我实测了单窗口收益的标准差:488.7bp。反算每组需要的窗口数:
| 要分辨的差异 | 需要的窗口数 |
|---|---|
| 50bp | 1,568 |
| 100bp | 392 |
| 200bp | 98 |
| 400bp | 24 |
而我一直只在 3–8 个窗口上做比较 —— 那只能分辨 400bp 以上的差异。
改进:用配对差值代替各自的中位数排名。同一窗口跑两个配置取差值, 市场状况被抵消,标准差从 488.7bp 降到 143.8bp —— 功效提高约 23 倍。
七、把「测不出」说成「测出来了」
这是我最后期才意识到的毛病。当时我写了「六个状态族封板、数据侧封板 —— edge 在数据侧测不出」。
但真相是:L2 真队列根本没采集。
而我的采集脚本第 46–48 行订阅的就是完整的档位数组,只是代码里只取了
levels[0][0] —— 改成 levels[0][:N] 就能落深度。
阴性结果不是结论。找不出来就老实写「来源未定位」, 并给出下一步用哪个可观测输入去测 —— 不要把「我没测」包装成「测不出来」。
八、一些技术性的自杀方式
- 用几分钟前的参考价给新鲜订单定价,会系统性虚高约 8–10bp。 同一批成交,参考价从「成交前 1 秒」换成「成交当时」, 结果从 +12.92bp 掉到 +3.62bp。
- 「写了但没调用」等于静默退化。我的主循环把未聚合的原始 ticks 直接喂给模拟器, 而聚合函数写了却没被调用 —— 成交数爆炸到 4,420~26,212 笔/6 小时(真实是 2,642), 库存浮盈被放大到 ±数千万美元。
- 把瞬时最优价量当成日流量,会得出错误的容量否决。 「BBO 中位 $4,体量撑不起」是把瞬时最优价量当成了日流量。
- 对数空间成立的仓位在美元账本上持不住。我用对数收益算杠杆 ETF 配对, 12 组全部为正、t 值 3~18;改成美元收益并显式建模再平衡后, KORU/EWY 变成 +3.6%/Sharpe 0.21/回撤 −30.8%, SOXL/SMH +6.1%/Sharpe 0.35/回撤 −60.6% —— log 分解出的衰减项恰好被美元仓位的路径漂移抵消了。
- 把「区间里最好的一天」当成水平值(见第四节)。
九、我现在的检查清单
把这些坑压成可执行的动作:
- 判据在动手前写死,并对两个方向一视同仁。 含 0 的置信区间一律不可部署 —— 不许一个标 ✅ 另一个标 ❌。
- 做前视敏感性扫描:加一个前视量参数,看结果是否单调变化。
- 成交模型必须校准,把校准锚点(穿越 3.2% / at 41.3%)写进复现脚本, 并报告幽灵成交的 markout 与穿透深度的关系。
- 离线回放与线上账本必须对得上 —— 这是最有效的前视探测器。 同一份数据、两套独立实现必须给出相同结果。
- 报数时同时给中位与均值、等权与金额加权,并检查两者是否给出相反的部署结论。
- 用配对差值代替独立排名,功效提升一个量级。
- 归档 ≠ 删数据。我曾经以为一段数据被清掉了,
其实归档库就在
data/archive/里, 文件名写着日期,与当前库只差 32 秒、无缝衔接。 - 结论变负时先怀疑自己的口径和统计功效,而不是先去找新解释。 「回放对不上线上」和「换窗口就反转」,我一开始都当成噪声,查下去才发现是系统性错误。
这两个月我最大的收获不是找到了什么,而是知道了自己会怎么骗自己。
上面每一条都不是理论。它们对应的是具体的某一天、某一份跑出漂亮数字的报告, 以及我兴冲冲地把那个数字写进规格、然后被自己的另一份代码推翻的过程。
而自我推翻的频率本身就是样本不足的度量 —— 我在某一轮 13 天里推翻了 4 次。这比任何一条结论都更能说明我现在的阶段。
— 记录于此,明年回看 —