人机协作迭代回测的正确姿势——以及量化新手的头号死因:过拟合
一个纯离线的参数扫描脚本 scripts/param_sweep.py:它把第 4 课的双均线策略跑 16 组参数(短均线 3/5/8/10 × 长均线 15/20/30/60),严格遵守第 4 课的回测规则(信号收盘产生、次日开盘成交、计入成本),输出每组参数的年化收益与最大回撤。
但表格不是重点。重点是这张表教你看的"地形":参数高原与孤峰的区别。看懂它,你就理解了量化交易里死人最多的那个坑——过拟合,也就明白了"让 Agent 帮你调参"的正确姿势和它的边界。
到这一课,你已经有了数据(第 2 课)、规则约束(第 1 课)、文本分析组件(第 3 课)和第一个回测(第 4 课)。现在把 Agent 正式拉进策略研发流程。正确的分工是一个循环,四步里有两步永远是人干的:
| 步骤 | 谁干 | 干什么 |
|---|---|---|
| 1. 定假设 | 人 | "茅台在震荡市里双均线会被反复打脸"——假设来自你对市场的理解,必须能用一句话说清"为什么它可能赚钱" |
| 2. 写码 | Agent | 把假设翻译成策略代码和回测框架——这是 Agent 的强项,快且不知疲倦 |
| 3. 验证 | 机器 | 跑回测、跑参数扫描,产出数字和表格 |
| 4. 审逻辑 | 人 | 审查:收益从哪来的?有没有未来函数?参数是高原还是孤峰?然后回到第 1 步修正假设 |
新手最常犯的错误是把循环压缩成"Agent 写码 → 看收益数字 → 让 Agent 继续调"。这样跑出来的不是策略,是一台过拟合制造机:Agent 会在你的数据上无限试参,直到数字好看为止——而那些数字恰恰是最不可信的。你花在第 1、4 步的判断力,才是整个流程里唯一不可外包的东西。
Agent 写回测代码最大的风险不是写不出来,而是写得太漂亮——它默认会给你一个"看起来对"的回测:当日信号当日成交(未来函数)、不计成本、只报一个最优收益。所以任务书里必须钉死四条约束,少一条,结果就是废纸:
我要回测一个A股双均线策略:短均线上穿长均线买入,下穿卖出,
标的贵州茅台,数据用 data/600519_qfq_2024.csv(前复权日线)。
硬性约束(违反任何一条视为错误):
1. 禁止未来函数:信号在T日收盘确认,成交只能在T+1日开盘价
2. 必须计入成本:买入0.03%,卖出0.08%(含卖出印花税0.05%)
3. 数据口径:只用前复权价,并在代码注释里写明口径
4. 输出参数敏感性:对短均线[3,5,8,10]×长均线[15,20,30,60]
共16组参数逐一回测,输出年化收益和最大回撤表格,
不许只报"最优参数"的结果
四条约束对应四个死因:约束 1 防回测幻觉(第 4 课亲手踩过的坑);约束 2 防收益虚高(第 1 课:双边摩擦约 0.1%,频繁交易一年磨掉本金四分之一);约束 3 防数据造假(第 2 课:不复权数据在除权日产生假跳水);约束 4 防孤峰欺骗——这正是本课的正题。
脚本是纯离线的,只读第 2 课落地的 CSV,不碰网络(免费接口会挂的道理不用再强调了——能离线就离线)。逐行复制:
cd "$HOME/AI Project/Quant Learning"
source .venv/bin/activate
python scripts/param_sweep.py
你会看到(我已在你的环境实测通过,数字是真实运行结果):
数据:data/600519_qfq_2024.csv,242 个交易日(前复权,除权口径正确)
回测规则:信号收盘产生、次日开盘成交;买入费 0.03%,卖出费 0.08%
表 1:年化收益率(16 组参数)
短\长 | 15 | 20 | 30 | 60
------------------------------------
3 |-22.0% -10.4% -14.1% -3.4%
5 |-19.7% -26.7% -16.0% -9.6%
8 |-14.2% -36.5% -29.6% -21.6%
10 |-24.9% -35.3% -28.3% -12.6%
表 2:最大回撤(16 组参数)
短\长 | 15 | 20 | 30 | 60
------------------------------------
3 |-23.1% -16.4% -16.5% -14.5%
5 |-20.9% -27.2% -21.6% -16.4%
8 |-21.3% -37.8% -30.4% -26.2%
10 |-31.8% -35.9% -27.6% -17.1%
对照:2024 年买入并持有茅台的总收益 = -6.2%
先别管分析,自己盯着表 1 看一分钟。你注意到三件事了吗:① 16 组参数全部亏损;② "最优"的 3/60(-3.4%)恰好蹲在表格边角上;③ 它旁边的格子(5/60、3/30)比它差一大截。这三件事就是本课的核心。
脚本最后会自动做"地形分析",输出如下(真实结果):
========================================================
地形分析:最优参数是孤峰还是高原?
========================================================
样本内最优参数:短均线 3 / 长均线 60,年化 -3.4%
其相邻参数(共 3 组)平均年化:-13.3%
最优格与邻居的差距:+9.9%
判定:孤峰——最优格显著高于周围,参数挪一格收益就塌。
这是过拟合的典型长相:它记住的是 2024 年茅台的噪音,不是规律。
把参数网格想象成一片地形,年化收益是海拔:
| 参数高原 | 孤峰 | |
|---|---|---|
| 长相 | 一片连续的高地:参数在小范围内挪动,收益变化不大 | 一根针:某个参数组合鹤立鸡群,挪一格收益就塌 |
| 含义 | 策略可能捕捉到了某种稳定的市场结构,对参数选择不敏感 | 策略"记住"了这段历史里的几笔偶然交易,对参数极度敏感 |
| 未来表现 | 值得进入样本外数据继续检验(仍可能失败,但可聊) | 几乎可以断定未来失效——噪音不会按原样重演 |
我们这张表里还有第三个信号:最优参数在网格边缘。3/60 的"短均线 3"已经是你扫描范围的最小值——如果它是真规律,为什么更小的值你没扫?边缘最优通常意味着两种可能:真正的"最优"在网格外(你的假设方向就错了),或者它纯粹是噪音。两种都不是好消息。
16 组参数全亏,买入持有也才 -6.2%。说明 2024 年的茅台是震荡下行市,双均线这类趋势策略在这里没有可捕捉的结构——参数扫描救不了一个错误的前提。这就是四步循环第 4 步"人审逻辑"该下的结论:不是"把 3 改成 2 再扫一遍",而是"这个假设在这只标的上不成立,回到第 1 步"。
现在把逻辑钉死。你扫描的数据叫样本内(in-sample);策略未来要面对的行情叫样本外(out-of-sample)。核心事实是:只要你试的参数组合足够多,总有一组会在样本内显得"最优"——哪怕数据是纯随机数。这不是策略发现了规律,是多重检验下的必然假象。r/algotrading 社区关于过拟合的讨论里,这几乎是每个新手帖的标配剧情(见延伸阅读)。
工程上的标准做法(本课只建立概念,后续课程实操):
概念出处:Marcos López de Prado 在 《The Deflated Sharpe Ratio》中用数学证明了"回测次数越多,样本内高夏普比率越可能是假的",是这一话题的经典文献(前瞻阅读,不求全懂)。
回到人机分工。这节课的扫描脚本,从想法到实测通过,如果用 Agent 辅助编写大约是十分钟的活——手写可能要一个下午。这就是 Agent 的真实价值:它把"写码"和"跑数"的成本压到接近零,让你可以在四步循环里转得快得多。原来一周验证一个假设,现在一天验证五个。
但请注意循环变快之后,瓶颈挪到了哪里:挪到了第 1 步的假设质量和第 4 步的审查深度。Agent 不会告诉你"双均线用在震荡下行单票上前提就不成立"——它没有你的持仓、你的市场体感、你的风险承受力,它只对"把代码写对"负责。把第 1、4 步也让渡给 Agent 的人,得到的不是量化策略,是一份样本内精美的亏损说明书。对你的实盘目标来说,记住这句话:Agent 决定你迭代得多快,你决定迭代的方向对不对。
打开scripts/param_sweep.py,把SHORT_WINDOWS和LONG_WINDOWS换成你自己的猜测(比如短 [5,10,20]、长 [30,60,120]),重跑。然后回答自己两个问题:新网格里出现了高原吗?如果有"最优参数",它在网格内部还是边缘?把你观察到的情况贴给我,我们一起判断它是不是孤峰。
凭记忆作答,答错会显示解析。
第 8 课 · 执行闭环架构:策略验证通过之后怎么落地?vn.py / QMT 两条实盘路径怎么选,风控(仓位、止损、熔断)如何装进系统——从"回测好看"到"实盘敢用"的最后一公里。
有任何不清楚的地方(比如地形分析的判定逻辑、样本外怎么切、Prompt 模板怎么改),直接问我——我是你的老师。