# 第 6 章：别高兴太早：防止过拟合

> 最新书稿已更新至 [XQuant 量化课堂页](https://xquant.shop/courses)。
> 想阅读最新版官方书稿，请前往图书页。

第 5 章我们给策略做了一次全面体检：基准对比、逐年拆解、收益分布、参数敏感性。现在你手上有一套完整的评估方法。

但所有体检结果都基于一个前提：**回测结果是真实的。**

真的吗？

回顾第 5 章的发现：TopNRanking 策略在 2021 年亏了 10%，换遍所有动量窗口都救不回来。这是动量策略的结构性弱点。但同样的策略在 2023-2025 年表现不错。这引出一个危险的念头：**如果我们在一段有利的市场里精心挑选参数，找到“收益最好”的组合，信心满满地投入资金，未来市场环境变了会怎么样？**

回测可以调参数让收益翻倍。这是真的好，还是自欺欺人？

### 路线图

本章仍然属于四个阶段里的**评估归因**阶段。第 5 章训练的是“看”：看指标、看年份、看分布、看参数敏感性。第 6 章训练的是“疑”：怀疑漂亮回测是否真实，检查它有没有把历史数据里的偶然性当成规律。

第 1 章你已经亲眼看过一次过拟合：参数扫描里的“冠军”，到了样本外反而掉到水下。第 5 章又留下两个问题：回测能调参数让收益翻倍，这是真的好，还是自欺欺人？如果你在 2023 年跑回测发现某个策略 2021-2023 年表现好，怎么知道它 2024 年还会好？

本章就回答这两个问题。全章一共做 4 个实验，路线如表 6-1 所示。

**表 6-1 第 6 章路线图**

| 节 | 内容 | 实验 |
|------|------|------|
| 6.1 | 能不能找到收益最好的参数？ | 1 |
| 6.2 | 市场在变，参数也该跟着变吧？ | 1 |
| 6.3 | 一条路径够可靠吗？ | 1 |
| 6.4 | 规则越多回测越好，是好事吗？ | 1 |

铁律不变：**先猜后验，数据说了算。**

---

## 6.1 能不能找到收益最好的参数？

第 1 章末尾你扫过 24 个一维参数，也就是均线窗口，并用 60/40 切分做过样本外测试。那一次你已经看见过拟合的样子：历史上最好看的参数，到了没见过的数据里并不好。

这一章接着上一次往前走。同样是切分数据，但难度升了一档：参数从 1 维增加到 4 维，包含动量窗口、波动率窗口、再平衡频率、止损阈值；组合数从 24 个增加到约 490 个。维度多了以后，过拟合会变成什么样？这一节给你答案。

第 5 章用了默认参数：动量窗口 20 天、波动率窗口 20 天、再平衡频率 10 天。但这些不是天经地义的。

直觉告诉我们：**多试几组参数，选收益最高的就行了。** 几百种组合里总有比默认值更好的吧？

这个想法太自然了。就像买手机：把市面上所有型号的参数列出来，逐项比较，选评分最高的那款。参数也一样，把所有组合都试一遍，选出表现最好的。这叫**参数优化**，也叫**网格搜索（Grid Search）**。一个参数组合就像一个格子，挨个格子试一遍，总能找到表现最好的组合。

真的这么简单吗？试试看。

### 动手实验 1：参数优化与样本内 / 样本外

这个实验用到 open-xquant 的参数优化工具：`GridSearch`（网格搜索，把所有参数组合都试一遍）和 `WalkForward`（前推分析，模拟“边走边调”的真实场景）。

我们一起把这份 spec 写出来。这次重点看三件新东西：**维度跃迁**（从一维到四维参数）、**时段语义切分**（有利市场挑参数 / 反复波动环境验证）、**业务约束的可读字符串写法**。

#### 第一、二段：上下文和任务描述

第 1 章 1.5 节用一行“前 60% / 后 40%”切了一刀。这是按比例切分。这一节不一样：每段时间要带市场含义。

> **上下文**：本章讨论参数优化和过拟合。第 5 章看过默认参数（动量 20 / 波动率 20 / 再平衡 10）的高原型和山峰型，但读者还没有试过“从一堆参数里找最好的”。承接第 5 章的叙事：如果 TopNRanking 在 2017-2020 年走势较顺的市场里选出好参数，到了 2021 年之后的反复波动环境会怎么样？
>
> **任务描述**：在 `q6-avoid-overfitting.ipynb` 中用 `oxq.optimize.GridSearch` 跑参数网格搜索，用样本内 / 样本外方法检验过拟合。

> 📌 **要点**：数据切分要带“市场含义”。第 1 章的 60/40 是按比例切的，这里换成“2017-2020 年走势较顺 / 2021-2023 年反复波动”。后者把每段对应的市场环境说清楚，读者才能看懂“为什么是这两段”。任何切分型 spec，起止日期都要四个全锁死（IS_START / IS_END / OOS_START / OOS_END），否则今年跑、明年跑切出来不一样，过拟合证据就站不住。

#### 第三段：任务要求

第 1 章扫过 24 个一维参数，也就是均线窗口。这一节直接跳到 4 维：动量窗口、波动率窗口、再平衡频率、止损阈值。

> **任务要求**：
>
> 1. 数据切割：样本内 `2017-01-01 ~ 2020-12-31`、样本外 `2021-01-01 ~ 2023-01-31`，四个端点全锁死
> 2. 用 `ParameterSet` 定义参数空间：动量 / 波动率窗口 `[10, 15, 20, 25, 30]`、再平衡频率 `[5, 10, 15, 21]`、止损阈值 `[0.03, 0.05, 0.07, 0.10, 0.15, 0.20]`
> 3. 分两组跑：无止损组（3 维）+ 含止损组（4 维），约束后总组合数约 490
> 4. 业务约束用可读字符串写：`add_constraint("RebalanceFrequencyRule.interval_days <= mom.period")`、`<= vol.period`
> 5. 再平衡频率不应超过信号窗口，这是领域知识，不是技术约束
> 6. 用 `GridSearch` 跑两组参数空间，在样本内、样本外各跑一次

> 📌 **要点**：维度增加后要分组管理。一维扫描可以直接扫，四维直接扫会膨胀到 5×5×4×6 = 600 个组合，还要再做约束过滤。分两组跑（无止损 70 组合 + 含止损 420 组合），既能控制总数，又能让止损这一维在结果里被单独看清，也为后面“规则负担”那一节埋伏笔。

> 📌 **要点**：业务约束用可读字符串，不要用 lambda。`"RebalanceFrequencyRule.interval_days <= mom.period"` 这种 DSL 字符串比 `lambda p: p["RebalanceFrequencyRule.interval_days"] <= p["mom.period"]` 更适合零基础读者看。约束的“为什么”要写到 spec 注释里：信号 20 天更新一次，再平衡 60 天才看一次，等于在用过期信号做决策。这是把领域知识嵌入 spec 的标准写法。

#### 第四段：验收标准

> **验收标准**：
>
> 1. 打印 Top 20 表（按样本内简化夏普排名）+ 样本外对比 + 全局相关系数 + 衰减分布
> 2. 散点图（样本内与样本外简化夏普，y=x 对角线虚线，Top 20 红色高亮）
> 3. Top 20 柱状图（每组双柱：样本内 / 样本外）
> 4. 总结打印按实际数据动态描述：Top 20 平均衰减、对角线下方点的占比、过拟合定义

完整示例 spec 在配套仓库的 [`q6-avoid-overfitting/specs/spec-01-parameter-optimization.md`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/specs/spec-01-parameter-optimization.md)。参考它确认自己的 spec 后，再复制给 AI，按提示允许执行。

AI 执行完毕后，你的 notebook 里应该出现网格搜索的完整结果。我们来看看代码做了什么。

数据被切成了两段：**样本内（In-Sample）** 用 2017-2020 年做优化，**样本外（Out-of-Sample）** 用 2021-2023 年做验证。为什么选这么早的数据？因为样本内需要足够长，至少 3-4 年，才能覆盖不同市场环境，避免只在单一市场走势下调参。时间顺序也必须正确：先优化，后验证。就像真实投资中你只能用历史数据做决策，然后面对未来。

**样本内**就是你看着答案复习的模拟考试卷；**样本外**就是真正的考试，是你从没见过的题目。

四个维度的参数组合：动量窗口 5 种 × 波动率窗口 5 种 × 再平衡频率 4 种 × 止损阈值（有/无），加上约束过滤（再平衡频率不能超过信号窗口，因为用过期信息做决策没有意义），最终产生 490 种有效组合。

`GridSearch` 把 490 种参数组合逐个跑回测，按简化夏普排名。然后用同样的参数在样本外再跑一遍，看“真正的考试”成绩如何。

### 运行结果

490 种组合全部跑完，先看样本内 Top 20 的“样本外真相”。样本内简化夏普 Top 20 及其样本外表现（部分）如表 6-2 所示。

**表 6-2 样本内简化夏普 Top 20 及其样本外表现（部分）**

| # | mom | vol | freq | sl | 样本内简化夏普 | 样本外简化夏普 | 衰减 |
|---|-----|-----|------|----|-----------|-----------|------|
| 1 | 15 | 10 | 10 | 0.03 | 1.39 | 0.50 | -0.88 |
| 2 | 15 | 15 | 10 | 0.03 | 1.34 | 0.49 | -0.85 |
| 3 | 15 | 25 | 10 | 0.03 | 1.30 | 0.38 | -0.92 |
| 4 | 15 | 20 | 10 | 0.03 | 1.29 | 0.39 | -0.90 |
| 5 | 30 | 10 | 5 | 0.03 | 1.25 | 0.02 | -1.23 |
| 7 | 30 | 10 | 10 | 0.03 | 1.24 | -0.07 | -1.31 |
| 14 | 15 | 10 | 10 | 无 | 1.18 | 0.01 | -1.17 |

**样本内 Top 20 统计：** 样本内简化夏普均值 1.22（范围 1.15 ~ 1.39），样本外简化夏普均值 0.20（范围 -0.09 ~ 0.50），平均衰减 -1.02。

**全部 490 个组合统计：** 样本内 / 样本外简化夏普相关系数 0.04，样本外比样本内差的比例 100%，简化夏普衰减均值 -0.82、中位数 -0.88。

散点图与 Top 20 柱状图如图 6-1 所示。

![490 个参数组合：样本内与样本外简化夏普](images/q6-avoid-overfitting/01-is-vs-oos-scatter-and-top20.png)

### 结果解读

第一个认知升级：**参数优化是一把双刃剑。**

先看数字。样本内 Top 20 的平均简化夏普 1.22。这在本书的简化口径下已经很好看。但拿到样本外（2021-2023 年，没见过的数据），平均简化夏普跌到了 0.20。衰减了 83%。

再看散点图。每个点代表一种参数组合，对角虚线是“样本外 = 样本内”的理想状态。如果所有点都在对角线上，说明样本内的好成绩在样本外也能兑现。但现实是：**100% 的点都在对角线以下**。没有一种参数组合的样本外表现达到样本内水平。

更刺眼的是：样本内简化夏普和样本外简化夏普的相关系数只有 0.04，几乎没有关系。样本内排名第一的组合，样本外并不一定最好。

这就是**过拟合（Overfitting）**：你的策略只会死读书，课后习题全都会做，结果高考考砸了。它“记住了”过去数据的特征（噪音），而不是发现了真正的规律。

柱状图更直观：蓝色柱子（样本内）高高在上，红色柱子（样本外）矮了一大截。有些组合样本外甚至为负，在“真正的考试”中亏了钱。

**数据说了算：参数优化后，必须在没见过的数据上验证。样本内的好成绩不算数。**

**这就是第一个检查工具：样本内 / 样本外切分。** 它不能直接帮你赚钱，但它能让你在第一时间看到“漂亮回测背后到底有没有真东西”。第 1 章那次“被自己骗”的经历，到这里变成了一项可以重复执行的检查。

怎么办？我们把数据切成了“优化”和“验证”两段，只验证了一次。但只验证一次够吗？市场在变，参数也该跟着变吧？

---

## 6.2 市场在变，参数也该跟着变吧？

第 1 个实验把数据切成了“优化”和“验证”两段，发现了过拟合问题。但我们只切了一次、只验证了一次。

市场每年都在变。2020 年选的参数，2023 年还适用吗？能不能模拟“每隔一段时间重新优化参数”的过程？

直觉告诉我们：**市场每年不一样，参数当然应该定期更新。** 用最近的数据重新优化，应该比死守一组参数更好。

这个想法叫 **Walk-forward（前推验证）分析**，也就是“边走边调”。在历史数据上模拟“每隔一段时间重新优化参数”的过程：用过去两年数据选参数，在接下来半年验证；然后窗口往前滑半年，再选参数、再验证，如此反复。

有两种滑动方式：

- **滚动式（Rolling）**：固定数据窗口宽度，窗口左边位置不断往右移动。好处是只看最近的市场，坏处是丢弃了更早的数据。就像只翻最近两年的课本复习。
- **锚定式（Anchored）**：固定数据窗口左边位置，窗口宽度不断拉长。好处是利用了所有历史数据，坏处是旧数据可能不再适用。就像从高一课本开始全部复习。

试试看。用两种不同的方式“边走边调”。

### 动手实验 2：Walk-forward 分析

我们一起把这份 spec 写出来。这次重点看两件新东西：**循环型 spec 的端点全锁**（窗口起止 + 长度 + 步长四个参数都不留浮动）、**对照实验式 spec 的并列结构**（让 Rolling 和 Anchored 自己跑出对比）。

#### 第一、二段：上下文和任务描述

> **上下文**：在 `q6-avoid-overfitting.ipynb` 中已有 490 个组合的网格搜索结果 + 样本内 / 样本外对比，过拟合现象已暴露。当前问题：数据只切了一次。市场在变，2023 年选的参数 2025 年还适用吗？能不能模拟“定期重新优化”的过程？
>
> **任务描述**：在 notebook 新建单元格，用 `oxq.optimize.WalkForward` 实现 Walk-forward 分析，对比 Rolling 和 Anchored 两种方式。

#### 第三段：任务要求

循环型 spec 比单次 spec 脆弱得多。单次 spec 漏掉一个日期可能没事，循环型 spec 漏掉就会让“折数”在不同时间跑出不同结果。

> **任务要求**：
>
> 1. 复用第 1 个实验的参数空间（无止损组 70 组合）和约束（`RebalanceFrequencyRule.interval_days <= mom.period` / `<= vol.period`）
> 2. 数据范围：`DATA_START="2017-01-01"` 到 `ANALYSIS_END="2026-05-06"`，四个端点全锁死，不要用 `pd.Timestamp.now()`
> 3. Rolling Walk-forward：训练窗口 `"2Y"`、测试窗口 `"6M"`、`anchored=False`
> 4. Anchored Walk-forward：同上，`anchored=True`（训练窗口从起点不断扩大）
> 5. `train_period="2Y"` 的依据：覆盖一个完整中期周期；`test_period="6M"` 的依据：足够长以观察策略表现，又足够短以让多个折落进数据范围

> 📌 **要点**：循环型 spec 的窗口端点要四个全锁死：起始日、结束日、窗口长度、步长。漏掉任何一个，折数都会随时间漂移。如果用 `pd.Timestamp.now()` 当结束日，半年后跑出来折数从 15 变成 16，参数稳定性的结论可能就反过来了。任何带 `for` 循环的 spec 都要回头检查这四个端点。

> 📌 **要点**：循环窗口的“为什么”也要写。`"2Y" / "6M"` 不是随手挑的，背后有“覆盖一个完整中期周期”和“折数 ≥ 5 才有统计意义”两个判断。领域知识里的窗口长度选择，跟参数选择本身一样重要。把判断写进 spec 注释，别让 AI 揣摩。

#### 第四段：验收标准

> **验收标准**：
>
> 1. 用 `to_dataframe()` 打印每折详情：训练时段、验证时段、最优参数、样本内 / 样本外简化夏普
> 2. 汇总：`oos_sharpe_ratio()`、`deterioration()`、各折参数集合
> 3. 对比图（figsize 14×6，上下两栏）：上栏每折样本外简化夏普（Rolling 红 / Anchored 蓝两条线），下栏参数稳定性线图（mom.period / interval_days 各折取值）
> 4. 标题「Walk-forward 分析：Rolling 与 Anchored」
> 5. 打印分析按实际数据动态描述：Anchored 是否锁定参数、Rolling 是否乱跳、衰减对比

完整示例 spec 在配套仓库的 [`q6-avoid-overfitting/specs/spec-02-walk-forward.md`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/specs/spec-02-walk-forward.md)。参考它确认自己的 spec 后，再复制给 AI，按提示允许执行。

AI 执行完毕后，你的 notebook 里应该出现 Rolling 和 Anchored 两种 Walk-forward 的完整结果。

训练窗口 2 年，验证窗口 6 个月。每次用 2 年数据选出最优参数，在接下来 6 个月验证。然后窗口滑动，重复这个过程。最终得到 15 折结果，也就是 15 次“选参数、再验证”的记录。

### 运行结果

两种方式的对比如表 6-3 所示。

**表 6-3 Rolling 与 Anchored 对比**

| 方式 | 样本外简化夏普 | 衰减 | 唯一参数组合 |
|------|-----------|------|-------------|
| Rolling | 0.14 | -36.2% | 7/15 |
| Anchored | 0.13 | -23.5% | 5/15 |

Anchored 参数更稳定。

关键发现：Rolling 在 15 折（每次滑动算一折，一共滑了 15 次）中选了 7 种不同的参数组合，平均两折就换一种。而 Anchored 只有 5 种，且其中 7 折选了相同的参数（动量窗口 20、波动率窗口 15、再平衡频率 5），单一组合占了近一半折数。

Anchored 的逐折参数一览如表 6-4 所示。

**表 6-4 Anchored Walk-forward 逐折参数与样本外简化夏普**

| Fold | mom | vol | freq | 样本外简化夏普 |
|------|-----|-----|------|-----------|
| 1 | 15 | 15 | 5 | 2.50 |
| 2 | 15 | 15 | 5 | 0.71 |
| 3 | 20 | 15 | 5 | -0.13 |
| 4 | 15 | 10 | 5 | 0.68 |
| 5 | 30 | 15 | 5 | -2.49 |
| 6 | 20 | 15 | 5 | -1.83 |
| 7 | 20 | 15 | 5 | 1.42 |
| 8 | 20 | 15 | 5 | -0.67 |
| 9 | 20 | 15 | 5 | 1.91 |
| 10 | 20 | 15 | 5 | -1.75 |
| 11 | 30 | 15 | 5 | 1.99 |
| 12 | 20 | 20 | 5 | 1.22 |
| 13 | 30 | 15 | 5 | 1.23 |
| 14 | 20 | 15 | 5 | 1.31 |
| 15 | 30 | 15 | 5 | 0.98 |

（mom=20, vol=15, freq=5）这组参数前后出现了 7 次，是 Anchored 的最频繁选择。其余四种组合（15, 15, 5）、（30, 15, 5）、（15, 10, 5）、（20, 20, 5）各出现 1 到 4 次。换句话说：动量窗口在 15-30 之间换、波动率窗口大多停在 15、再平衡频率 15 折全选 5。Rolling 的波动更宽，动量窗口在 15-30 之间跳，波动率窗口从 10 一直到 30，没有明确偏好。

每折样本外简化夏普走势与参数稳定性对比如图 6-2 所示。

![Walk-forward 分析：Rolling 与 Anchored](images/q6-avoid-overfitting/02-walk-forward-rolling-vs-anchored.png)

### 结果解读

第二个认知升级：**参数稳定性比参数最优更重要。**

先看上图。两条实线是样本外简化夏普。Rolling（红色）和 Anchored（蓝色）都有起伏，但起伏的方向大体一致。下图更关键：Rolling 的参数线在 mom=15-30、vol=10-30 间跳来跳去，Anchored 的两条线被压在更窄的区间内（mom 多在 20、vol 多在 15）。

**参数稳定性（Parameter Stability）**：好的参数组合应该在不同时间窗口中保持一致。如果每换一个窗口参数就变，说明“最优参数”取决于你看哪段市场，这个参数可能只是在拟合噪音。

Anchored 在 15 折中 7 折都选了（mom=20, vol=15, freq=5）这一组，15 次里 7 次是同一个答案；剩下的折在四组里跳，但波动率窗口大多稳在 15、再平衡频率 15 折全是 5。不管训练集从 2 年扩大到 9 年，参数选择都被限制在一个窄区间，这是一个好信号。而 Rolling 在 15 折里选出 7 种不同组合，每两折就换一次，说明它在追逐短期市场特征，而不是捕捉长期规律。

样本内到样本外的衰减比例：Rolling 衰减 36.2%，Anchored 衰减 23.5%。Anchored 衰减更小，因为它用了更多数据训练，不容易被噪音骗。

Walk-forward 的核心价值：不是找到一个“永远最优”的参数，而是检验你的优化方法在历史上是否靠谱。如果历史上每次优化后验证都不错，你对下一次优化会更有信心。

**数据说了算：参数稳定 = 更可信。如果最优参数总在变，你怎么知道现在选的参数下个月还是最优？**

**这就是第二个检查工具：Walk-forward。** 它把“换段时间还稳定吗”从一种焦虑变成了一项可以滚动跑的检查。参数稳定的策略你可以继续研究，参数到处变的策略就要谨慎。

但 Walk-forward 只用了一种切法。如果数据切的方式不同，结论还一致吗？

---

## 6.3 一条路径够可靠吗？

Walk-forward 模拟了“边走边调”，但我们只用了一种切法。如果换一种切数据的方式，结论还一样吗？

做科学实验有个基本原则：**一次实验不够，要重复多次。** 只做一次实验就下结论，可能只是碰巧。同样的道理：只验证一次（第 1 个实验）或只用一种切法（第 2 个实验）都不够可靠。

直觉告诉我们：**好策略不管怎么切都好。** 如果 5 次验证里 4 次都选了同样的参数，比只验证 1 次可信得多。

这就是**交叉验证（Cross Validation）**：用多种数据切分方式验证同一个策略，看结论是否一致。

但金融数据有一个特殊的限制，先用一个生活类比说清楚：**做菜可以“先看明天的菜单再决定今天买什么菜”，但交易不行**，因为你今天根本不知道明天会发生什么。

把这个限制翻译成两个量化术语：

- **时间序列属性（Time-Series Nature）**：金融数据有先后顺序。今天比昨天晚发生，比明天早发生。顺序不能打乱。
- **时间穿越 / 数据泄露（Look-Ahead Bias / Data Leakage）**：在训练或验证时，不小心让模型“看到了未来”。比如本应该用 2020 年的数据训练，结果你混了一点 2024 年的数据进来。这等于让你的策略在考试前“偷看了答案”，回测会异常漂亮，但真实交易时可能亏钱。

所以做交叉验证时，**每折必须保持时间顺序**。这就是 **时间序列分割（Time-Series Split）**：训练集永远在测试集之前，绝不让“未来”出现在“过去”的训练里。

试试看。

### 动手实验 3：交叉验证

我们一起把这份 spec 写出来。这次重点看两件新东西：**时序切分而非随机切分**（金融数据的硬约束）、**接续型 spec 的减法**（不复述前面已经讲过的概念，专心做一件新事）。

#### 第一、二段：上下文和任务描述

> **上下文**：在 `q6-avoid-overfitting.ipynb` 中已有参数优化 + Walk-forward 分析。Walk-forward 只用了一种切法。如果换一种切法，结论还一样吗？一次验证和多次验证，可信度差多少？
>
> **任务描述**：在 notebook 新建单元格，用 `oxq.optimize.TimeSeriesCV` 实现时间序列交叉验证。

#### 第三段：任务要求

> **任务要求**：
>
> 1. 复用第 2 个实验的参数空间和约束（无止损组 70 组合）
> 2. 数据范围：`DATA_START="2017-01-01"` 到 `ANALYSIS_END="2026-05-06"`，与第 2 个实验同步锁死，保证 5 折切分点在任何时间跑都一致
> 3. 用 `oxq.optimize.TimeSeriesCV`（不是 sklearn 的 `KFold`）：`n_splits=5`、`expanding=True`
> 4. 传入 `paramset`，让 `cross_validate()` 自动在每折训练集上做 GridSearch
> 5. `n_splits=5` 的依据：ML 教科书默认值，7 年数据上每折约 1.4 年验证集，足够长
> 6. `expanding=True` 的依据：训练集逐折扩大。第 2 个实验的 Anchored 比 Rolling 更稳定，本步跟随该结论

> 📌 **要点**：金融数据要用时序切分，不能用随机切分。普通机器学习里常见的 `KFold` 可以随机切，但金融场景必须用时间序列切分：训练集永远在测试集之前，绝不让“未来”出现在“过去”的训练里。否则就是把答案偷给了考试中的你。回测看起来漂亮，真实交易时可能亏钱，这是这条硬约束最常见的代价。

> 📌 **要点**：接续型 spec 应该做减法。spec-03 比 spec-01 / spec-02 都短得多。它不重新定义参数空间，直接复用第 2 个实验的 `wf_paramset`；也不重讲一次什么是过拟合，只专心做“5 折结果对比”一件事。承接型 spec 应当克制重复。前面 spec 讲过的就别再讲，只引入本 spec 真正新的东西。

#### 第四段：验收标准

> **验收标准**：
>
> 1. 用 `cv_result.to_dataframe()` 打印汇总表，再 for 循环逐折打印（训练时段、验证时段、最优参数、样本内 / 样本外简化夏普、样本外收益）
> 2. 汇总：`mean_oos_metric()` ± `std_oos_metric()` + 范围 [min, max] + 唯一参数组合数
> 3. 柱状图（figsize 12×6）：每折样本内 / 样本外简化夏普双柱 + 样本外均值水平线
> 4. 标题「5 折交叉验证：每折的样本内与样本外简化夏普」
> 5. 打印分析按实际数据动态描述：5 折中几折选了相同参数、变异系数大小、与第 2 个实验 Anchored 结论是否一致

完整示例 spec 在配套仓库的 [`q6-avoid-overfitting/specs/spec-03-cross-validation.md`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/specs/spec-03-cross-validation.md)。参考它确认自己的 spec 后，再复制给 AI，按提示允许执行。

AI 执行完毕后，你的 notebook 里应该出现 5 折交叉验证的完整结果。

`TimeSeriesCV` 把数据切成 5 折。每折用前面的数据训练（训练集逐渐扩大），紧跟着的一段做验证。每折都独立做网格搜索，选出该折的最优参数，再到验证集上检验。

### 运行结果

5 折交叉验证的逐折详情如表 6-5 所示。

**表 6-5 5 折交叉验证逐折详情**

| Fold | 训练时段 | 验证时段 | 最优参数 | 样本内简化夏普 | 样本外简化夏普 | 样本外收益 |
|------|----------|----------|----------|-----------|-----------|-----------|
| 1 | 2017-01-01 ~ 2018-07-22 | 2018-07-23 ~ 2020-02-11 | mom=15, vol=15, freq=5 | 0.87 | 0.69 | 14.37% |
| 2 | 2017-01-01 ~ 2020-02-11 | 2020-02-12 ~ 2021-09-02 | mom=15, vol=10, freq=5 | 0.72 | 0.44 | 10.82% |
| 3 | 2017-01-01 ~ 2021-09-02 | 2021-09-03 ~ 2023-03-25 | mom=20, vol=15, freq=5 | 0.66 | 0.28 | 4.33% |
| 4 | 2017-01-01 ~ 2023-03-25 | 2023-03-26 ~ 2024-10-14 | mom=20, vol=15, freq=5 | 0.53 | 1.56 | 37.83% |
| 5 | 2017-01-01 ~ 2024-10-14 | 2024-10-15 ~ 2026-05-06 | mom=20, vol=20, freq=5 | 0.72 | 1.79 | 67.22% |

**TopNRanking CV 汇总：** 样本外简化夏普 0.95 ± 0.68，样本外范围 [0.28, 1.79]，唯一参数组合 4/5。

5 折中 Fold 3 与 Fold 4 选择了相同的参数（mom=20, vol=15, freq=5），其余三折各不相同。这组结果该怎么读？

每折样本内与样本外简化夏普对比如图 6-3 所示。

![5 折交叉验证：每折的样本内与样本外简化夏普](images/q6-avoid-overfitting/03-cross-validation-5fold.png)

### 结果解读

第三个认知升级：**多次验证比单次验证更可靠。**

先看参数一致性。5 折里只有 Fold 3 和 Fold 4 选了同一组参数（mom=20, vol=15, freq=5），其余三折各不相同。单看 5 折，共识不算强。但有意思的是：这组（mom=20, vol=15, freq=5）也正是第 2 个实验里 Anchored Walk-forward 在 15 折里出现 7 次的最频繁参数。两种独立的切法（一种 15 折滑动，一种 5 折时序切分）都把概率最高的位置指向了同一组参数。切法换了，结论没变。

再看表现波动。样本外简化夏普的范围从 0.28 到 1.79，标准差 0.68。变异系数（标准差 / 均值）约为 0.72，仍小于 1，说明样本外表现整体相对一致，但波动不容忽视：最好的折（1.79）比最差的折（0.28）高了 6 倍以上。参数对了不代表什么时候都有正收益，策略对市场环境仍然敏感。

交叉验证的价值不是给你一个“最优参数”，而是告诉你**你的参数选择有多可靠**。5 折下来如果共识强，可以更有信心；共识弱，就该接受“参数选择本身有不确定性”，而不是盯着某一折的结果。

**数据说了算：多次验证给出更立体的画像。共识强可以更进一步，共识弱就该承认不确定性。即使参数对了，也别期望每年都有正收益。**

**这就是第三个检查工具：交叉验证。** 它让你在多种切法下独立验证同一个结论。一次可能是巧合，多次都指向同一个答案就更可信。前两个检查工具告诉你“是不是过拟合”，这一个告诉你“你有多大把握”。

到这里，我们从三个角度检验了参数选择：一次验证（第 1 个实验）、动态验证（第 2 个实验）、多次验证（第 3 个实验）。但还有一种过拟合更隐蔽：不是参数选错了，而是**规则太多了**。

---

## 6.4 规则越多回测越好，是好事吗？

前三个实验检验的都是参数选择：找参数、更新参数、换切法验证参数。但还有一种过拟合更隐蔽。

回顾第 4 章：我们给策略加了止损、止盈、移动止损等交易规则。当时的结论是“不是所有规则都有用”。但那时我们没有做样本内 / 样本外验证。如果在样本内不断加规则、调参数，回测收益一路上升，这是真的在改进策略，还是只是在“凑”出好看的数字？

直觉告诉我们：**规则越多保护越全面，效果应该越好。** 止损防亏损、止盈锁利润、移动止损跟踪趋势，每一条都有道理。

真的吗？试试看。从最简单的策略开始，一步一步加规则，看样本内和样本外分别怎么变。

### 动手实验 4：规则负担

我们一起把这份 spec 写出来。这次重点看两件新东西：**规则数的定义**（“规则数”和“自由度”是两个不同的轴）、**业务约束链的金融语义**（止损 < 止盈 / 移动止损 ≥ 固定止损 / 回撤 ≥ 止损都是领域知识）。

#### 第一、二段：上下文和任务描述

> **上下文**：在 `q6-avoid-overfitting.ipynb` 中已有参数优化 + Walk-forward + 交叉验证。前三步检验“参数选得对不对”。本步检验另一种过拟合：规则太多。每加一条规则，就多了一个可调旋钮；旋钮越多，越容易在样本内“调出”好看的结果。
>
> **任务描述**：在 notebook 新建单元格，做两个堆叠实验展示规则负担。

#### 第三段：任务要求

把“加规则”和“加自由度”分两个实验跑，读者才能看出“是规则本身的问题，还是自由度的问题”。

> **任务要求**：
>
> 1. 编写 `run_layer(layer_name, rules, paramset, description)` 辅助函数，复用第 1 个实验的样本内 / 样本外切分
> 2. **4A 规则堆叠**（6 步）：基础策略 mom=20 / vol=20 / freq=10 固定，逐步添加交易规则
>    - Layer 0：基础策略（无额外规则）
>    - Layer 1：+ 优化再平衡频率
>    - Layer 2：+ 优化止损
>    - Layer 3：+ 优化止盈（约束 sl < tp）
>    - Layer 4：+ 优化移动止损（约束 trail >= sl）
>    - Layer 5：+ 最大回撤风控（约束 max_dd >= sl）
> 3. **4B 自由度堆叠**（5 配置）：从 1 自由度逐步加到 6 自由度
>    - A：仅动量窗口（1 自由度）
>    - B：+ 波动率（2）
>    - C：+ 再平衡频率（3）
>    - D：+ 止损阈值（4）
>    - E：+ 止盈 + 移动止损（6 自由度，粗网格 [10,20,30] 防止组合数爆炸）
> 4. 每一步在样本内选最优，再到样本外验证
> 5. 约束链按金融语义编码：止损 < 止盈、移动止损 ≥ 固定止损、回撤阈值 ≥ 止损

> 📌 **要点**：第 6 章第一次显式区分“规则数”和“自由度”两个轴。4A 数的是“加了几条交易规则”。止损是一条规则，即便它只有一个 threshold 参数。4B 数的是“有几个可优化的旋钮”。一条规则可能贡献 1 个或多个自由度。两个轴不一样，金融策略里必须分清楚：止损是一条独立业务规则，它在 4A 里占一步；但它的 threshold 在 4B 里只贡献 1 个自由度。读者看到两个实验的曲线分别出现拐点，才能理解问题不只来自规则数，也不只来自自由度，而是两者都会增加过拟合风险。

> 📌 **要点**：业务约束链要用金融语义，不是技术约束。`sl < tp` 表示止损不能高于止盈，否则止损永远先触发；`trail >= sl` 表示移动止损不能比固定止损更严，否则后者无意义；`max_dd >= sl` 表示组合整体风控阈值不应小于单个持仓的止损，否则单个持仓止损还没触发，组合整体就先停了。这三组约束本身就是领域知识。spec 里的约束链，是嵌入领域知识的标准位置。读者看 spec 学的不只是参数，还有“为什么这些参数之间该有先后关系”。

#### 第四段：验收标准

> **验收标准**：
>
> 1. 两张汇总表：4A 六步 + 4B 五配置（含自由度、组合数、样本内 / 样本外简化夏普、衰减）
> 2. 两张图（各 figsize 12×6，双折线）：4A「规则堆叠：样本内与样本外」+ 4B「自由度越多不等于越好」
> 3. 打印分析按实际数据动态描述：4A 找出“超过这一步的规则都是规则负担”的最佳步、4B 找“自欺欺人开始”的拐点
> 4. 用一句话总结规则负担：规则本身也可能成为过拟合来源

完整示例 spec 在配套仓库的 [`q6-avoid-overfitting/specs/spec-04-rule-burden.md`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/specs/spec-04-rule-burden.md)。参考它确认自己的 spec 后，再复制给 AI，按提示允许执行。

AI 执行完毕后，你的 notebook 里应该出现两组实验的完整结果。

**4A：逐步添加规则。** 从最简单的基础策略出发，每一步多加一条规则，每一步都在样本内做优化。约束条件先把不合理的组合（比如止损大于止盈、移动止损小于固定止损）过滤掉，剩下的结果才不会被荒谬组合污染。

**4B：逐步增加参数维度。** 从只调 1 个参数（动量窗口）开始，逐步增加可调参数的数量。**自由度（Degrees of Freedom）**，就是可以调节的参数和规则数量。自由度越多，优化器的“调节空间”就越大。

### 运行结果

4A 规则堆叠的样本内 / 样本外表现如表 6-6 所示。

**表 6-6 4A 规则堆叠：每一步规则的样本内 / 样本外简化夏普与衰减**

| Layer | 描述 | 组合数 | 样本内简化夏普 | 样本外简化夏普 | 衰减 |
|-------|------|--------|-----------|-----------|------|
| 0 | 基础（固定参数） | 1 | 0.94 | 0.52 | -0.43 |
| 1 | + 优化再平衡频率 | 3 | 1.06 | -0.07 | -1.13 |
| 2 | + 优化止损 | 18 | 1.11 | 0.36 | -0.75 |
| 3 | + 优化止盈 | 57 | 1.33 | 0.15 | -1.18 |
| 4 | + 优化移动止损 | 183 | 1.33 | 0.15 | -1.18 |
| 5 | + 最大回撤风控 | 825 | 1.33 | 0.15 | -1.18 |

4B 指标堆叠（按可优化的自由度分档）的对比如表 6-7 所示。

**表 6-7 4B 自由度堆叠：自由度递增下的样本内 / 样本外简化夏普**

| 配置 | 自由度 | 组合数 | 描述 | 样本内简化夏普 | 样本外简化夏普 | 衰减 |
|------|--------|--------|------|-----------|-----------|------|
| A | 1 | 5 | 动量窗口 | 1.07 | -0.17 | -1.24 |
| B | 2 | 25 | + 波动率窗口 | 1.18 | 0.01 | -1.17 |
| C | 3 | 70 | + 再平衡频率 | 1.18 | 0.01 | -1.17 |
| D | 4 | 420 | + 止损阈值 | 1.39 | 0.50 | -0.88 |
| E | 6 | 323 | + 止盈 + 移动止损 | 1.56 | -0.07 | -1.62 |

随规则数 / 自由度增加，样本内一路向上，样本外却持平甚至下降，对比如图 6-4 所示。

![4A 规则堆叠与 4B 自由度越多不等于越好](images/q6-avoid-overfitting/04-rule-burden-4a-4b.png)

### 结果解读

第四个认知升级：**规则越多不等于策略越好，简单就是美。**

这是本章最重要的认知冲击。

先看 4A。样本内简化夏普从 Layer 0 的 0.94 爬升到 Layer 3 的 1.33。每加一条规则，优化器都能在样本内找到更好看的结果。但样本外呢？Layer 0（基础策略，完全没有额外规则）的样本外简化夏普是 0.52，这是所有步骤中最高的。加了再平衡频率优化后（Layer 1），样本外直接变成了 -0.07。止损（Layer 2）挽回了一些（0.36），但之后的止盈、移动止损、回撤风控全部无效，样本外停在 0.15 不再提升。

注意 Layer 3-5 三行的样本内、样本外、衰减数字一字不差（IS 1.33 / OS 0.15 / 衰减 -1.18）。这不是抄错。Layer 3 引入了 `sl < tp`，Layer 4 又叠 `trail >= sl`，Layer 5 再叠 `max_dd >= sl`。三条约束把搜索空间一步步锁住。再增加的旋钮被前面的约束吃掉，最后什么都调不动。**这是规则负担的另一面：旋钮多了，相互之间的约束也多，优化器其实根本没机会真正“利用”那几个新自由度。**

也就是说：最简单的基础策略，反而是样本外表现最好的。

再看 4B。从配置 D 到配置 E，自由度从 4 增加到 6，样本内简化夏普从 1.39 升到 1.56（好了 12%），但样本外从 0.50 跌到 -0.07。**自欺欺人的拐点**出现了。多出来的两个参数让优化器在样本内找到了更好看的结果，但在样本外完全不兑现。

为什么会这样？每条新规则都增加一个**自由度**，也就是可以调节的旋钮。旋钮越多，优化器就有越多的空间去“凑”出好看的回测结果。这不是在发现真规律，而是在记忆噪音。这就是**规则负担（Rule Burden）**：规则本身也可能成为过拟合的来源。规则太多会让回测看起来很漂亮，甚至在前推验证中也可能过关，但真实交易时很危险。

规则越多，凑出漂亮回测的余地就越大，对真实未来的把握反而可能越小。你不是在让策略更聪明，而是在给历史数据更多机会“配合你”。

**铁律：能用 2 个参数解决的问题，不要用 6 个。每增加一条规则，都要问：样本外也改善了吗？如果没有，删掉它。少即是多。**

**这就是第四个检查工具：规则负担。** 它告诉你少即是多。不是“加规则越多策略越好”，而是“每加一条规则都要在样本外验证它真的帮上忙”。这个工具和前三个不同：前三个帮你检查已有的策略，这一个帮你在设计阶段就少走弯路。

---

## 6.5 回头看：你刚才做了什么？

这一节把本章四个实验铺成一条以后可以复用的检查路径。四个实验回答的是同一个核心问题：这个参数组合是真的好，还是运气好？

- **第 1 个实验：参数优化。** 490 种组合，样本内很好，样本外全面衰减，这就是过拟合。
- **第 2 个实验：Walk-forward。** 模拟“边走边调”，看参数是否稳定。
- **第 3 个实验：交叉验证。** 5 折独立验证，共识强可以更进一步，共识弱就该承认不确定性。
- **第 4 个实验：规则负担。** 规则越多样本内越好，但样本外不买账。简单就是美。

从今以后，拿到任何策略的回测结果，可以用一套防过拟合检查清单核对一遍真实性，如表 6-8 所示。

**表 6-8 防过拟合检查清单**

| 检查项 | 怎么做 | 好的信号 | 坏的信号 |
|--------|--------|----------|----------|
| 样本外验证 | 在没见过的数据上跑 | 衰减小 | 衰减大 |
| Walk-forward | 分窗口优化 + 验证 | 参数跨窗口稳定 | 参数每次都变 |
| 交叉验证 | 多折验证 | 各折结果一致 | 各折差异巨大 |
| 规则负担 | 从简到繁，逐步加 | 样本外同步改善 | 样本内改善但样本外不变 |
| 参数敏感性（第 5 章） | 扫描参数 | 高原型 | 山峰型 |

加上第 5 章的参数敏感性，你手上已经有五个检查工具。第 1 章那次“被自己的回测骗”的经历，到这里沉淀成了一份可以复用的检查清单。这就是“疑”从感觉变成方法的过程。下次再接到一份“漂亮”的回测，你不会只看收益率，而是知道该问哪几个问题、做哪几项检查。

更重要的是：这些检查工具不只是让你识别过拟合，也让你敢于做更有探索性的策略实验。因为你知道，有一套方法可以把漂亮结果拆开验证。怀疑回测不是为了畏手畏脚，而是为了更负责任地动手。

---

## 6.6 本章总结

### 策略进化路径

**第 5 章产出：完整的策略评估方法 + 体检清单**

- 第 1 个实验：参数优化。样本内很好，但样本外衰减，这就是过拟合。
- 第 2 个实验：Walk-forward。模拟定期重新优化，检查参数稳定性。
- 第 3 个实验：交叉验证。多次验证更可靠，也能暴露结果差异。
- 第 4 个实验：规则负担。规则和参数越多，越要警惕过拟合。

**第 6 章产出：防过拟合检查清单 + “怀疑回测”的方法**

### 概念速查表

本章涉及的核心概念汇总如表 6-9 所示，方便随时回查。

**表 6-9 第 6 章核心概念速查**

| 概念 | 含义 | 类比 |
|------|------|------|
| 参数优化 / 网格搜索 | 把所有可能的参数组合都试一遍，选出表现最好的 | 从几千所大学里挑分数最高的 |
| 样本内（训练集） | 用来优化参数的那段数据 | 模拟考试的试题 |
| 样本外（测试集） | 用来验证参数的另一段数据，优化时没见过 | 真正的考试 |
| 过拟合 | 样本内表现很好但样本外大幅衰减 | 背了真题成绩好，但没学会真知识 |
| Walk-forward 分析 | 在历史上模拟“定期重新优化”的过程 | 每学期重新选课本复习 |
| 滚动式 Walk-forward | 训练窗口固定大小，向前滑动 | 只翻最近两年的课本 |
| 锚定式 Walk-forward | 训练窗口从起点开始，越来越大 | 从高一课本开始全部复习 |
| 参数稳定性 | 好参数在不同窗口中保持一致 | 不管怎么考，你的强项科目都一样 |
| 交叉验证 | 用多种切法验证同一个策略 | 做实验要重复多次才可信 |
| 时间序列分割 | 保持时间顺序的交叉验证 | 不能用未来考题“复习”过去 |
| 规则负担 | 每条新规则都可能增加过拟合风险 | 规则越多，越容易把历史数据配合好 |
| 自由度 | 可以调节的参数和规则数量 | 可调旋钮越多，越容易凑出高分 |

### 本章核心认知

走完四步实验后，本章最值得带走的八条认知如表 6-10 所示。

**表 6-10 第 6 章核心认知**

| 认知 | 来源 |
|------|------|
| 参数优化是双刃剑，优化后必须在新数据上验证 | 第 1 个实验 |
| 样本内的好成绩不算数 | 第 1 个实验 |
| 参数稳定性比参数最优更重要 | 第 2 个实验 |
| 好参数跨窗口稳定，坏参数到处变 | 第 2 个实验 |
| 多次验证比单次验证更可靠 | 第 3 个实验 |
| 不同验证方法指向同一个参数，可信度更高 | 第 3 个实验 |
| 规则越多不等于策略越好 | 第 4 个实验 |
| 能用 2 个参数解决的问题，不要用 6 个 | 第 4 个实验 |

### 带走的问题

我们已经知道怎么评估策略（第 5 章）、怎么防过拟合（第 6 章）。回测世界的问题基本解决了。

但回测终究是模拟。在回测里，你可以按任何价格、在任何时间成交。真实世界里呢？

- 你想买的时候，市场上有足够的卖盘吗？
- 你下单到成交之间，价格会不会已经变了？
- 回测假设的手续费，和真实交易一样吗？

**从回测到真实交易，还有多少问题在等着你？** 这就是第 7 章要回答的问题。

你在第 1 章看见过过拟合，第 5 章手里有了体检表，这一章把怀疑变成了具体检查。下一章，回测世界要让位给真实成交的世界。

> 本章所有代码的可运行版本见配套仓库的 [`q6-avoid-overfitting/notebooks/q6-avoid-overfitting.ipynb`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/notebooks/q6-avoid-overfitting.ipynb)
