唯一的工具是 rolling walk-forward,而它验的不是一组参数。
你在教科书上、甚至在我们自己的〈回测是什么?回测报告怎么看〉里都读过同一句话:不要在全部数据上优化,至少留 20–30% 做样本外验证。然后你打开工作页,把每个分页都点过一遍,找不到那个比例设定。
没找到是正常的,产品没有这个功能。样本外这件事在 Blave 上只有一个实作:第五分页的 rolling walk-forward,而它填的是天数、验的东西也跟简单切分不太一样。这篇讲清楚差在哪、那个分页上的每个数字怎么读。
〈如何避免策略过拟合〉里有一张表,把三种做法标了等级:全数据优化(无效)、前 70% 训练后 30% 测试(有效但有限)、滚动窗口重选(最真实)。那张表是概念层的判断;产品层的事实是——只有第三种有按钮。
| 做法 | 概念上 | 在 Blave 上 |
|---|---|---|
| 用全部历史优化,回报全部历史 | 样本内,必然乐观 | 回测分页本来就是在整段历史上算成绩,所以才需要另外验 |
| 切一刀:前段训练、后段测试 | 有效但只能用一次 | 没有这个功能,找不到比例设定 |
| Rolling walk-forward:每轮重选、测下一段 | 最接近实盘的做法 | 工作页第五分页「样本外验证」 |
差别不只是界面。简单切分验的是「这一组参数在后段数据上好不好」;rolling walk-forward 验的是「用历史挑参数」这个做法本身在这支策略上还站不站得住——它把「用一段历史挑参数」这个动作重复做了 n 次,每次都用没看过的下一段来记分。
一句话:在每一个训练窗里重新挑一次参数,套到紧接着的测试窗;把所有测试窗接成一条。
| 轮 | 训练窗 | 测试窗 |
|---|---|---|
| 1 | 2022-01-05 → 2024-12-30 | 2024-12-31 → 2025-01-29 |
| 2 | 2022-02-04 → 2025-01-29 | 2025-01-30 → 2025-02-28 |
| 3 | 2022-03-06 → 2025-02-28 | 2025-03-01 → 2025-03-30 |
| … | (中间各轮同样每次往前挪一个测试窗,共 20 轮) | |
| 20 | 2023-07-29 → 2026-07-23 | 2026-07-24 → 2026-08-22 |
测试窗首尾相接到日:轮 1 停在 2025-01-29、轮 2 从 2025-01-30 接上去,中间没有空隙也没有重叠——20 段接起来就是那条样本外序列。
训练窗则是大幅重叠:三年的窗每一轮只往前挪 30 天,所以任两轮的训练期几乎是同一段数据。重叠的东西拼不成一条连续的线——这就是为什么图上只有样本外那一条。
实作上它跑两趟:第一趟把每一组候选参数的信号在全历史上只算一次,切出各轮训练片段定价;第二趟只重算各轮的胜出组合,把测试片段串成一条、用一次定价算完整段。所以它比「跑 n 次完整扫描」快得多——下面那个 20 轮、4.7 年小时线的例子,2026-09-17 那次执行 3.5 秒跑完(界面确认框给的是通则:「通常两分钟左右跑完,结果会出现在这个分页。」)。
Rolling 是训练窗每一轮整个往前滑;anchored 是起点固定、训练窗越滚越长。产品只提供 rolling,anchored 刻意不提供。 你跟 agent 要 anchored,它会回一句固定的话就照跑 rolling:
也不用想着绕过去:跑 walk-forward 的那支 lib 是唯读的,常驻程序会直接拒绝编辑;而且结果档里没有字段可以标记「这是 anchored 跑的」,真的改了,画面还是会照 rolling 显示。
分页上只有两个输入框:训练窗与测试窗,单位都是天,旁边即时显示会切出「共 n 轮」。两个都不填,就走这张默认表:
| 数据长度(天) | 训练窗(天) | 测试窗(多久重选一次,天) |
|---|---|---|
| ≥ 1185 | 1095(三年) | 30 |
| 455–1184 | 365(一年) | 30 |
| < 455 | 测试窗的 4 倍 | 总天数 ÷ 12,最少 30 |
三年那一阶是有理由的:它比较接近「用户从整段回测里挑一组固定参数」的真实做法。中等长度的历史若直接套比例公式,训练窗会短于一年,所以中间补了 365/30 这一阶。
两个硬上下限会直接挡下来,不给半调子的结果:少于 3 轮不跑、也不写结果档(「数据只够 {n} 轮,至少要 3 轮才看得出样本外表现」);超过 1000 轮一样挡(「把测试窗调大」)。末尾不足一轮的数据不会硬凑,图下方会注明「末尾 {d} 天不足一轮,未纳入」。
结论卡只看一个数字:
WFE = 样本外 Sharpe ÷ 样本内平均 Sharpe
分子是拼接后那一整段样本外序列的 Sharpe;分母是各轮训练窗里选中那一格自己的 Sharpe 的平均。四档标签照这个分界(都是「小于」,所以 0.5 落在「刚过门槛」、0.7 落在「高于门槛」):
| WFE | 结论卡显示 |
|---|---|
| 算不出来 | 样本外效率:无法判断 |
| < 0.2 | 样本外效率:远低于门槛 |
| < 0.5 | 样本外效率:低于门槛 |
| < 0.7 | 样本外效率:刚过门槛 |
| ≥ 0.7 | 样本外效率:高于门槛 |
而那个数字旁边的说明,请照字面读:
一个很小或负的分母会把整个诊断倒过来:一支样本内从来就不强的策略,反而会因为「没吐回什么」而拿到很高的「效率」。所以有两道防线:
教学范例策略 btc_sma_cross(BTCUSDT 1h,2022-01-01 起、共 1,720 天,SMA 45/100,参数一字未改)。数据落在第一阶,默认窗 1095/30,切出 20 轮(测试窗合计 600 天),末尾 25 天不足一轮、未纳入。
| 项目 | 样本外(测试窗接起来,600 天) | 样本内(20 轮训练窗各自算完取平均) |
|---|---|---|
| Sharpe | −0.45 | 1.3471 |
| 年化报酬 | −16.55% | +50.16% |
| 最大回撤 | −46.57% | — |
| 交易笔数 | 183 | — |
样本外效率 WFE = −0.45 ÷ 1.3471 = −0.334 → 远低于门槛
三件事值得指出。第一,没有任何一轮被排除——20 轮的训练窗 Sharpe 全都大于 0,分母是完整的 20 轮平均、不是挑剩的。
第二,分母 1.3471 远高于 0.25,第二道防线没启动,除法有效:「WFE 是负的」和「WFE 无法判断」是两回事,前者是算出来的结果,后者是拒绝作答。
第三,那两栏涵盖的期间本来就不同——样本内是 20 个互相重叠的训练窗的平均,样本外是其后那 600 天,所以不是干净的同期对照;WFE 要量的就是这个落差本身。
同一天、同一份码,这支策略的 MCPT p 值是 0.0160(n = 2000),stdout 自己印 significant edge at 95%——照上架关卡的判准是过的。
| 检验 | 它问的问题 | 结果 |
|---|---|---|
| MCPT | 在这组固定的部位序列上,这个成绩分不分得出实力与运气? | p = 0.0160 → 过 |
| Walk-forward | 照历史挑参数这件事本身,到下一段还管不管用? | WFE = −0.334 → 远低于门槛 |
两个都没算错,因为它们回答的不是同一个问题。这也是为什么 p 值再漂亮都不能当成样本外验过了(见〈MCPT p 值怎么看〉)。顺带说清楚:这不是在说这支策略坏掉了——它是 workspace 附的教学范例,这里用它示范的是两个检验的语意差异。
每一轮选参数用的是 3×3 邻域平均(跟参数扫描同一条规则,也就是〈如何避免策略过拟合〉讲的挑高原不挑峰值),但轮表上回报的训练 Sharpe 是选中那一格自己的数字。这是刻意的:
| 分母用什么 | 后果 |
|---|---|
| 该格自己的 Sharpe(现行) | 分母偏高,WFE 偏保守 |
| 3×3 邻域平均 | 邻域平均在结构上必然低于被它包住的那一格 → 分母被压小 → WFE 灌水,整个分页读起来过度乐观 |
分母灌水的诊断不如不做。
两轮交界的地方,序列带的是新那一轮自己的部位——是被剪开,不是归零。强制每个接缝平仓等于凭空捏造 n−1 笔没人交易过的出场。
换参数的成本不会被漏掉:接缝处权重改变,定价时照收手续费。也因为每一轮的数字都是从同一条拼好的序列切出来的、不重新定价,轮表加总就等于整段总数。
信号在全历史上算(rolling 指标才准),但每个训练片段各自跳过自己开头的预热根数——不是只有第一轮。没有这一步,第一轮会在指标还没热的 K 线上训练、挑到噪声,各轮的有效长度也不一样、不可比。上面那张轮表就是实证:策略的 START 写的是 2022-01-01,但轮 1 的训练窗从 2022-01-05 才开始——差的那几天正是 WARMUP = 100 根小时线(约 4.2 天)被扣掉。
测试窗不裁切,因为它已经热了,而且必须连续才能拼接。顺带回答一个进阶读者常问的问题:这里不需要 embargo 隔离期——测试窗就紧贴在挑它参数的训练窗后面,它之后的数据完全没被用到。
跑完之后没有「套用建议参数」这个按钮,agent 也不准因为这个结果去改你的策略码。空状态就把话讲在前面了:
为什么不能抄?看那次执行的参数漂移。20 轮只选出 4 组不同参数:
(45,100) ×14 → (55,80) → (45,100) → (45,80) ×3 → (35,80)
前 14 轮都停在同一格。就算「大部分时候都选 45/100」看起来像个结论,那条 −0.45 的样本外曲线也不是 45/100 的成绩——它是 4 组参数接力跑出来的一整段,不属于其中任何一组,所以没有一组可以被「采用」。你要求套用时,agent 会回固定的一句:
参数漂移网格会画出每一轮选中哪一格、被选中几轮,虚线框标出你目前的参数,图例也明写「最后一轮选中的参数只是参考,不是建议值」——那次执行最后一轮选的是 35/80,只代表「最后那一轮的训练窗刚好挑到这一格」。另外,这个分页不显示 p 值、也永不跑 MCPT;跑一次 walk-forward 在 agent 的工作守则里算一次迭代。
轮数够不够? 少于 3 轮根本不会跑。轮数太少时,WFE 只是一两个测试窗的运气。
有没有轮被排除? 每轮明细会注明哪几轮训练窗 Sharpe ≤ 0。上面那次一轮都没被排除;被排除的轮越多,代表这支策略连在自己的训练窗里都常常挑不到东西。
WFE 是「无法判断」还是一个负数? 不一样。「无法判断」代表样本内平均低于 0.25、这个比值没有意义;负数代表除法做了、样本外那段是亏的。
参数漂移得多凶? 看「{n} 轮选出 {u} 组不同参数」那一行。上面那次是 20 轮 4 组;每轮都跳到完全不同的格子,等于每次重选都在追不同的东西。
你是不是在找建议参数? 没有。要换参数走参数扫描的稳健点,不是抄最后一轮。
跑过一次之后,如果 WFE 掉得很难看,问题通常不在样本外这一步,而在挑参数那一步——那是〈如何避免策略过拟合〉的范围。如果你手上还没有值得验的策略,策略库里的官方策略每一支都附真实回测与品质关卡结果,可以先看数字再决定:策略库。