# 第 1 章：第一次跑通一个完整策略

前言中那张地图——**飞轮的四层（候选 / 组合 / 执行 / 归因）+ 三字要点（做 / 看 / 疑）**——在这一章会被你**亲手走一圈**。你把“做 + 看 + 疑”三个动作都练一次，飞轮也在你手里转完第一圈。

7 个动手实验串成一条主线。先从最简单的定投起步，把动作写成规则（**做**），看它能不能赚钱（**看**）。再给它加一条判断让它“聪明”一点，看判断有没有让结果更好（**再做、再看**）。最后扫描一圈参数，亲眼看到量化交易最常见的失效场景——**回测里看着赚钱，换一段时间就不行了**（**疑**）。

这就是为什么本书的口号是「做要规则、看要指标、还要怀疑你的指标」 。

**路线图**

本章 5 节实验 + 1 节总结，串成第一次完整飞轮闭环，路线如表 1-1 所示。

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

| 节 | 内容 | 实验 |
|----|----|----|
| 1.1 最简单的量化策略长什么样？ | 从“笨定投”起步——拿到沪深 300ETF 数据，跑一次最朴素的每月固定金额买入回测 | 2 |
| 1.2 我的收益算好还是算差？ | 引入“基准”——和“什么都不做的买入持有”对比，理解 Alpha 与 Beta | 1 |
| 1.3 怎么才能赢过市场？ | 给定投加一条均线判断，看见“加判断”的双刃剑 | 2 |
| 1.4 找到“最优参数”就能盈利了？ | 暴力扫描 24 个均线参数，找历史收益率最高的“冠军” | 1 |
| 1.5 真相时刻 | 把数据切成训练 / 测试，看冠军参数到了新数据上是否真有本事 | 1 |
| 1.6 本章总结 | 把 7 个实验串成一条策略进化路径，带走第一次飞轮闭环 | 0 |
| 拓展阅读 | 量化在金融世界的位置；钱从哪来 | — |

## 1.1 最简单的量化策略长什么样？

这一节我们走完飞轮的第一步——**做**：把策略写成规则、跑一次回测、看结果。

我们从最简单的策略开始：**定投**。

没错，就是你可能在银行 App 上见过的那个“定期定额投资”。每个月发了工资，拿出固定的一笔钱买入，不看涨跌、不做判断，机械地重复。这算量化吗？当然算——“量化”只要满足两个条件就够了：规则明确 + 执行可重复。定投完全够格。

不过，我们定投的不是某只个股，而是 **ETF（Exchange-Traded Fund，交易所交易基金）**。你可以把 ETF 理解成一份“指数套餐”。我们选择的是沪深 300ETF（代码 510300），它把 A 股市场上规模最大的 300 家公司打包在一起——银行、白酒、新能源、互联网，各行各业的龙头都在里面。你买一份 ETF，就等于同时持有了这 300 家公司的股票，不需要你自己去挑选哪家公司好、哪家公司差。

为什么选 ETF 而不是某只股票？因为选股本身就是一门极深的学问。个股可能暴涨，也可能退市归零。而 ETF 跟踪的是整个市场的平均表现——单家公司出问题，其他 299 家还在。对新手来说，一键买入整个市场，是最省心、也最不容易犯大错的起点。

所以我们的定投规则就是：**每月第一个交易日，用固定金额（1000 元）买入沪深 300ETF。** 没有任何判断，没有任何预测，不看新闻，不猜涨跌。就这么简单。

本章 7 个实验都写在同一个 notebook 文件 `q1-strategy.ipynb` 里——第一份 spec 会让 AI 帮你创建它，后面 6 份直接接着写，变量、函数、图都在同一个 notebook 里累积下来。

### 动手实验 1：获取历史数据

接下来是你的第一个动手实验——拿到沪深 300ETF 5 年的历史数据。我们一起把这份 spec 写出来。

#### 起草上下文

我们刚装好环境，notebook 是空的，什么都还没做。上下文一句话说清：

> **上下文**：学员已完成环境配置（Python 3.12 + 虚拟环境 + 依赖包已安装）。这是课程的第一个操作步骤。
>
> 📌 **要点**：上下文段交代“前提是什么”。AI 不知道你之前做了什么，要明示——否则它可能假设环境已有现成数据，跳过下载步骤。

#### 起草任务

任务段要具体到一句话能说清。“获取数据”是反例——什么数据？什么时间？什么形式？`q1-strategy.ipynb` 是这份任务说明的交付物，这个文件就是一个 Python notebook 文件。notebook 文件是一种交互式的编程源代码，区别于非交互式的 Python 代码，我们在这份文件中可以看到策略的探索过程。

> **任务**：在当前工作目录创建 `q1-strategy.ipynb`，获取沪深 300ETF（510300.SS）最近 5 年的历史日线数据并可视化。
>
> 📌 **要点**：关键事物要点名到 ID。“沪深 300”不够——A 股有沪深 300 指数（000300.SH）也有跟踪它的 ETF（510300.SS、510310.SS、510330.SS），AI 不知道你想要哪个。spec 里所有关键名词都要具体到不能再具体。

#### 起草要求

要求段是步骤主体——AI 自由度最低的部分。我们规定四件事：

> **要求**：

1.  导入 yfinance, pandas, numpy, matplotlib.pyplot
2.  跨平台中文字体 fallback 链：`plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'STHeiti']` + `plt.rcParams['axes.unicode_minus'] = False`
3.  yfinance 获取数据：标的 `510300.SS`、起始 `2021-01-01`、参数 `auto_adjust=True, multi_level_index=False`
4.  数据存到变量 `df`，索引为 DatetimeIndex，列名含 Close

> 📌 **要点**：每个非默认参数都要点名。yfinance 默认会自动调整价格、用多级列名——这两个参数不写出来，AI 可能用默认，结果跟书里不一致。**不要让 AI 揣摩你的偏好。**

#### 起草结果呈现

最后是“成功长什么样”——这是 spec 最容易被忽略也最重要的部分：

> **结果呈现**：

1.  打印数据的前 5 行
2.  打印数据时间范围（起始日期 到 结束日期）和总行数
3.  画一条收盘价折线图，标题为「沪深 300ETF (510300) 收盘价走势」

> 📌 **要点**：结果呈现要让 AI 跑完自己能说“对了 / 错了”。“画一张图”是反例——AI 可能画 K 线、画柱状图、画散点图。要明确到具体的 X/Y 轴、标题文字、变量名。

完整 spec 在 [`specs/spec-01-get-data.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-01-get-data.md)——把它复制到 AI 编程工具的对话框，弹窗选「允许」。spec 文件用 Markdown 写成（`#` 标题、`-` 列表、反引号代码——都是给 AI 看的格式，完整复制就好）。

AI 助手执行完毕后，你会在你的文件夹中看到多出一个叫 `q1-strategy.ipynb` 的文件。第一次双击打开时，TRAE 会询问你使用什么 Python 核，意思就是：这份 notebook 交给哪个 Python 环境来运行。我们在这里会选择在“开始动手之前”这一章中初始化的 Python 3.12 环境，如图 1-1 所示。

![选择 notebook 使用的 Python 核](images/q1-how-to-profit/notebook-python-core.png)

打开 notebook 文件后，源代码是一个一个单元格从上向下构成。这个界面中有几个常用按钮，如图 1-2 所示。

1.  单步运行：每一个单独的代码单元格，可以单独运行这一步。运行结果会在这个单元格下方出现。

2.  全部运行：整个notebook从上向下，逐一全部运行，直到出现错误为止。每个单元格的运行结果都会紧接着出现在代码单元格下方，运行错误也在下方。

3.  重新启动：重新启动所选择的python核。通常，在初始化的python环境中安装了新的python包，或者更新了某个旧的python 包，会重新启动。

4.  当前 python 核：显示当前 notebook 在使用哪个 python 核。

![notebook 常用按钮](images/q1-how-to-profit/notebook-buttons.png)

回到你的第一个成果上来，这时候你的 notebook 里应该出现了一张折线图和一些打印输出。AI 帮你做了这些事：从雅虎财经下载了沪深 300ETF（代码 `510300.SS`）从 2021 年 1 月至今的每日价格数据，打印了前 5 行让你看看数据“长什么样”，然后画了一张收盘价走势图，如图 1-3 所示。

![沪深 300ETF 收盘价走势](images/q1-how-to-profit/01-price-history.png)

### 读懂你的第一张图

看看这张收盘价走势图。

**先看整体趋势**：价格总体是在涨还是在跌？有没有明显的“大起大落”？你大概率会看到价格经历了几轮上涨和下跌的周期，不是一条直线往上走，也不是一路向下。

**再看波动范围**：最高点和最低点大概差多少？价格在什么区间内震荡？你会发现，波动幅度可能比你想象的大——同一只 ETF，不同时间点买入，结果可能天差地别。

**这说明什么？** 价格有涨有跌，波动是常态。你不可能买在最低点、卖在最高点——没有人能做到。既然择时这么难，那有没有一种方法，可以不依赖“买在最低点”也能参与市场？定投就是这个思路。

有了数据，一个自然的问题就出来了：**如果从几年前开始，每月固定买入一些，到现在是赚了还是亏了？** 我们来用历史数据回答这个问题。

### 动手实验 2：定投回测

在真正投钱之前，我们当然想先知道这个策略靠不靠谱。怎么办？用历史数据模拟一遍。

这就是 **回测（Backtest）** 的概念——用历史数据模拟“如果当时按某个规则操作，结果会怎样”。就像在脑海中“重播”历史，看看你的策略在过去能不能盈利。回测不能预测未来，但能帮你验证一个想法是否靠谱。

我们一起把这份回测 spec 写出来。这次重点看两件新东西：**spec 之间怎么接续**，以及**复用代码怎么封装成函数**。

#### 起草上下文

这次的上下文不再是“什么都没有”——上一份 spec 已经留下了 `df` 变量。我们直接接着用：

> **上下文**：在 `q1-strategy.ipynb` 中已有上一步的代码：使用 yfinance 获取了沪深 300ETF（510300.SS）的日线数据，存储在变量 `df` 中，索引为 DatetimeIndex，包含 Close 列。
>
> 📌 **要点**：spec 之间会有“接续关系”——前一份 spec 的输出是后一份的输入。上下文段把这条链条写明（变量名、形状、关键列）。这样 AI 不会重新下载数据、也不会假设一个不存在的列。

#### 起草任务 + 要求

任务一句话：“实现简单定投回测——每月第一个交易日用 1000 元按收盘价买入”。要求段重点在两件事：

> **要求**：

1.  把回测逻辑封装为函数 `backtest_dca(df, monthly_amount=1000)`，返回 DataFrame（**后续步骤会复用此函数**）
2.  实现：按月分组取每月第一交易日 → 每月份额 = 1000 / 收盘价 → 累计份额逐月累加 → 月末市值 = 累计份额 × 当月最后收盘价 → 累计收益率 = (市值 - 成本) / 成本
3.  返回 DataFrame 含列：`total_cost, total_shares, portfolio_value, return`

> 📌 **要点**：复用型代码必须封装成函数。“后续步骤会复用此函数”这一句话是给 AI 的硬约束——不要写一段一次性脚本，要写一个有清晰入参的函数。后面的实验 5 会基于它派生 `backtest_dca_with_ma`，函数没封装好整条 spec 链就接不上。
>
> 📌 **要点**：返回的 DataFrame 列名要点死。这里写的是 `total_cost, total_shares, portfolio_value, return`——四个名字一字不差。后面好几节实验都要从 `result_dca.return` 这种路径取数，列名只要换成 `ret` 或 `cum_return`，后面所有 spec 都得连带改。**返回值的字段名是 spec 链的“API 契约”，锁死才能让后面的 spec 写起来一行不差。**

#### 起草结果呈现

> **结果呈现**：打印投资期间 / 投资月数 / 总投入金额 / 最终市值 / 最终收益率。画两张图（上下排列，figsize 12×8）：上图持仓市值 vs 累计成本（盈利绿、亏损红），下图累计收益率（零线上下绿/红填充）。

完整 spec 在 [`specs/spec-02-dca-backtest.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-02-dca-backtest.md)——复制给 AI，弹窗选「允许」。

AI 助手执行完毕后，你的 notebook 里会出现一组数字和两张图。AI 帮你做了这些事：按月分组找到每个月的第一个交易日作为定投日，用 1000 元除以当日收盘价算出每月能买多少份，逐月累加持有份额和投入成本，最后计算每月末的持仓市值和累计收益率。

定投的机制有一个天然的好处：价格低的时候，同样 1000 块钱能买到更多份额；价格高的时候，买到的份额更少。就像去菜市场，菜贵的时候少买点，菜便宜的时候多买点。

AI 助手还画出了上下两张图（如图 1-4 所示）：

- **上图「持仓市值 vs 累计成本」**：蓝色实线是你的持仓市值，它会随市场波动上下起伏；另一条线是你的累计投入成本，它稳步上升（毕竟每个月都在投钱）。两条线之间的区域用颜色标注——绿色代表盈利的时段，红色代表亏损的时段。
- **下图「累计收益率」**：更直观地看到收益率随时间的变化曲线。零线以上是赚，以下是亏。

![定投回测结果](images/q1-how-to-profit/02-dca-backtest.png)

### 读懂你的回测结果

先看打印出来的数字：总投入是多少，最终市值是多少，收益率是正还是负？

再看两张图。你大概率会发现，收益率在正负之间来回穿越——有一段时间盈利，过一阵又亏回去，然后可能又赚回来。这不是你的策略有问题，而是市场本来就这样。定投不保证盈利，也不保证亏损。它只是一种“分散买入时点”的方法，避免了把所有钱一次性投在最高点的风险，降低了运气的成分。

不管你看到的最终结果是正收益还是负收益，有一件事比结果本身更重要：**你怎么知道这个结果算好还是算差？**

赚了 5%，听起来还行？但如果同期银行理财能给你 4%，那你承受了股市的波动，只多赚了 1%，值得吗？亏了 3%，听起来很糟？但如果同期大盘跌了 15%，你其实已经大幅跑赢了市场。

这就像考试拿了 80 分——你得知道全班平均分才能判断自己考得好不好。我们需要一个参照物。

## 1.2 我的收益算好还是算差？

上一节最后留下了一个问题：一个收益率数字本身，不能告诉你任何事。赚了 20% 是好是坏？亏了 5% 是糟糕还是其实还行？这取决于你跟谁比。

你需要一个 **基准（Benchmark）**。基准就是“全班平均分”——你不知道 80 分算好算差，但如果全班平均 60 分，你就知道自己考得不错；如果全班平均 95 分，那 80 分就得反思了。投资也是一样。你的收益率只有跟某个参照物放在一起看，才有意义。

跟谁比？其实有三种不同层次的基准：

- **绝对基准**：你能接受的最低收益率。比如“年化至少 5%，否则我还不如买理财”。这是你的个人底线。
- **相对基准**：市场指数。你的策略是赢了大盘还是输了大盘？赢了说明你有超额收益，输了说明你还不如买指数基金。
- **策略基准**：同类策略中最简单的那个。择时策略要和“买入持有”比——你费心选时机，总得比什么都不做强吧？权重优化策略要和“等权重”比——你费心调配比例，总得比平均分配强吧？

在后面的章节中，我们会逐步引入这三种基准。但眼下先从最基本的开始。

那什么是最自然的基准？想象你在投资的第一天，面前有两个选择：一是按照某个策略每月定投，二是把所有钱一次性全部买入，然后什么都不做，一直拿着到最后一天。第二个选择就是 **买入持有（Buy & Hold）**——它代表“什么都不做”的结果。你的策略要是连“什么都不做”都赢不了，那它存在的意义是什么？

所以我们接下来要做的事很简单：算出“什么都不做”能赚多少钱，然后跟定投的结果放在一起比。

### 动手实验 3：和基准对比

我们一起把这份基准对比 spec 写出来。这次重点看一个新东西：**结果呈现段如何写“动态结论”**——不知道跑出来谁赢谁输，怎么提前定义两种结果的解读？

#### 起草上下文 + 任务

> **上下文**：在 `q1-strategy.ipynb` 中已有沪深 300ETF 日线数据（变量 `df`，含 Close 列）和简单定投回测结果（变量 `result_dca`，含 return 列）。
>
> **任务**：计算「一次性买入持有」作为基准，和定投策略对比，帮助理解收益来源。

#### 起草要求

> **要求**：

1.  基准策略（Buy & Hold）：起始日全部资金一次性买入，持有到最后一天；基准收益率 = (终点 - 起点) / 起点，存到 `benchmark_return`
2.  计算基准的逐月累计收益率曲线（每月末收盘价相对起始价的涨跌幅）
3.  与定投策略对比

#### 起草结果呈现

这一段是这份 spec 最有意思的地方——我们不知道沪深 300 这 5 年是涨是跌，所以**得提前写好两种结果的对应文案**：

> **结果呈现**：

1.  打印起点 / 终点价格、一次性买入收益率、定投收益率、两者差异
2.  根据对比结果**动态选**一句话：
    - 如果定投跑赢：「在下跌市中，定投通过分批买入避开了部分高位」
    - 如果定投跑输：「在上涨市中，越早全部投入越好」
3.  无论哪种结果，都补充一句共同收束：「但两种策略都没有 Alpha——收益完全取决于市场涨跌，这就是 Beta」
4.  画一张对比图（figsize 12×6）：定投 vs 一次性买入两条累计收益率曲线，图例标注名称

> 📌 **要点**：spec 不能预设结果方向。“分批买入避开高位” / “越早全部投入越好”听起来像两个相反的结论，但它们其实是对**同一个现象**的两种表述——市场跌了 / 涨了。把“双分支动态结论”写进 spec，让 AI 跑完**结果会自己讲故事**，无论涨跌都不会出现“硬写但和数据不符”的尴尬。
>
> 📌 **要点**：双分支后面常需要一句“共同收束”。本 spec 的“两种策略都没有 Alpha”是把读者的注意力**从‘谁赢’拉到‘两者本质相同’**——这是教学型 spec 写作的隐藏技巧：分歧的下面藏着共识。

完整 spec 在 [`specs/spec-03-benchmark.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-03-benchmark.md)——复制给 AI，弹窗选「允许」。

AI 助手执行完毕后，你的 notebook 里会出现一组对比数字和一张双线对比图。AI 帮你做了这些事：计算了买入持有的收益率（用最后一天的价格减去第一天的价格，再除以第一天的价格），然后把它和定投的收益率放在一起对比，算出两者的差异。

两条收益率曲线画在同一张图上（如图 1-5 所示）。它们之间的距离，就是两种策略在每个月的差异。有时候定投在上面，有时候买入持有在上面——领先和落后是交替出现的。这张图的价值在于：它不只告诉你最终结果谁赢了，还让你看到整个过程中两者的“赛跑”。

![定投 vs 买入持有](images/q1-how-to-profit/03-dca-vs-buyhold.png)

### 读懂对比结果

先看打印出来的数字。你会发现一个有意思的现象：沪深 300ETF 的起点价格和终点价格可能非常接近。也就是说，如果你在数据起始日一次性买入，持有到现在，可能几乎没赚没亏——市场在这几年里“坐了一趟过山车”，最后又回到了起点附近。

但定投的结果呢？大概率是正收益。为什么“什么都不做”几乎没有收益，而定投反而获得了正收益？

原因不复杂。回想一下定投的机制：价格低的时候，同样 1000 块钱能买到更多份额；价格高的时候，买到的份额更少。过去几年沪深 300 经历了一轮明显的下跌，又有所回升。在下跌过程中，定投不断在低位买入，积累了大量便宜的份额。等到价格回升，这些便宜份额带来的利润弥补了高位买入的亏损。而一次性买入就没有这个“摊低成本”的机会——你在最开始买了，然后只能眼睁睁看着价格跌下去再涨回来。

再看对比图。两条线的交叉和分离，就是两种策略的攻守转换。你会发现，在市场下跌的阶段，定投那条线通常在上面（跌得少）；在市场快速上涨的阶段，买入持有可能追上来甚至反超。这也符合直觉——下跌市里，分批买入天然就比一次性买入有优势，因为你避开了在最高点“全仓”的风险。

### 两个收益率背后的大问题

到这里，不管谁赢了，你可能会觉得：差距其实也没那么大嘛。对，你说得对。因为这两种策略做的是同一件事——买入沪深 300ETF 然后持有。区别只是“一次性全买”还是“分批买”。它们的收益来源是完全一样的：市场本身的涨跌。市场涨，两个策略都赚；市场跌，两个策略都亏。

这引出了量化交易中两个最重要的概念。

**Beta（贝塔）** 是你“坐电梯”获得的收益。你走进商场，坐上电梯，电梯往上走，你就到了高处。你做了什么吗？你什么都没做，只是站在电梯里。市场就是那部电梯。市场涨，你的资产就跟着涨；市场跌，你的资产就跟着跌。你承担了市场整体涨跌的风险，市场给你的回报就是 Beta。

定投和买入持有，本质上都是在“坐电梯”。收益完全取决于这部电梯——沪深 300——是往上走还是往下走。你没有做出任何“判断”，没有选择买入时机，没有选择哪只股票，没有在任何地方做出比市场更聪明的决定。赚的钱全是 Beta。

那如果有人不甘心只坐电梯呢？

**Alpha（阿尔法）** 是你“爬楼梯”多走的路。别人坐电梯到了 10 楼，你坐电梯到了 10 楼之后，又自己爬了 5 层到 15 楼——那多出来的 5 层就是你的 Alpha。翻译成投资语言：Alpha = 你的收益 - 市场的收益。如果市场涨了 10%，你赚了 15%，那你有 5% 的 Alpha。如果市场涨了 10%，你只赚了 8%，那你的 Alpha 是 -2%——你不仅没爬楼梯，还在电梯里摔了一跤。

Alpha 需要你做出某种“判断”，并且这个判断要比市场聪明。比如，判断什么时候该买、什么时候该卖、该买什么、不该买什么。这些判断如果正确，就能产生正的 Alpha；如果错误，Alpha 就是负的——你还不如什么都不做。

回到我们的定投策略：它有 Alpha 吗？没有。零。定投不做任何判断，不猜涨跌，不择时。它的收益百分之百来自 Beta。市场好，定投就盈利；市场差，定投就亏损。它只是一种资金分配方式，不是一种“聪明”的策略。

这个结论很重要，值得你记住：**到目前为止，我们的策略没有任何 Alpha。收益完全来自 Beta——市场好就赚，市场差就亏。**

既然定投没有 Alpha，怎样才能赢过市场？我们需要在策略里加入一些“自己的判断”。最简单的判断工具，是一条线。

## 1.3 怎么才能赢过市场？

上一节结尾说了：要产生 Alpha，就得在策略里加入“判断”。但什么叫“判断”？不是你拍脑袋觉得明天会涨就买——那叫赌博。量化的判断，必须是一条可以写成明确规则的逻辑。

最简单的判断工具，是一条线——**均线（Moving Average）**。

均线是什么？打个比方。你今天量体温 37.5°C，是不是发烧了？不好说，也许你刚跑完步、也许体温计不太准。但如果你连续 5 天量体温，算个平均值，发现平均体温都偏高——那大概率是真的不舒服了。均线就是这个“平均体温”。它取过去 N 天收盘价的平均值，把每天的随机波动抹平，留下更稳定的趋势方向。单看某一天的价格就像单看某一次体温，可能被噪音干扰；但看一段时间的平均值，信息就可靠多了。

那“价格在均线上方”意味着什么？意味着最近的价格比过去一段时间的平均价格高。换句话说，市场最近在涨，可能正处于上升趋势。注意我用了“可能”——均线是个**滞后指标（Lagging Indicator）**，它看的是过去，不预测未来。就像你说“这个星期天气都不错”，不代表明天一定晴天。但作为一个参考信号，它总比什么都不看要强。

基于这个逻辑，我们给定投策略加一条规则：每月定投日，**仅当收盘价高于 N 日均线时才买入**，否则这个月跳过。就这么简单。不买的月份，钱留在手里，份额和成本都不变。

这就是**择时（Market Timing）**——不再闭着眼睛每月都买，而是先看看“天气”再决定出不出门。市场趋势向上的时候跟着买入，趋势不明朗的时候按兵不动。听上去很合理，对吧？

但这里有一个问题：N 取多少？过去 10 天的平均值？20 天？还是 60 天？不同的 N 就是不同的“回看窗口”：

- **MA10**（10 天，约 2 周）：短期均线。反应灵敏，价格稍微涨几天它就跟着翘头，但也容易被短期波动骗到——价格随便抖一抖，信号就来回翻转。
- **MA20**（20 天，约 1 个月）：中期均线。过滤掉了大部分日常噪音，是比较常用的参数。
- **MA60**（60 天，约 3 个月）：长期均线。非常稳定，但反应迟钝。市场可能已经涨了一大截，它才慢悠悠地确认“嗯，确实在涨”。

到底哪个好？光想没用，我们跑一遍就知道了。但在跑回测之前，先画一张图，亲眼看看均线到底长什么样。

### 动手实验 4：看见均线

这份“画图” spec 比前面几份更挑细节——它要把图怎么画**全部锁死**到颜色、线型、标记形状。我们一起把它写出来，看看“视觉契约”要写多细才够。

#### 起草上下文 + 任务

> **上下文**：在 `q1-strategy.ipynb` 中已有沪深 300ETF 日线数据（变量 `df`，含 Close 列）。上一节引入了均线的概念——过去 N 天收盘价的平均值。
>
> **任务**：画出收盘价与三条均线（MA10 / MA20 / MA60）的对比图，并标记 MA20 的上穿 / 下穿点。

#### 起草要求：视觉契约

要求段是这份 spec 的灵魂——**视觉细节全部锁死**：

> **要求**：

- 截最近 1 年数据：`df_recent = df.loc[df.index[-1] - pd.DateOffset(years=1):].copy()`
- 三条均线：`rolling(10/20/60).mean()`
- 找 MA20 交叉点：上穿（前 Close ≤ MA20，当 Close \> MA20）/ 下穿（前 Close ≥ MA20，当 Close \< MA20）
- 画对比图（figsize 14×7）：
  - 收盘价（黑色实线）/ MA10（绿色虚线）/ MA20（蓝色实线）/ MA60（红色点线）
  - 上穿点：绿色向上三角（`marker='^'`, markersize=10）
  - 下穿点：红色向下三角（`marker='v'`, markersize=10）
  - 标题「沪深 300ETF：收盘价与均线（最近 1 年）」+ 图例 + 网格

> 📌 **要点**：可视化 spec 必须把视觉契约写到颜色、线型、标记形状级别。“画一张对比图”是反例——AI 可能给你 default 蓝色细线、不带 marker、不带网格的图，跟书里截图完全对不上。**每一个视觉决策都要进 spec：颜色用什么 hex / linestyle 用“–”还是“:”/ marker 多大 / figsize 多少。** 这是【精确】原则在视觉场景的具体落点。
>
> 📌 **要点**：截窗口要带 `.copy()`。要求里的 `df.loc[...].copy()` 不是装饰——pandas 里没 `.copy()` 直接切片是“视图”，后面再往这个视图上加列（MA10/MA20/MA60）会触发 `SettingWithCopyWarning`，有时甚至改不到原数据。**spec 里写明** `.copy()` **比写一句“注意切片”更稳**——AI 看到 `.copy()` 就知道你要的是独立副本。

#### 起草结果呈现

> **结果呈现**：价格与均线对比图（含交叉点标记）+ 打印交叉统计「上穿 N 次 / 下穿 N 次」。

完整 spec 在 [`specs/spec-04-ma-visual.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-04-ma-visual.md)——复制给 AI，弹窗选「允许」。

AI 助手执行完毕后，你的 notebook 里会出现一张价格与均线的对比图，如图 1-6 所示。

![收盘价与均线](images/q1-how-to-profit/04-ma-visual.png)

### 读懂均线图

现在你亲眼看到了均线和交叉信号。几个值得注意的地方：

**三条均线的“性格”不同。** 绿色的 10 日均线紧贴价格，几乎同步波动——价格稍微涨几天它就跟着翘头，跌几天它就立刻掉头向下。蓝色的 20 日均线稍微平滑一些，过滤掉了一些日常噪音。红色的 60 日均线最“淡定”，价格上下翻飞它才慢慢跟。这正是前面说的“回看窗口”的差异：窗口越短，越灵敏也越容易被骗；窗口越长，越稳定也越迟钝。

**交叉点就是信号。** 绿色三角标记的是“上穿”——收盘价从 MA20 下方穿到上方，意味着短期价格超过了最近一个月的平均水平，趋势可能在转好。红色三角标记的是“下穿”——收盘价跌破了 MA20，趋势可能在转弱。

**信号不少，但不是每次都“对”。** 仔细看图：有些上穿之后确实涨了一段，信号是“对”的；但也有些上穿之后价格又很快跌回去——刚穿上去就掉下来，这就是“假信号”。均线越短，假信号越多；均线越长，信号越少但更迟钝。没有哪条均线能百分百准确。

有了这个直觉，我们来试试：如果真的按这些信号来决定买不买，结果会怎样？

### 动手实验 5：加入均线择时

这份均线择时 spec 多了一件事——把可能爆炸的边界 case 列出来。均线初期没值、累计成本为 0 时除法会爆，这些都要在 spec 里点名。我们一起把它写出来。

#### 起草上下文 + 任务

> **上下文**：在 `q1-strategy.ipynb` 中已有沪深 300ETF 日线数据（`df`）、简单定投回测函数 `backtest_dca` 和结果（`result_dca`）、基准对比代码。
>
> **任务**：在定投基础上加入均线择时——每月第一个交易日，仅当收盘价 \> N 日均线才买入。

#### 起草要求：边界 case 显式

> **要求**：

1.  函数 `backtest_dca_with_ma(df, monthly_amount=1000, ma_period=20)`，返回格式同 `result_dca`，额外含 signal 列（布尔值）。**后续步骤会复用此函数**
2.  三个边界 case 必须显式处理：
    - 均线计算初期（前 N 天）没均线值，这些月份不买入
    - 跳过的月份，累计成本和份额保持不变
    - 累计成本为 0 时（尚未发生任何买入），收益率直接设为 0，**不要用除法**，避免除零警告
3.  分别跑 MA10、MA20、MA60 三个参数

> 📌 **要点**：边界 case 必须显式写进 spec。“每月有买入条件”听起来简单，但隐藏着三个炸点：① 前 N 天没均线 ② 跳过月份的状态怎么传递 ③ 累计成本为 0 时除法会 NaN。**只要 spec 不点名这三件事，AI 写出的代码 60% 概率会在某一个炸点上出 warning 或错值。** 你不需要让 AI 自己发现边界——你列清楚，它照办。
>
> 📌 **要点**：派生函数的入参命名要和原函数对齐。`backtest_dca_with_ma(df, monthly_amount=1000, ma_period=20)` 的前两个入参和实验 2 的 `backtest_dca(df, monthly_amount=1000)` 完全一致——只在末尾加 `ma_period`。这不是形式主义：spec 链越往后，函数会被反复调用，入参命名一致才能让“两个回测”的对比代码读起来对称。**新加参数追在已有参数后面、不打乱原顺序，是 spec 链可读性的隐藏前提。**

#### 起草结果呈现

> **结果呈现**：

1.  打印每个参数：`MA10: 收益率 X.XX%, 买入 N/M 次` + 简单定投作为对照
2.  画对比图（figsize 12×6）：简单定投灰虚线 + MA10/20/60 三色实线，图例 + 网格 + 零线

完整 spec 在 [`specs/spec-05-ma-timing.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-05-ma-timing.md)——复制给 AI，弹窗选「允许」。

AI 助手执行完毕后，你的 notebook 里会出现一组对比数据和一张多线对比图。AI 帮你做了这些事：在每个交易日计算过去 N 天收盘价的平均值（均线），然后在每月定投日比较当天收盘价和均线值——价格在均线上方就买入，在下方就跳过。和简单定投的区别就在这里：简单定投每个月都买，均线策略有些月份会选择“不出手”。AI 分别用 MA10、MA20、MA60 三个参数各跑了一遍回测，然后画了一张对比图（如图 1-7 所示）：灰色虚线是简单定投作为基准，三条有色实线是不同均线参数的策略，四条线放在一起一眼就能看出谁在什么时候领先、什么时候落后。

![均线择时对比](images/q1-how-to-profit/05-ma-timing.png)

### 读懂均线择时的结果

先看打印出来的数字。每一行告诉你两件事：最终收益率和买入次数。

注意买入次数的变化。简单定投是每个月都买，均线策略则会跳过一些月份。MA10 跳过得少一些，因为短期均线容易被价格快速穿越；MA60 跳过得多一些，因为长期均线更“迟钝”，价格要涨很久才能站到它上面。换句话说，均线周期越长，策略越“挑剔”——不是每个月都觉得“可以买”。

再看收益率。三个参数的收益率各不相同——有的可能比简单定投好，有的可能更差。这是第一个值得注意的现象：**加了均线判断，不一定收益更高。**

为什么会这样？因为均线择时是一把双刃剑。它帮你跳过了一些“价格在下跌通道”的月份，避免了在下跌中继续投钱，这是好事。但它也可能让你错过一些“刚开始反弹”的月份——价格刚从底部起来，还没站上均线，你就没买到最便宜的那几次。少买的月份如果后来涨了，你就少赚了。

现在看对比图。灰色虚线是简单定投，有色实线是三条均线策略。注意它们什么时候分开、什么时候靠近。你可能会发现：在市场大幅下跌的阶段，均线策略的曲线有时比简单定投下跌得少——因为它跳过了一些下跌月份的买入。但在市场持续上涨的阶段，均线策略可能跟不上简单定投——因为有些月份它“犹豫”了，没有买入。

这里有一个关键观察，请你记住：**加入判断条件确实改变了结果，但改变的方向不确定。它不是一个“只赚不赔”的改进，而是一种取舍——用“可能错过上涨”换“可能避开下跌”。**

而且三个参数的效果不一样。MA10、MA20、MA60，谁最好？在你手上这段数据里，可能 MA20 赢了，也可能 MA60 更好——这取决于市场在过去几年具体走了什么样的路径。

三个参数效果不同——MA10、MA20、MA60，到底哪个最好？为什么不干脆把所有可能的参数都试一遍呢？从 MA5 到 MA120，每隔 5 天试一个，总有一个是最优的吧？

## 1.4 找到“最优参数”就能盈利了？

上一节我们试了三个参数，效果各不相同。你心里可能已经在想：才三个参数就想找到最优解？太不严谨了。干脆全试一遍——从 MA5 到 MA120，每隔 5 天取一个，一共 24 个参数。跑完之后，选收益率最高的那个。听起来是不是很“科学”、很“严谨”？毕竟我们没有拍脑袋猜，而是用数据说话、穷举搜索。

为什么是 5 到 120 这个范围？5 天太短，几乎等于在看每周的波动，全是噪音；120 天约等于半年，再长的话均线几乎不动，策略基本不会发出买入信号。5 到 120 覆盖了实务中常用的所有均线周期——短期、中期、长期都包含在内。每隔 5 天取一个是为了效率：不需要逐天试，大趋势的差异在 5 天级别就能体现出来，MA15 和 MA16 的结果不会有质的不同。

不过在看结果之前，你可以先想一个问题——如果我朝墙上射了 24 支箭，总有一支是最接近靶心的。但这说明我箭术好吗？先把这个问题放在心里，我们来看看实验结果。

### 动手实验 6：参数扫描

这份参数扫描 spec 有点反常——它**故意保持 naive**：不警告“过拟合”，不分训练 / 测试集，就让你“找最优”。我们一起把它写出来，先经历一次“找到漂亮数字”的过程，下一节再回头看看那个数字到底是什么。

#### 起草上下文 + 任务

> **上下文**：在 `q1-strategy.ipynb` 中已有 `df`、`result_dca`、带均线条件的回测函数 `backtest_dca_with_ma`。
>
> **任务**：扫描所有均线参数（MA5 到 MA120，步长 5），找到历史收益率最高的「最优参数」并可视化。

#### 起草要求 + 结果呈现

> **要求**：扫描范围 MA5 到 MA120 步长 5（共 24 个参数）→ 调用已有的 `backtest_dca_with_ma` 跑一遍 → 记录每个参数的最终收益率 → 找出最高那个。
>
> **结果呈现**：打印最优参数 / 最优收益率 / 简单定投对照 / 提升幅度；画柱状图（figsize 12×5），正收益绿、负收益红、最优金色高亮黑边框、灰虚线标定投基线、黑实线标零线。
>
> 📌 **要点**：教学型 spec 有时要“刻意保持 naive”。这份 spec 的 spec-writer 是知道过拟合存在的——但 spec **不能**写“小心过拟合”或者“加一个 train/test split”。如果这里就警告了，下一节“真相时刻”的冲击力就消失了。**让学员先经历“找到漂亮数字”的兴奋，下一份 spec 再让那个数字在新数据上原形毕露，认知冲击才有教学价值。** 学员自己写“研究探索型 spec”时也是同理——先写一个 naive 版本看看会怎样，再升级。
>
> 📌 **要点**：spec 的“语气”也是设计选择。本 spec 的任务段写“找到历史收益率最高的最优参数”——用了“最优”这个词。如果 spec 写“扫描参数看看分布”，学员就不会有“啊我找到秘诀了”的错觉，后面再讲过拟合就没冲击力了。**spec 的措辞要服务于章节的节奏。**

完整 spec 在 [`specs/spec-06-param-scan.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-06-param-scan.md)——复制给 AI，弹窗选「允许」。

AI 助手执行完毕后，你的 notebook 里会出现一组数字和一张柱状图。AI 帮你做了这些事：从 MA5 到 MA120，每隔 5 天取一个参数，逐个运行均线回测并记录收益率，一共 24 次——就像考试出了 24 套卷子，让同一个策略换不同的均线参数去做，看哪套得分最高。然后从中找出收益率最高的“冠军参数”，并画成柱状图。

参数扫描结果如图 1-8 所示。柱状图中每根柱子代表一个参数的收益率。绿色柱子 = 正收益，红色柱子 = 负收益，金色高亮那根就是“冠军”——24 个参数里收益率最高的。灰色虚线是简单定投的水平线，让你一眼看出哪些参数跑赢了简单定投，哪些跑输了。黑色实线是零线——在它下面的参数连本都没保住。

![参数扫描结果](images/q1-how-to-profit/06-param-scan.png)

### 读懂参数扫描的结果

先看打印出来的数字。最优参数的收益率是多少？和简单定投比，提升了多少？你大概率会看到一个相当漂亮的数字——看起来我们真的“找到了盈利的秘诀”。

但是——请你仔细看那张柱状图的形状。

如果真的存在某个“最优均线周期”的内在规律，你会期望柱状图呈现一个“山峰”形状：最优参数附近的参数表现应该也不差，离最优越远表现越差，像一座钟形山丘，顶点就是最优值。这说明“最优参数附近就是甜蜜区”，你稍微偏一点也没关系。

但实际的柱状图是这样吗？大概率不是。你看到的更像是高高低低、毫无规律的锯齿形状。MA35 可能很好，MA40 突然变差，MA45 又回来了。相邻的两个参数——仅仅差 5 天——表现可能天差地别。金色那根“冠军”柱子的左右邻居，可能一个正收益一个负收益。

这说明什么？

说明这个“最优参数”的“好”，很可能只是碰巧。它恰好在这段特定的历史数据上表现最好，而不是因为它抓住了什么深层规律。如果真的有深层规律，相邻参数不应该差这么多——MA35 和 MA40 只差 5 天的回看窗口，它们看到的趋势信息几乎一样，凭什么一个盈利一个亏损？唯一的解释是：结果对参数极度敏感，而这种敏感性来自数据中的随机波动，不是来自某种稳定的市场规律。

现在回到开头那个问题：你朝墙上射了 24 支箭，然后走到离某支箭最近的地方画了一个靶心。“百发百中”？当然不是——你只是射得够多罢了。24 个参数里，总有一个碰巧在这段时间表现最好，这是统计学的必然，不是你发现了什么秘密。就像你抛 24 次硬币，总有一次正面连续出现最多——你不会因此断定那枚硬币有“特异功能”。

这个现象我们暂时叫它“锯齿现象”——参数和参数之间的差异不是平滑过渡的山峰，而是高低跳动的锯齿。它有一个更正式的名字，但在给它正式命名之前，我们需要一个更严格的方法去验证：这个“冠军”到底是真本事，还是只是 24 次掷骰里运气最好的那一次。

方法很简单——用这个参数从未见过的新数据来测试它。下一节就做这件事。

## 1.5 真相时刻

怎样才算一场“公平考试”？

你不能拿学生复习过的原题来考他——他全都见过，考满分不说明任何问题。要测出真实水平，得出一张他从没做过的卷子。投资策略也一样。上一节我们用全部 5 年的历史数据做参数扫描，找到了“冠军参数”。但这个冠军是在“开卷考”里拿的第一名——它见过所有数据，当然能挑到最好的答案。这不算本事。

要给它一场闭卷考，方法是把数据分成两段。

你手上有大约 5 年的历史数据。我们把前 60% 拿出来——大约 3 年——作为**训练集（Training Set）**，用来“复习”，也就是做参数扫描、找最优参数。剩下的后 40%——大约 2 年——作为**测试集（Test Set）**，这是“考试卷”。训练集里发生的事，测试集完全不知道；测试集里发生的事，训练的时候完全没看过。两段数据，互不相见。

考试的类比可以帮你记住这个逻辑：你手上有 5 年的真题。你用前 3 年的真题来复习——反复做、反复总结、找到最顺手的解题方法。然后你用后 2 年的真题来“考试”——这 2 年的题你在复习的时候完全没碰过。如果你是真的理解了规律，换一张新卷子也能考好。如果你只是背了前 3 年的答案，新卷子就会露馅。

> 📌 小词典：什么是训练集 / 测试集？
>
> **训练集（Training Set）**：用来“复习”的那段数据。
>
> **测试集（Test Set）**：用来“考试”的那段新数据，模型训练时完全没见过。
>
> 核心约束：两段数据互不交叉。否则就是开卷考。

为什么是 60/40？没有什么魔法数字。训练集不能太少，否则“复习”不充分，找到的参数不可靠；测试集也不能太少，否则“考试”的题量不够，结果说明不了问题。60/40 是一个常见的折中：前面大约 3 年够做一轮完整的参数扫描，后面大约 2 年够看出趋势。这不是完美的方法——真正的未来当然不可知，我们只是用“后来发生的事”来假装未来。但它是我们能做到的最公平的模拟。

接下来的实验就是这个思路：在训练集上重新做参数扫描，找到训练集的“冠军参数”。然后拿着这个冠军参数去测试集上跑一遍——看看它能不能通过“考试”。同时我们在测试集上也跑一遍简单定投作为对照。为什么要对照？因为简单定投不做任何参数优化，它就是“什么都不想”的基线。如果你精心优化的参数连“什么都不想”都赢不了，那优化的意义何在？

### 动手实验 7：样本外测试

这份样本外测试 spec 是本章压轴的一份——它把数据按时间切成两段，让上一节找到的“冠军参数”在没见过的新数据上重新考一次。我们一起把它写出来，重点是**数据 split 的可复现写法**和**怎么把三个数字摆在一起做对比**。

#### 起草上下文 + 任务

> **上下文**：在 `q1-strategy.ipynb` 中已有 `df`、`backtest_dca`、`backtest_dca_with_ma`、参数扫描代码（MA5 到 MA120）。
>
> **任务**：把数据按时间前 60% / 后 40% 分为训练集 / 测试集，验证「最优参数」在未见过的新数据上是否仍然有效。

#### 起草要求：split 可复现 + 三栏对比

> **要求**：

1.  数据分割：**按时间前 60% 为训练集，后 40% 为测试集**（按时间顺序切，不打乱）
2.  在训练集上扫描参数（MA5 到 MA120 步长 5），找到训练集上的最优参数
3.  用该最优参数在**测试集**上跑回测
4.  同时在**测试集**上跑简单定投作为对照
5.  对比三个收益率：训练集最优 / 测试集该参数 / 测试集简单定投

> 📌 **要点**：数据 split 的写法直接决定可复现。“按时间前 60%”是确定的，“随机分 60/40”是不确定的——后者每次跑会得到不同 split，结果就不能 1:1 对照了。**任何切分操作都要写明：是按时间还是随机？切点在哪？随机种子？锁住了你的 spec 才能跨次复现。**
>
> 📌 **要点**：当一份 spec 要核对前一份 spec 的结论时，固定套路是把三个数字摆在一起对比——前一份给出“看似最优”，本份给出“测试集上的真实成绩”+“什么都不做的成绩”。三者一对比，前一份的“最优”到底是真本事还是巧合，一目了然。三个数字不是随便凑的：训练集最优是“你以为”，测试集该参数是“其实”，测试集简单定投是“什么都不做”——三者放一起，过拟合就藏不住了。

#### 起草结果呈现

> **结果呈现**：

1.  打印训练集 / 测试集时间范围和条数
2.  三个数字并排打印：训练集最优参数 + 收益率 / 测试集上该参数的收益率 / 测试集上简单定投的收益率
3.  对比结论双分支：
    - 最优参数在测试集上比简单定投还差：打印「这就是过拟合——你在历史数据上找到的是巧合，不是规律」
    - 否则：打印「结果看起来还行，但仍需谨慎，样本量有限」

完整 spec 在 [`specs/spec-07-overfitting.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-07-overfitting.md)——复制给 AI，弹窗选「允许」。

AI 助手执行完毕后，你的 notebook 里会出现一组关键数字。这可能是本章最重要的一组数字——请你做好心理准备。

AI 帮你做了这些事：把数据按时间顺序在第 60% 的位置一刀切——前面是训练集（“复习用的真题”），后面是测试集（“考试当天才看到的新题”）。注意数据是按时间顺序切的，不是随机打乱的，因为在现实中你只能用过去的数据做决策，未来的数据还没发生。然后在训练集上重新做参数扫描找到冠军参数，拿着这个冠军参数去测试集上跑一遍（“正式考试”），同时在测试集上也跑一遍简单定投作为对照。

### 读懂样本外测试的结果

现在看看这三个数字。

第一个数字：训练集上最优参数的收益率。这个数字大概率很漂亮——毕竟它是 24 个参数里挑出来的冠军，在训练集上当然表现最好。记住，这是“开卷考”的成绩。

第二个数字：测试集上用同一个参数的收益率。这是“闭卷考”的成绩。

第三个数字：测试集上简单定投的收益率。这是“什么都不想”的成绩。

把第二个数字和第三个数字放在一起看。

大概率你会看到这样的结果：冠军参数在测试集上的收益率，比简单定投还差。

请你再读一遍这句话，确认你没有看错。你花了大量精力做参数扫描，从 MA5 到 MA120 穷举了 24 个参数，在训练集上找到了表现最好的那个。然后你拿着这个精心挑选的冠军参数去面对“未来”——它不仅没有比简单定投更好，反而更差了。

换句话说：**你精心优化的策略，还不如什么都不做。**

这不是你的操作有误，也不是代码有 bug。这就是过拟合的真实面目。

在训练集上，你的冠军参数看起来无懈可击——它是 24 个候选人里的第一名，收益率比第二名还高出不少。但到了测试集上，它差到几乎像换了一只策略。它在训练集上的好成绩不是因为它抓住了市场的某种深层规律，而是因为它恰好适配了那段特定数据的特定走势。换一段数据，这种适配就不存在了。

为什么会这样？我从三个角度帮你理解。

**从统计的角度看。** 24 个参数里挑最好的那个，就像在教室里随便拉 24 个人，选最高的那个。他身高 185cm——但这能代表这 24 个人的平均身高吗？当然不能。他只是碰巧最高的那个。你选的参数越多，“碰巧最好”的那个看起来越好——因为你给了随机波动更多的机会来制造“看起来很厉害”的假象。这种现象叫选择偏差（Selection Bias）：当你从一堆结果里挑最好的那个，你挑到的往往不是“真正最好”的，而是“运气最好”的。

**从市场的角度看。** 前 3 年和后 2 年的市场环境不同。也许前 3 年是一轮漫长的熊市，价格一路下跌；后 2 年是触底反弹，价格逐步回升。或者反过来。一个参数在熊市中表现好——比如 MA60，因为它足够迟钝，帮你躲过了大部分下跌——但到了牛市，这种迟钝反而成了累赘：市场都涨起来了，MA60 还在犹豫要不要买入，白白错过了一大段涨幅。市场不是一成不变的。一件衣服不可能四季都合适——冬天穿着暖和的羽绒服，夏天穿就热死人。你在前 3 年找到的“最合身的衣服”，到后 2 年换了季节，哪里都不对。

**从直觉的角度看。** 最优参数就是为那段训练数据“量体裁衣”的产物。量体裁衣做出来的西装确实完美合身——但那是为你一个人做的。换一个身材不同的人穿，哪里都不对劲。你的冠军参数也是一样：它是为 2021 年到 2023 年某段特定行情“量身定做”的，每一个均线周期、每一次买入跳过，都恰好踩在了那段数据上。换一段数据——哪怕只是后面紧接着的两年——整套策略就不再合身。

这就是过拟合——量化交易最大的敌人。

它不是一个小 bug，不是代码写错了一行，修一修就好。它是一种系统性的思维陷阱。只要你在历史数据上做优化，过拟合的幽灵就始终在你身边。你测试的参数越多，优化的维度越多，它就越危险——因为你给了“碰巧”更多的生存空间。

每一个做量化的人，不管是刚入门的新手还是华尔街的资深研究员，都必须时刻警惕它。在历史数据上看到的任何“好结果”，在被样本外测试验证之前，都不值得相信。训练集上的冠军可能只是考试原题背得最熟的那个学生，真正的考试一来就露馅了。

你在本章走过的路——从简单定投、到加入均线判断、到参数扫描找最优、再到样本外测试看清真相——正是每一个量化新手都会经历的完整旅程。区别只在于：有些人在“找到最优参数”那一步就停下来了，兴冲冲地拿去实盘交易，然后亏了真金白银才明白过拟合是什么。而你，在投入一分钱之前，就已经看到了真相。

记住这个教训：**历史数据上的“最优”，不等于未来的“可用”。** 在量化交易的世界里，怀疑永远比自信更值钱。

## 1.6 本章总结

回头看看我们走过的路。从最简单的定投开始，到加入均线判断做择时，到参数扫描寻找“最优解”，再到样本外测试看着精挑的参数全线衰减——7 个实验，一步一步，你经历了一次完整的认知升级。这不只是学了几个策略，而是亲眼见证了“看起来有效”和“真的有效”之间的巨大鸿沟。

### 概念速查表

本章涉及的核心概念汇总如表 1-2 所示。

**表 1-2 第 1 章核心概念速查**

| 概念 | 含义 | 类比 |
|----|----|----|
| 回测（Backtest） | 用历史数据模拟策略执行 | 在脑海中“重播”历史 |
| 基准（Benchmark） | 用来比较的参照物 | 全班的平均分 |
| Beta（贝塔） | 承担市场风险获得的收益 | 坐电梯——市场涨你跟着涨 |
| Alpha（阿尔法） | 超越市场的那部分收益 | 爬楼梯——靠自己的本事往上走 |
| 均线（Moving Average） | 过去 N 天价格的平均值 | 连续几天的平均体温 |
| 过拟合（Overfitting） | 把数据中的噪音当成规律 | 背答案 vs 理解规律 |
| 训练集（Training Set） | 用来“复习”和找参数的那段数据 | 复习用的真题 |
| 测试集（Test Set） | 模型从未见过、用来检验的那段数据 | 没做过的考试卷 |

### 策略进化路径

第 1 章 3 个版本的策略进化与教训如表 1-3 所示。

**表 1-3 第 1 章策略进化路径**

| 版本 | 策略            | 教训                                         |
|------|-----------------|----------------------------------------------|
| v0.1 | 简单定投        | 纯 Beta 暴露，收益完全随市场涨跌             |
| v0.2 | 定投 + 均线择时 | 加入判断是双刃剑，不保证比无脑定投更好       |
| v0.3 | 参数扫描优化    | 历史最优不等于未来可用，过拟合的风险就在面前 |

### 飞轮第一圈：你刚刚走完的路

回头看：你已经把飞轮的四层都各自踩过一次。**候选层**——你选了沪深 300ETF；**组合层**——你定了“每月买一份”或“上穿均线再买”的规则；**执行层**——回测引擎替你按规则下单；**归因层**——你比了三种策略的累计收益、对照了样本内/外。

更重要的是：**做、看、疑**三个动作你都练了一遍。前言里那句口号——“**做要规则、看要指标、还要怀疑你的指标**”——你不是听别人讲，而是亲眼看见冠军参数在测试集上现原形之后才真正信的。**这就是为什么本书把“疑”和“做”“看”放在同等位置**：会做、会看，但不会疑，最容易在某些时候把赚到的钱加倍还给市场。

### 本章最重要的收获

说实话，我们并没有学会一个能稳定盈利的策略。定投跑不赢市场，加了均线也未必更好，参数优化更是一脚踩进了过拟合的坑。如果你期待的是“学完第 1 章就能盈利”，那确实要让你失望了。

但你学会了一样比任何策略都值钱的东西——**怀疑**。面对任何一条漂亮的回测曲线，你现在知道第一个要问的问题是：“这是规律，还是巧合？”这种本能的警觉，会让你在未来少走很多弯路，少亏很多真金白银。

### 带走的问题

本章带你跑通了第一个完整策略，但也留下了一些更深的问题。带着它们继续往下走：

- **只买沪深 300 够吗？** 本章 7 个实验全在拿沪深 300 做样本，但真正投资时不会只买一只。下一章我们要选 3 只 ETF，组成一个真正的“投资宇宙”。——第 2 章会解答。
- **怎么区分运气和能力？** 一个策略盈利了，到底是真有水平还是瞎猫碰上死耗子？——第 5 章、第 6 章会解答。
- **有没有不容易过拟合的策略？** 既然参数优化这条路走不通，还有别的路吗？——第 4 章、第 6 章会解答。
- **怎么知道策略在未来还能盈利？** 样本外测试只是第一步，还有更严格的检验方法吗？——第 5 章会解答。

答案都在后面。继续。

> 💡 **提示**：本章正文聚焦“亲手跑通最小闭环”。「拓展阅读」给了两段世界观铺垫——读不读都不影响主线，但读完会让后面几章吸收得更快。
>
> 💡 **提示**：本章所有代码的可运行版本见 `notebooks/q1-strategy.ipynb`。

## 拓展阅读

本章正文聚焦“亲手跑通最小闭环”。下面两节是动手之前可以省略的视野铺垫——读完正文后回头看，反而吸收得更快。

### 你做的是哪种交易？

“量化”、“程序化”、“算法”、“高频”——这四个词常被混用，其实是层层包含的关系，如图 1-9 所示。

![四个概念层层包含](images/q1-how-to-profit/quant-concepts-nested.png)

四个概念的具体对照如表 1-4 所示。

**表 1-4 量化 / 程序化 / 算法 / 高频四个概念对照**

| 概念 | 干什么 |
|----|----|
| **量化交易**（最外层） | 用数据和模型做决策（对面是靠经验直觉的“主观交易”） |
| **程序化交易** | 用程序自动执行——你完全可以做量化但手动执行，很多个人量化交易者就这么做 |
| **算法交易** | 解决“怎么买”——把大单拆小单降低市场冲击 |
| **高频交易**（最内层） | 毫秒级速度，专用机房和光纤，门槛千万美元起 |

**“投资”与“交易”也是两件事**：投资买的是公司、赚的是公司成长的钱；交易买的是筹码、赚的是别人犯错的钱。本课程偏交易，不分析公司成长。

**本课程做的是中低频量化交易，手动或半自动执行**——持仓周期周到月级别，普通电脑就能跑。你的对手不是 Citadel 这种顶级机构，而是没有系统、靠感觉交易的散户。在这个层面上，纪律严明的量化策略已经是巨大优势。

### 钱从哪来，为什么大多数人赚不到

不管你买什么，赚到的钱本质上**只来自两类**——前言里我们用过同样的分法：**资产创造**（你拿到的是资产本身在帮你赚钱）和**价格变化**（你赚的是别人愿意付的差价）。两类的具体来源如表 1-5 所示。

**表 1-5 投资收益的两大类来源**

| 大类 | 具体来源 | 核心本质 | 关键特征 |
|----|----|----|----|
| **资产创造** | 分红 | 公司把利润分给股东 | 不依赖市场情绪（股票、ETF） |
| **资产创造** | 利息 | 借出资金的报酬 | 不依赖市场情绪（债券、国债） |
| **价格变化** | 价差 | 低买高卖的差额 | 赌价格变化方向（股票、ETF、期货） |
| **价格变化** | 套利 | 不同市场间的瞬时差价 | 几乎不预测方向，门槛高（跨市套利） |

一句话总结：**分红和利息赚的是“现金流”，价差赚的是“共识差”，套利赚的是“空间差”。** 本课程的策略主要赚价差。

**短期负和、长期正和。** 每笔交易都要付佣金、印花税、买卖价差——频繁交易等于把自己放进一个“整体必输”的游戏。但长期持有指数级资产能分享企业增长，蛋糕在变大、大家一起赚。这就是格雷厄姆那句“**短期是投票机，长期是称重机**”。本课程定位“中频”——既捕捉规律，又不被成本吃掉。

**三个差距，量化的三种对抗。** 虽然市场整体可以赚钱，但大多数散户没赚到——核心是三个“差”，恰好对应量化的三种优势（如表 1-6 所示）：

**表 1-6 三个差距与量化的对应优势**

| 普通人的劣势 | 体现 | 量化的对应优势 |
|----|----|----|
| 信息差 | 你看到新闻时，机构早已先动 | 同时扫描几千标的找统计规律 |
| 情绪差 | 涨了就追买、跌了就吓得抛掉，本能在股市害人 | 规则写好机器执行，无情绪干扰 |
| 成本差 | 频繁操作累积成本，可能吃掉一半本金 | 明确规则约束，不频繁操作 |

量化不能保证赚钱，但能让你在更公平的位置上竞争。

---

## 反馈与读者群

如果你发现本章有错别字、概念不清或实验结果不一致，欢迎
[提交 GitHub Issue](https://github.com/xingwudao/xquant-beginner/issues/new/choose)。

课程、正式书和后续更新见
[xquant.shop/courses](https://xquant.shop/courses)。

添加作者微信 `xquanter` 进入读者群。请备注：`XQuant 读者`。
