你跑了一次回测,夏普 1.8,最大回撤 12%,曲线漂亮。
你上手做,三个月后曲线不长那样。
问题不在于"回测会失真"这句老生常谈——问题在于你把回测的输出当成了一次测量。它不是。它是一次抽样,而你只抽了一次,还是从你自己挑出来的那段历史里抽的。
前面十几篇反复出现同一句话:这个需要检验。趋势系统需要多少样本(第 30 篇)、加一个过滤器要付出什么代价(第 32 篇)、缠论规格化之后该怎么测(第 37 篇)——但「检验」这件事本身,手册一直没有正面写过。
这一篇补上它。
本系列仅用于记录与普及基础知识,不构成投资建议。这一篇全部是统计与工程方法,不涉及任何具体策略、标的或买卖规则。
零、和第 7 篇的分工
第 7 篇第九、十节已经给了一份很实在的清单:五个步骤 + 六个陷阱。那一份不重复。
两者的区别是:
| 第 7 篇 | 这一篇 | |
|---|---|---|
| 形式 | 清单——别做什么 | 流水线——按什么顺序做 |
| 判据 | 定性为主 | ⭐ 给数字和停止规则 |
| 覆盖 | 点名了 Walk-Forward、紧缩夏普 | ⭐ 把它们展开成可执行规格 |
打个比方
清单像一份体检项目表,流水线像急诊分诊。
体检表告诉你有哪些指标要查,但不排顺序——反正都要查一遍。分诊不一样:它决定先看什么,因为有些结果一出来就该停,后面的都不用做了。
⚠️ 清单和流水线不是同一个东西。 清单告诉你有六个坑,但不告诉你先跳哪个——而顺序恰恰决定了你会不会在还有救的时候就停下来。
这一篇的价值大半在顺序上:把最便宜、淘汰率最高的检查放在最前面,可以省掉后面全部的工作量,也省掉你在一个注定不成立的想法上继续投入感情。
一、⭐ 先接受一件事:回测的输出不是一个数
一次回测给你一条净值曲线、一个夏普、一个最大回撤。这些数字看起来是测量结果,但它们不是。
⭐ 它们是一次抽样。
同一个策略、同一套规则,如果历史沿着另一条同样合理的路径走过一遍,你会得到一组不同的数字。你看到的那一组,是从一个分布里抽出来的一个点。
这件事的后果是具体的:
- 「夏普 1.2」这句话没有信息量——你需要的是「夏普 1.2,95% 区间 [0.3, 2.1]」
- 「最大回撤 18%」是这条路径上的最大回撤,不是这个策略的最大回撤
- ⚠️ 而且你的参数正是在这条路径上挑的,所以这个 18% 系统性偏乐观
⭐ 整篇的方法论都建立在这一句上:把点估计换成分布。 第五节给具体做法。
二、⭐ 检验的顺序,以及顺序为什么不能换
⭐ 这就是分诊:排在前面的不是最重要的检查,是"一旦结果不对,后面全都不用做了"的那些。 顺序换了不会得出错误结论,只会让你在一个注定不成立的想法上多花几周。
九道关卡,按顺序过,不通过就停:
| # | 关卡 | 消耗样本吗 | 淘汰率 |
|---|---|---|---|
| 0 | 可实现性:数据当时存在吗、这个价位成交得了吗 | ❌ 不消耗 | 中 |
| 1 | ⭐ 成本关:毛收益够不够付往返成本 | ❌ 不消耗 | 最高 |
| 2 | 样本量关:数据量够不够检验这个量级的边缘 | ❌ 不消耗 | 高 |
| 3 | 样本内表现 | ✅ 消耗 | 低 |
| 4 | 参数稳健性 | ✅ 消耗 | 中 |
| 5 | 步进 / 样本外 | ✅ 一次性 | 中 |
| 6 | 多重比较修正 | — | 中 |
| 7 | ⭐ 相关性去重 | — | 常被跳过 |
| 8 | 小规模实盘 | 真金白银 | — |
排序原理和第 32 篇的七道关卡完全一样:先跑便宜的、淘汰率高的、且不消耗数据的。
前三关全都是纯算术:不需要跑回测,不消耗样本外数据,几分钟就能算完,而且它们合起来能淘汰掉大部分想法。在这三关之前就去写回测代码,是把最贵的资源花在最先该被否决的东西上。
⚠️ 必须是布尔判据,不能打分加权。 「成本关差一点,但样本内表现特别好,综合看还行」——这句话在数学上没有意义:成本是确定要付的,样本内表现是估计出来的,用一个估计量去抵消一个确定量,方向就是错的。(同第 26 篇第 3 层的理由。)
三、成本关为什么必须排第一
三个理由,一个比一个硬:
- 它不消耗任何样本——纯算术,算错了也不污染数据
- 它的淘汰率最高——第 31 篇那张费率表,短周期上大部分格子是 ❌
- ⭐ 它是确定量——成本是你一定会付的,而收益是估计的
一个可以直接算的判据
先用零成本跑一遍回测,得到每笔平均毛收益 g(以标的价格的百分比计),再取往返成本 c:
g < c ❌ 直接死,不用继续
c ≤ g < 2c ⚠️ 净期望不到毛期望的一半
——此时结论由「成本估得准不准」主导,而不是由策略主导
g ≥ 3c 🟢 可以进入下一关
⚠️ 中间那档是最危险的,因为它看起来「还能做」。但成本估计本身的误差(滑点分布、极端时段的价差扩大、部分成交)在那一档足以让结论翻转——你测的其实是你的成本模型,不是你的策略。
⭐ 一个第 7 篇没有的推论:成本会放大样本量需求
第 7 篇给的样本量公式是:
t = (μ ÷ σ) × √N 要 t = 2 ⇒ N = 4σ² ÷ μ²
关键在于 N 和 μ 是平方反比关系。而你真正要检验的不是「毛收益 > 0」,是**「扣完成本还 > 0」**:
μ_净 = μ_毛 − 成本
于是:
成本吃掉一半毛收益 ⇒ 需要的样本量变成 4 倍。 成本吃掉三分之二 ⇒ 变成 9 倍。
| 成本占毛收益 | μ 剩下 | 所需样本量 |
|---|---|---|
| 0% | 100% | ×1(基准) |
| 25% | 75% | ×1.8 |
| 50% | 50% | ×4 |
| 67% | 33% | ×9 |
| 75% | 25% | ×16 |
⚠️ 这条推论和第 31 篇的结论叠加起来很难受:短周期成本占比高,所以既更难赚,又更难证明自己赚了。 它唯一的补偿是笔数积累得快——但那个优势的一部分,正好被这里的倍数吃掉。
四、样本量关:不够就别测
第 7 篇的两张表(每笔 R × 标准差、年化夏普 × 年数)已经很完整,不重复。这里只补一条常被跳过的检查:
⭐ 先算你手上的数据在这个周期上到底有多少笔,再决定要不要测。
趋势系统,单品种日线,一年 10–20 笔
→ 10 年数据 = 100–200 笔
→ 按第 7 篇的表,平均 +0.20R / 标准差 1.0R 需要 100 笔
结论:勉强够,且只够检验「相当强」的边缘
⚠️ 如果算出来「需要 900 笔,我有 150 笔」,正确的动作是不要测——不是「先测了看看」。因为一旦你看过这份数据,它就不再是干净的样本外数据了(第六节)。
⭐ 这一关是免费的,而且它能在你写任何代码之前就否决掉一个想法。
五、⭐ Bootstrap:把单条曲线变成分布
这是第 7 篇没有的部分,也是第一节那句话的具体做法。
做法
原始数据:策略的 N 笔交易收益序列(或每日收益序列)
重复 B 次(B = 1000 或 10000):
从原序列中有放回地抽样,抽出同样长度的一条新序列
在这条新序列上算:夏普、总收益、最大回撤
输出:这三个指标各自的分布
取 2.5% 和 97.5% 分位 → 95% 区间
⚠️ 但直接这样做是错的,因为它打乱了顺序,而金融收益序列有自相关(波动率聚集,见第 35 篇)。
⭐ 必须用分块 bootstrap(Block Bootstrap)
不抽单个点,抽连续的块:
块长 L:取能覆盖你关心的自相关尺度
经验起点 L ≈ N^(1/3),日频数据常用 5–20 天
抽样:随机选起点,取连续 L 个点,拼接到长度 N
块长的作用很直接:L = 1 退化成普通 bootstrap(毁掉所有时间结构);L = N 就是原序列(什么也没做)。 中间才有意义。
⚠️ 块长本身是一个可调参数,所以要报告 L 在合理范围内变动时区间是否稳定——如果换个块长结论就变,说明结论依赖的是块长,不是策略。(这是第 37 篇跨定义检验的同一个思路。)
它能给你三样东西
| 输出 | 用途 |
|---|---|
| 夏普的 95% 区间 | ⭐ 区间下界跨过 0 ⇒ 你没有证据 |
| 最大回撤的分布 | 见下,这个最有用 |
| 总收益的区间 | 对预期做校准 |
最大回撤那一项值得单独说。 回测给你的是一条路径上的 MDD。跑 bootstrap 会发现分布的中位数通常明显大于它——因为参数就是在那条路径上选的。
仓位应该按 MDD 分布的高分位(比如 90%)来定,不是按回测里看到的那一个。
这条直接接到第 6 篇:用回测 MDD 反推杠杆,是在用一个系统性偏小的数当分母。
Bootstrap 不能修的东西
- 它假设未来和过去同分布——市场结构变了它不知道
- 它不能修复过拟合:如果你的策略是拟合出来的,bootstrap 会忠实地给出一个漂亮的区间
- 它不能修复前视偏差——垃圾进,精确的垃圾出
Bootstrap 量化的是「运气的宽度」,不是「逻辑的对错」。
六、步进检验的具体规格
第 7 篇提了一句「前进式检验(Walk-Forward)」,这里给规格。
两种模式
滚动(Rolling):训练窗固定长度,整体向前滑
[训练 24m][测试 6m]
[训练 24m][测试 6m]
[训练 24m][测试 6m]
锚定(Anchored):训练窗起点固定,只往右扩
[训练 24m][测试 6m]
[训练 30m ][测试 6m]
[训练 36m ][测试 6m]
滚动假设市场结构会变(近期数据更相关);锚定假设结构稳定(数据越多越好)。⭐ 这个选择本身是一个假设,应该写下来,而不是默认。
三个参数与一个约束
| 参数 | 怎么定 |
|---|---|
| 训练窗 | 至少要装得下第四节算出的最小样本量 |
| 测试窗 | 短到能捕捉结构变化,长到有统计意义 |
| 步长 | 通常 = 测试窗(不重叠) |
⚠️ 约束:段数太少,步进本身就没有统计意义。 3 段测试窗给你 3 个数——从 3 个数里看不出一致性。 起码要 10 段以上才值得读。
⭐ 真正该看的不是总收益
把所有测试段拼起来算一条总净值,是最常见的用法,也是浪费。步进真正的产出是段与段之间的一致性:
| 现象 | 含义 |
|---|---|
| 各段都小赚 | 🟢 最好的结果 |
| 少数几段贡献了全部收益 | ⚠️ 可能是几次运气,不是系统 |
| 每段选出的最优参数差异巨大 | ❌ 参数没有稳定含义 ⇒ 拟合噪声 |
最后一行是步进最有价值的输出,而它在「总收益」那条曲线上完全看不见。
随机 K 折在时序数据上是错的
顺带说明一个在机器学习里极常见的错误:随机 K 折交叉验证是几乎所有教程的默认选项,但在时间序列上——
它等于用未来训练、用过去测试。
时序数据上正确的做法就是本节的步进检验(第 47 篇第六节还列了另外两种 ML 特有的前视偏差)。
步进也会被过拟合
如果你调整训练窗长度、测试窗长度、或者滚动/锚定的选择,直到步进结果好看为止——那么步进就变成了样本内。
判断标准很简单:你在看到步进结果之后,改过步进的设置吗? 改过,它就不再是样本外检验了。
七、⭐ 多重比较:把「试了多少次」变成数字
第 7 篇点名了紧缩夏普比率,这里给出它背后的直觉和一张能吓住人的表。
你试的次数远比你以为的多
「我只测了三个策略」——通常不是真的。下面每一项都算一次试验:
参数网格 20 × 20 × 5 = 2,000 次
在 5 个品种上分别看 ×5
换了 3 个周期 ×3
加了 / 去掉了一个过滤器 ×2
这就已经是 60,000 次了。 而且还没算你在看图时凭直觉排除掉的那些方向——⚠️ 那些也算,因为它们同样消耗了这份数据。
纯运气能给出多高的夏普
假设每一个策略的真实期望收益都是零,在 T 年数据上估计出的夏普标准误约 1/√T。试 N 次,取最好的那个:
以 5 年数据(标准误 ≈ 0.45)为例:
| 试过的次数 N | 最好那个的夏普(期望) |
|---|---|
| 10 | 0.70 |
| 100 | 1.13 |
| 1,000 | 1.46 |
| 10,000 | 1.73 |
⭐ 在 5 年数据上试 1000 个参数组合,最好的那个夏普 1.46——而它的真实期望收益是零。
⚠️ 而 1000 个组合,就是上面那个 20×20×5 网格的一半。一次常规的参数扫描,就足以凭空造出一个看起来很专业的夏普。
怎么办
- ⭐ 记一个尝试计数器——在开始之前建一个文件,每跑一次记一行。事后回忆一定低估
- 把候选数量压在前三关(成本、样本量、可实现性都不消耗数据)
- 报告的时候带上 N:「夏普 1.5」和「试了 2000 次之后最好的夏普 1.5」是两句完全不同的话
- 紧缩夏普比率就是把上表那个基准从你的观察值里扣掉——它的含义就是「减去运气能达到的水平」
这条和第 32 篇第六节的过滤器判据是同一个东西的两面:那里说加过滤器要付 √(N原/N后) 的代价,这里说多试一次要付 √(2 ln N) 的代价。两处都是「你动手了,就要付费」。
八、⭐ 三个策略往往不是三个策略
这是全手册之前完全没有出现过的一块,而它的后果很大。
假设你有 10 个策略,每个夏普 0.5。组合起来夏普是多少?
如果它们互相独立:0.5 × √10 = 1.58。
但它们通常不独立。有效独立策略数:
N_有效 = N ÷ (1 + (N−1) × ρ) ρ = 平均两两相关系数
| 平均相关 ρ | 10 个策略的有效数 | 组合夏普 |
|---|---|---|
| 0.0 | 10.0 | 1.58 |
| 0.3 | 2.70 | 0.82 |
| 0.5 | 1.82 | 0.67 |
| 0.7 | 1.37 | 0.59 |
| 0.9 | 1.10 | 0.52 |
⭐ 平均相关 0.5 时,10 个策略的分散效果只相当于 1.8 个。
⚠️ 而这正是第 27 篇那个结论的量化版:
- 「均线金叉 + MACD 金叉 + RSI 上穿」——三个信号,
ρ接近 1,N_有效 ≈ 1 - 「多周期共振」——同一份数据看两遍,
ρ接近 1 - ⭐ 只用收盘价的信号,彼此在信息上等价,所以它们的 ρ 天然就高
具体该做什么
① 把每个策略的日收益序列拉出来
② 算两两相关系数矩阵
③ 算 ρ 均值,代入上面的公式
④ ρ > 0.7 的一对:⚠️ 当成一个策略看待
顺序上这一关排在多重比较之后,因为它常常把「我有 10 个策略」变成「我其实有 2 个」——而那会反过来改变第七节里 N 的含义。
九、⚠️ 回测引擎本身是否正确(工程问题,不是统计问题)
前面八节全部建立在一个前提上:回测算出来的数是对的。这个前提经常不成立,而且失败是静默的——引擎不会报错,它只会给你一个偏乐观的数字。
检查清单
| 项 | 典型错误 |
|---|---|
| K 线内顺序 | 同一根既触止损又创新高,默认假设几乎总是偏乐观(第 30 篇) |
| 成交假设 | 用收盘价成交,但信号是收盘后才算出来的(第 32 篇前视偏差) |
| 复权 | 美股分红拆股;用未复权价算收益率会凭空造出跳空 |
| point-in-time 标的池 | 用今天的列表回测过去(第 26 篇) |
| 时区与时间戳 | 交易所时间 / UTC / 本地时间混用,误差恰好一根 K 线 |
| 资金占用 | 同时开 10 个仓的回测,没检查过账户是否真的够保证金 |
| 数据修正 | 宏观数据、财报都会被事后修正,历史库里存的常是修正后的值 |
⭐ 一个能抓住大部分引擎 bug 的自检
跑一个必然亏损的策略。
规则:随机进场、随机出场,频次和你的真实策略一样
预期结果:净收益 ≈ −(往返成本 × 交易次数)
如果引擎显示它接近零或者赚钱,引擎有 bug。 最常见的两个原因就是成本没算进去、或者存在前视偏差。
⭐ 这个测试的价值在于它有一个已知答案。 你所有其他的回测都没有已知答案——所以它们无法验证引擎,只有这个可以。
十、实盘偏离:最后一道,也是唯一真实的一道
第 7 篇第五步说要小规模实盘并「测量回测与现实的差距」。这里给指标。
每一笔都记两个价:
信号触发时的可见价(回测会用的那个) → P_理论
实际成交均价 → P_实际
单笔偏离 = |P_实际 − P_理论| ÷ P_理论
累计之后和回测里假设的成本比:
实际往返成本 ÷ 回测假设成本 = 偏离倍数
| 偏离倍数 | |
|---|---|
| < 1.2 | 🟢 成本模型可用 |
| 1.2 – 2.0 | ⚠️ 回到第三节重算成本关——很多策略在这里就死了 |
| > 2.0 | ❌ 停止,成本模型是错的 |
⭐ 停止规则要在开始实盘之前写下来,理由和第 7 篇第四步一样:事后你总能给偏离找到解释。
⚠️ 而且偏离会随规模增长——你用 1/20 的仓位测出来的滑点,不能线性外推到全仓(第 15 篇第五节:持久冲击约按 √(Q/V) 走,拆单的收益是递减的)。
十一、能回答和不能回答什么
| 问题 | |
|---|---|
| 这个想法值不值得写回测代码 | ✅ 前三关,纯算术,几分钟 |
| 我的夏普有多不确定 | ✅ 分块 bootstrap 给区间 |
| 仓位该按多大的回撤来定 | ✅ MDD 分布的高分位,不是回测那一个 |
| 我试了这么多次,多少是运气 | ✅ 第七节的表直接查 |
| 我这 10 个策略真的分散吗 | ✅ 相关矩阵 + 有效独立数 |
| 我的回测引擎有没有 bug | ✅ 跑随机策略,它有已知答案 |
| 这个策略未来能赚多少 | ❌ 检验只能否决,不能承诺 |
| 样本外失败了,能不能改改再试 | ❌ 能,但那份数据已经作废了 |
| 回测夏普 2.0 是不是很好 | ❌ 不知道 N 就无法回答 |
| 市场结构变了怎么办 | ❌ 所有方法都假设同分布,这一条修不了 |
十二、这一篇的结论
- ⭐ 回测的输出不是一个数,是一次抽样。 「夏普 1.2」没有信息量,「夏普 1.2,95% 区间 [0.3, 2.1]」才有。整篇方法论就是把点估计换成分布。
- ⭐ 顺序比清单重要:前三关(可实现性、成本、样本量)全是纯算术、不消耗数据、淘汰率最高。在它们之前写回测代码,是把最贵的资源花在最先该被否决的东西上。
- ⚠️ 每一关必须是布尔判据。 用「样本内表现好」去抵消「成本关不过」,是拿一个估计量抵消一个确定量——方向就错了。
- 成本会放大样本量需求,而且是平方关系:成本吃掉一半毛收益 ⇒ 所需样本量 ×4;吃掉三分之二 ⇒ ×9。⚠️ 短周期因此双重受罚:既更难赚,也更难证明自己赚了。
- 分块 bootstrap 是必需的,不是可选的——普通 bootstrap 会毁掉波动率聚集。而块长本身是参数:换个块长结论就变,说明结论依赖块长。
- 最有实操价值的一个输出是最大回撤的分布:回测里那一个是在你选参数的那条路径上取的,系统性偏小。仓位该按分布的高分位定。
- 步进真正的产出不是总收益,是各段最优参数的一致性。 每段选出的参数差异巨大 ⇒ 参数没有稳定含义 ⇒ 拟合的是噪声。而看到步进结果之后再改步进设置,它就不再是样本外了。
- 一次常规参数扫描就足以凭空造出一个专业级夏普:5 年数据、1000 次试验,纯运气的最好夏普是 1.46。所以「夏普 1.5」和「试了 2000 次之后的夏普 1.5」是两句完全不同的话——报告时必须带上 N。
- 三个策略往往不是三个策略:平均相关 0.5 时,10 个策略的有效独立数只有 1.8。而只用收盘价的信号,
ρ天然就高——这是第 27 篇「同一个观测数了三遍」的量化版。 - 回测引擎该用一个有已知答案的测试来验:跑随机进出、频次相同的策略,结果必须约等于「−成本×次数」。显示接近零或赚钱 ⇒ 引擎有 bug。你其他所有回测都没有已知答案,所以只有这一个能验证引擎。
- 实盘偏离要有停止规则,且写在开始之前。 实际成本超过回测假设 2 倍 ⇒ 停。而且偏离随规模增长,小仓位测出的滑点不能线性外推。
- 最后一条,也是最容易被忘的:这一整套只能否决,不能承诺。全部通过的策略,仍然可能在实盘失败——检验的作用是让你少做几件蠢事,不是给你一张保单。