本节摘要:6.1 节解决了"单次回测可信",但没人只跑一次回测。你换了参数、换了区间、换了标的,最后记住的是最好的那份报告——这正是过拟合的入口。QuantDinger 把这部分审查做进了 strategy_evolution 模块(官方文档口径),提供四件工具:walk-forward 滚动窗验证防"单区间参数过拟合",PBO 过拟合概率防"试了很多组参数挑最好",Deflated Sharpe 防"试了很多策略挑最好",Monte Carlo 扰动重采样防"运气好"。本节逐件讲清防什么坑、结果怎么读、怎么配置,最后给一张联合判读表。统计原理在《量化投资方法论》第 14 章《风险控制与绩效度量》与第 15 章《批判性思维与研究纪律》展开,这里只讲操作与读数,末尾附脚本化的读数卡与高频疑问。
先建立一个心理模型。假设一个策略完全没有预测能力,你随机试了五十组参数,每组出一条净值曲线——总会有几组"看起来很好"。这个好是抽样噪声,不是能力。问题在于:看报告的你无法区分"真有能力"与"五十次掷硬币里最幸运的一次"。
四件套就是把这件事量化:
| 工具 | 防的坑 | 一句话读法 |
|---|---|---|
| walk-forward | 参数只在一段历史上最好 | 拼起来的样本外业绩还站得住吗 |
| PBO | 试了 N 组参数挑最好 | 挑选过程本身过拟合的概率有多大 |
| Deflated Sharpe | 试了 N 个策略挑最好 | 校正多重检验后还显著为正吗 |
| Monte Carlo | 交易顺序带来的运气 | 坏分位情形下回撤是否可承受 |
四道锁不是并列选项,是层层递进的审查:先看时间上的稳健(walk-forward),再看挑选过程(PBO、Deflated Sharpe),最后看路径依赖(Monte Carlo)。
思路:把历史切成连续的"训练窗+测试窗"。在每个训练窗内选参数,选好的参数立刻在紧随其后的测试窗上跑一遍;窗口右移,重复。训练窗内怎么优化都行,测试窗只记录、不调整:
walk-forward 滚动示意(4 折示例) [ 训练1 ][测1] [ 训练2 ][测2] [ 训练3 ][测3] [ 训练4 ][测4] 测试段 1~4 首尾相接拼起来 = "参数从未见过未来数据"的样本外业绩
怎么读:只看拼接后的测试段指标。测试段与训练段的落差是正常现象(训练段总更好),要警惕的是断崖——测试段夏普掉到零附近甚至为负,说明参数紧紧贴着历史噪声。另一个信号是逐折参数剧烈跳变:最优参数每折换一个值,往往说明参数面平坦而噪声主导,策略对参数不具稳健性(判读思路详见《量化投资方法论》第 15 章《批判性思维与研究纪律》)。
PBO(Probability of Backtest Overfitting)审查的是"挑选"这个动作本身。实现思路(组合对称交叉验证,CSCV,原理见《量化投资方法论》第 14 章):把整段回测切成若干子块,对称地组合出一批"训练集/测试集"划分;每次划分里,看训练集上排名最高的配置在测试集上的排名。如果"训练最优"在测试集上频繁落到后半段,说明你的挑选过程与抛硬币无异。
怎么读:PBO 是 0 到 1 之间的概率。接近 0.5 表示挑选结果与随机无异;越接近 0 越好。经验起步门槛:低于 0.2 值得继续,高于 0.3 建议推倒重来(经验建议值,非官方标准)。PBO 高的常见原因:参数组合试得太多而数据太短、多个参数维度同时网格搜索。对策不是"再优化一轮",而是减少参数数量、拉长数据、或接受这个策略不成立。
PBO 审参数,Deflated Sharpe(DSR)审策略。你本月写了十个策略,挑出夏普最高的那个——即便十个策略都是噪声,最高的那个也会显得不错。DSR 按你尝试的次数、各次夏普的离散度、收益分布的偏度与峰度,把"观测到的最高夏普"折算成"真实能力为正的概率"(Bailey 与 López de Prado 等人的方法,推导见《量化投资方法论》第 14 章)。
怎么读:报告中通常给出 DSR 概率值。经验起步门槛:不低于 0.95 才认为"校正后仍显著"(经验建议值)。特别提醒:试了多少次要如实填——少报尝试次数会让 DSR 虚高,这是自欺报表里最常见的填法错误。
前两件审查"选出来的结果",Monte Carlo 审"路径"。同一个策略、同一组交易,换个成交顺序,最大回撤可能差出一倍。两种常用扰动(模块以官方文档口径支持,名称以文档为准):
怎么读:看报告给出的分位数。重点看 5% 分位的最大回撤与 95% 分位的夏普:若 5% 分位回撤深到你会手动砍仓的地步,说明策略与你的承受力不匹配——这从来不是"再多优化一下"能解决的问题。
一份四件套齐备的 evolution 配置(示意结构,字段名以官方文档为准):
# evolution_breakout.yaml —— strategy_evolution 四件套配置(示意,字段以官方文档为准) strategy: breakout_v2 symbol: BTC/USDT timeframe: 1h walk_forward: train_bars: 2000 # 训练窗长度(示意值) test_bars: 500 # 测试窗长度(示意值) anchor: rolling # 滚动窗(相对 anchoring 而言) pbo: blocks: 16 # CSCV 子块数(示意值,偶数为宜) deflated_sharpe: trials: 40 # 如实填写本轮研究真实尝试的策略数(示意值) monte_carlo: runs: 2000 # 重采样次数(示意值) modes: [trade_shuffle, return_bootstrap] report: confidence_levels: [0.05, 0.50, 0.95]
联合判读表(经验做法,门槛值为示意建议):
| 情形 | walk-forward 测试段 | PBO | DSR | MC 5% 分位回撤 | 结论 |
|---|---|---|---|---|---|
| A | 夏普保持七成以上 | 低 | 高 | 可承受 | 值得进 6.3 压力测试 |
| B | 断崖式下跌 | 高 | — | — | 参数贴噪声,减少参数重来 |
| C | 尚可 | 尚可 | 偏低 | 可承受 | 尝试次数不足信或能力弱,搁置观察 |
| D | 尚可 | 低 | 高 | 深到你拿不住 | 与承受力不匹配,降低目标仓位或放弃 |
注意 D 行:统计上"通过"与心理上"拿得住"是两回事。策略存活的前提是回撤最深时你仍然执行它,这个约束不在引擎里,在你身上。
判读本身也可以落成一段脚本,避免"每次用不同的尺子"(示意实现,门槛为本节经验建议值):
# verdict_card.py —— 四件套联合判读的读数卡(纯标准库,示意实现) def verdict(wf_keep_ratio, pbo, dsr, mc_dd_ok): gates = [ ("WF 测试段指标保持", wf_keep_ratio >= 0.7), # 示意门槛 ("PBO 低于起步门槛", pbo < 0.2), # 示意门槛 ("DSR 校正后仍显著", dsr >= 0.95), # 示意门槛 ("MC 坏分位回撤可承受", mc_dd_ok), # 布尔判据,人工评估 ] passed = [name for name, ok in gates if ok] failed = [name for name, ok in gates if not ok] return passed, failed if __name__ == "__main__": passed, failed = verdict(0.82, 0.14, 0.96, True) # 示意读数 print("通过:", "、".join(passed)) print("未过:", "、".join(failed) if failed else "无")
第四个判据刻意留给人工:回撤"可不可承受"不是统计问题,是你与你的资金、你的睡眠质量的问题——脚本替你统计,不替你承受。读数卡与配置文件一起归档,下次判读同一策略时,尺子不变。
| 疑问 | 简答 |
|---|---|
| 折数与窗长怎么定 | 经验起点:测试段合计样本量要撑得起指标;窗长让每折参数有机会变化(示意) |
| trials 到底怎么数 | 本轮研究里所有"看过结果"的尝试都算,包括放弃的与难看的 |
| MC 两种扰动都要跑吗 | 都要:顺序重排看回撤,收益重采样看指标稳定性,回答的问题不同 |
| PBO 高但 WF 尚可,听谁的 | 听 PBO:挑选过程已被污染,那份尚可的 WF 也在被挑之列 |
| 四件套能证明策略赚钱吗 | 不能;它们只降低"把噪声当能力"的概率,不生产能力 |
四道锁全过的策略,还剩最后一关:它在实验室条件下成立,但你最终要在有摩擦的世界里活着。下一节做成本压力测试——把费率与滑点逐级加码,找出策略死掉的那个点,并用红绿灯清单决定它是否有资格进入 paper 模式。