Files
xquant-beginner/book/q1-how-to-profit.md
T

686 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第 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 上见过的那个“定期定额投资”。每个月发了工资,拿出固定的一笔钱买入,不看涨跌、不做判断,机械地重复。这算量化吗?当然算——“量化”只要满足两个条件就够了:规则明确 + 执行可重复。定投完全够格。
不过,我们定投的不是某只个股,而是 **ETFExchange-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`,获取沪深 300ETF510300.SS)最近 5 年的历史日线数据并可视化。
>
> 📌 **要点**:关键事物要点名到 ID。“沪深 300”不够——A 股有沪深 300 指数(000300.SH)也有跟踪它的 ETF510300.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 获取了沪深 300ETF510300.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 读者`