11.过拟合预防与验证
优化是量化交易中最诱人的环节,也是最容易掉进去的陷阱。看着回测曲线一路向上,夏普比率漂亮得惊人,很多人已经幻想着实盘后的美好生活。但残酷的现实是,这些光鲜的数字很可能只是幻觉——过拟合的杰作。
过拟合就像给策略穿上了一件量身定制的紧身衣,在历史数据上完美贴合,但市场稍微一变,衣服就崩开了。Jesse 在设计上已经内置了一些防护机制,比如训练/测试集拆分、偏向稳健性的目标函数,但这些还不够。真正要确认策略的可靠性,需要更系统的验证方法。
三段式验证方法
Jesse 的优化模式虽然会自动拆分训练集和测试集,但专业的做法是主动将数据划分为三个独立时期:训练期、测试期和验证期。这种方法能更彻底地暴露过拟合问题。
为什么需要三段式验证
两段式拆分(训练+测试)有个缺陷:我们在优化过程中会反复查看测试集的结果,然后调整策略思路。这个过程实际上让测试集也参与了"训练",导致它对策略来说不再是完全未知的数据。三段式验证的核心思想是保留一块完全未触碰的"净土",只在最后做一次性的检验。
想象在考试前,训练期是平时的练习题,测试期是模拟考,而验证期才是真正的期末考试。如果策略在期末考试中也能稳定发挥,说明它真的掌握了知识,而不是死记硬背了模拟考的答案。
如何划分数据集
时间跨度决定了划分的粒度。假设有从 2018 年 1 月到 2020 年 1 月共 24 个月的数据,一个合理的划分是:
- 训练期:2018-01-01 到 2019-04-30(16 个月,占 67%)
- 测试期:2019-05-01 到 2019-08-31(4 个月,占 17%)
- 验证期:2019-09-01 到 2020-01-01(4 个月,占 17%)
训练期要足够长,让策略经历不同的市场环境。测试期和验证期各 15-20% 是比较平衡的选择。如果数据量有限,至少保证验证期有 3-6 个月,覆盖一些市场波动。
实战:在Jesse中实施三段式验证
在 Jesse 中实施三段式验证需要手动控制优化和回测的时间范围。先运行优化时使用训练期和测试期:
# 优化配置,使用训练期和测试期
opt_config = {
'starting_balance': 10000,
'fee': 0.001,
'type': 'futures',
'futures_leverage': 2,
}
# 优化命令,时间范围覆盖训练+测试
jesse optimize 2018-01-01 2019-08-31 --config opt_config.py
优化完成后,会得到一系列 DNA(参数组合)。不要急着选表现最好的那个,而是挑选几个在训练期和测试期都表现稳定的参数。然后,用这些参数在验证期上运行普通回测:
# 回测配置,单独使用验证期
backtest_config = {
'starting_balance': 10000,
'fee': 0.001,
'type': 'futures',
'futures_leverage': 2,
}
# 对选中的几个DNA分别运行回测
jesse backtest 2019-09-01 2020-01-01 --dna "参数1"
jesse backtest 2019-09-01 2020-01-01 --dna "参数2"
这个过程的关键是:验证期的数据对策略来说是完全未知的。如果某个参数组合在验证期表现断崖式下跌,说明它严重过拟合了训练期和测试期的特定模式。
解读验证结果
挑选最终参数时,不要只看验证期的绝对收益。一个稳健的策略应该满足:
- 三期表现一致性:训练、测试、验证三期的夏普比率、最大回撤等关键指标波动不大
- 交易次数充足:验证期的交易次数不少于训练期的 30%,确保不是偶然
- 收益来源合理:收益主要来自策略逻辑,而非特定市场的异常波动
如果某个参数在训练期夏普 4.0,测试期 3.5,验证期骤降到 1.2,这就是典型的过拟合信号。相反,如果三期夏普都在 2.5-3.0 之间波动,即使不是最高,也说明策略具有真正的适应能力。
过拟合警告信号
在优化和验证过程中,一些迹象表明策略可能已经过拟合。识别这些信号能帮助我们及时止损,避免将资源浪费在脆弱的策略上。
训练集表现远好于测试集
这是最直接的信号。如果优化结果显示某个参数组合在训练期年化收益 200%,夏普比率 5.0,但在测试期收益降到 30%,夏普 1.0,说明策略只是在记忆训练期的噪音。Jesse 的优化过程会同时评估训练集和测试集,但最终的参数选择还是由我们决定。不要被训练期的漂亮数字迷惑,测试期的表现才是真实能力的体现。
交易次数过少
策略在测试期只有寥寥几笔交易,却贡献了大部分收益。比如训练期有 500 笔交易,测试期只有 20 笔,其中 3 笔大赚。这种情况下,测试期的结果完全是统计偶然。Jesse 提供了 optimal_total 参数可以过滤掉交易次数过少的组合,但人工判断仍然必要。一般来说,测试期至少要有 50-100 笔交易,结果才具有统计意义。
参数值过于特殊
优化得到的参数值看起来毫无规律,比如均线周期是 147,止损倍数是 3.87。这些"精确"的数字往往是为了迎合历史数据的特定波动而微调出来的。真正稳健的策略参数通常是相对"整"的数字,或者在一定范围内变化时表现相对稳定。如果参数稍微变动 1-2%,性能就大幅下降,这个策略的鲁棒性堪忧。
收益好得不真实
策略的年化收益远超市场正常水平,比如在没有杠杆的情况下年化 500% 以上。虽然量化策略可以超越市场,但异常高的收益通常意味着策略捕捉到了历史数据中的特定异常,比如某次黑天鹅事件的逆向操作。检查策略的交易记录,看看收益是否集中在某个特定时期。如果是,那很可能是过拟合。
记住,我们的目标是找到稳健的参数,而不是最优的参数。稳健意味着在不同市场条件下都能有尚可的表现,而不是在特定条件下有极致的表现。
蒙特卡洛概述
当三段式验证完成后,策略看起来还不错,这时还需要更严格的考验。蒙特卡洛分析通过创造大量"平行宇宙",测试策略在不同情境下的表现。如果说三段式验证是期末考试,蒙特卡洛就是模拟各种极端天气条件下的实战演练。
什么是蒙特卡洛分析
蒙特卡洛分析的核心思想是:通过随机改变输入数据或交易顺序,生成成百上千个替代的历史场景,然后观察策略在这些场景下的表现分布。如果策略在原始数据上表现很好,但在大多数模拟场景中表现糟糕,说明它过度依赖历史的特定路径。
Jesse 提供了两种蒙特卡洛方法,分别测试策略的不同方面:
- 交易打乱模拟(
monte_carlo_trades):测试交易时机的重要性 - K线自助模拟(
monte_carlo_candles):测试策略在不同市场条件下的鲁棒性
两种蒙特卡洛方法对比
交易打乱模拟不改变市场数据,只是重新排列交易的顺序。这能回答:策略的成功是因为选对了交易,还是因为交易恰好按有利的顺序发生?如果打乱后业绩大幅下降,说明策略依赖特定的交易序列。
K线自助模拟则改变市场数据本身,通过添加噪声或重采样价格变动,创造新的市场场景。这能回答:策略的成功是因为适应了特定市场模式,还是具有真正的普适性?如果在扰动后的数据上表现不稳定,说明策略对市场噪音过于敏感。
蒙特卡洛如何帮助预防过拟合
过拟合的策略就像只会做一道题的学霸,题目稍微变化就束手无策。蒙特卡洛分析通过生成大量变体数据,暴露策略的脆弱性。如果原始回测结果是所有模拟场景中的异常值(比如比 95% 的场景都好),这反而是危险信号——说明策略可能只是恰好匹配了历史数据的特定路径。
使用蒙特卡洛分析时,我们应该:
- 选择在大多数模拟场景中表现一致的配置,而非仅在原始数据上表现最好的
- 结合两种方法,同时测试时机敏感性和结构鲁棒性
- 在策略开发的早期和调参后都进行测试,确保改进的是真实稳健性而非历史拟合度
交易打乱模拟
交易打乱模拟是蒙特卡洛分析中最直观的方法。它不改变任何一笔交易的结果,只是像洗牌一样改变交易的先后顺序,然后观察对整体业绩的影响。
打乱交易顺序的原理
假设策略在一个月内做了 10 笔交易,收益分别是 +5%、-2%、+8%、-1%、+12%、-3%、+6%、-4%、+9%、-2%。原始顺序下,可能因为前几笔盈利提供了安全垫,后续交易心态更好。但如果顺序变成:-4%、+8%、-3%、+6%、-2%、+12%、+5%、-1%、+9%、-2%,最大回撤和最终收益可能完全不同。
交易打乱模拟会生成上千个这样的随机排列,每个排列都重新计算权益曲线和各项指标。通过比较原始结果在所有排列中的排名,可以判断策略对交易顺序的依赖程度。
代码实现与参数详解
在 Jesse 中实现交易打乱模拟非常简单:
from jesse.research import monte_carlo_trades
results = monte_carlo_trades(
config=config,
routes=routes,
data_routes=data_routes,
candles=candles,
warmup_candles=warmup_candles,
num_scenarios=1000,
progress_bar=True,
benchmark=True,
fast_mode=True,
cpu_cores=None
)
这段代码会启动蒙特卡洛模拟,返回一个包含详细结果的字典。让我们逐个理解参数:
config、routes、data_routes、candles、warmup_candles:这些和普通回测的参数完全一样,定义了策略配置和数据num_scenarios=1000:生成 1000 个打乱场景。场景越多,统计结果越可靠,但耗时也越长。建议至少 1000 次progress_bar=True:显示进度条,方便观察运行状态benchmark=True:包含基准对比,可以看到策略相对于买入持有的表现fast_mode=True:启用快速模式,跳过一些非必要的计算,大幅提升速度cpu_cores=None:自动检测并使用 80% 的可用 CPU 核心。可以手动指定数字来控制资源使用
返回的 results 字典结构很清晰:
# 原始回测结果
original = results['original']
# 所有模拟场景列表
scenarios = results['scenarios']
# 置信度分析
confidence = results['confidence_analysis']
original 包含了完整的原始回测数据,包括指标、交易列表、权益曲线等。scenarios 是一个列表,每个元素代表一个打乱后的场景,包含该场景下的各项指标和权益曲线。confidence_analysis 提供了统计摘要,包括原始结果在各指标上的百分位排名和 p 值。
结果解读:判断交易时机的重要性
拿到结果后,关键是看原始策略在模拟分布中的位置。Jesse 提供了方便的解读方式:
🔀 MONTE CARLO TRADES (trade-order shuffle test)
Simulations: 1000
Metric | Original | Worst 5% | Median | Best 5%
-----------------+----------+----------+--------+--------
Max Drawdown (%) | 10.35% | 28.3% | 16.0% | 10.5%
Sharpe Ratio | 3.28 | 2.74 | 3.08 | 3.44
Calmar Ratio | 21.95 | 2.89 | 5.11 | 7.76
解读这张表时,关注几个关键点:
原始结果的位置:如果原始结果接近或优于 Best 5%,反而是危险信号。这意味着策略业绩高度依赖特定的交易顺序,可能只是运气好。理想情况是原始结果接近 Median,说明即使运气一般,策略也能有不错的表现。
指标的稳定性:看不同指标的表现是否一致。比如夏普比率在 Median 附近,但卡尔马比率远高于 Best 5%,说明策略可能通过承担极端风险换取收益。
最差 5% 的表现:这代表了坏运气下的结果。如果最差 5% 的夏普比率仍然是正的,说明策略的抗打击能力很强。
交易打乱模拟特别适合检验策略的资金管理能力。如果策略使用了复杂的仓位调整或止损止盈逻辑,打乱交易顺序可能会显著影响结果。稳健的策略应该主要依赖交易选择能力,而非交易顺序的运气。
K线自助模拟
如果说交易打乱模拟测试的是策略的"内功"(交易选择),K线自助模拟测试的就是"外功"(适应市场变化)。这种方法通过改变市场数据本身,检验策略在不同市场条件下的表现。
基于K线的市场扰动
K线自助模拟不直接打乱交易,而是先对 K 线数据进行变换,然后在变换后的数据上重新运行完整回测。这种方法能测试策略是否过度依赖特定的价格模式。
Jesse 提供了两种内置的 K 线变换管道:
- 高斯噪声管道:在价格中添加随机噪声,模拟市场微观结构的随机性
- 移动块自助管道:重采样价格变动块,保留短期趋势结构的同时改变整体路径
高斯噪声管道详解
高斯噪声管道通过添加符合正态分布的随机扰动来改变价格,但会保持 OHLC 的合理关系:
from jesse.research.monte_carlo.candle_pipelines import GaussianNoiseCandlesPipeline
pipeline_kwargs = {
"batch_size": 7 * 24 * 60, # 1周的分钟数
"close_sigma": 10.0, # 收盘价噪声标准差
"high_sigma": 5.0, # 最高价噪声标准差
"low_sigma": 5.0 # 最低价噪声标准差
}
参数设置需要一些技巧:
close_sigma应该大致匹配标的资产的平均分钟波动。如果 BTC 平均每分钟波动 10 美元,就设为 10.0。设置过大会创造不现实的价格跳跃,设置过小则测试不够严格high_sigma和low_sigma通常设为 close_sigma 的一半左右,这样能保持真实的影线特征batch_size控制每次处理的 K 线数量。设为一周(7 * 24 * 60 分钟)能在保留短期结构的同时引入足够变化
这个管道会确保变换后的数据仍然满足 High ≥ max(Open, Close) 和 Low ≤ min(Open, Close) 的约束,所有价格保持为正,成交量和时间戳不变。
移动块自助管道详解
移动块自助管道是更推荐的选择,因为它不需要手动调整噪声参数:
from jesse.research.monte_carlo.candle_pipelines import MovingBlockBootstrapCandlesPipeline
pipeline_kwargs = {
"batch_size": 7 * 24 * 60 # 1周的分钟数
}
这个管道的工作原理是:
- 将价格序列划分为重叠的块(块长度由 batch_size 决定)
- 随机抽取这些块并重新组合,生成新的价格路径
- 保持块内的价格关系,但在块边界处允许跳跃
这种方法的优势在于自动保留了市场的短期记忆效应。比如,一个上涨趋势通常会持续几天,移动块自助能维持这种局部结构,同时改变整体趋势的顺序和持续时间。batch_size 越大,保留的短期结构越长;越小,市场状态切换越频繁。
代码实现与最佳实践
完整的 K 线自助模拟代码如下:
from jesse.research import monte_carlo_candles
from jesse.research.monte_carlo.candle_pipelines import MovingBlockBootstrapCandlesPipeline
results = monte_carlo_candles(
config=config,
routes=routes,
data_routes=data_routes,
candles=candles,
warmup_candles=warmup_candles,
num_scenarios=100,
progress_bar=True,
fast_mode=True,
candles_pipeline_class=MovingBlockBootstrapCandlesPipeline,
candles_pipeline_kwargs={"batch_size": 7 * 24 * 60},
cpu_cores=None
)
与交易打乱模拟相比,K线自助模拟的 num_scenarios 可以设得低一些,因为每个场景都要运行完整回测,计算量更大。100-200 个场景通常足够。
返回的结构类似:
# 原始回测结果(scenario_index == 0)
original = results['original']
# 模拟场景列表(排除原始结果)
scenarios = results['scenarios']
# 场景数量
num_scenarios = results['num_scenarios']
每个场景包含完整的回测结果:指标、权益曲线、交易列表等。这让我们能深入分析策略在何种市场条件下表现不佳。
最佳实践建议:
- 先用移动块自助:作为默认选择,避免手动调参的偏差
- 再用高斯噪声:针对特定市场微观结构进行压力测试
- 对比不同 batch_size:测试策略对不同时间尺度市场结构的敏感度
- 结合交易打乱:两种方法一起使用,全面评估策略稳健性
如何阅读蒙特卡洛结果
蒙特卡洛分析会生成详细的统计表格和图表,正确解读这些信息是预防过拟合的关键。
理解结果表格
典型的输出如下:
📊 MONTE CARLO CANDLES (market condition test)
Metric | Original | Worst 5% | Median | Best 5%
----------------------+----------+----------+--------+--------
Net Profit Percentage | 81.8% | 19.0% | 55.5% | 101.7%
Max Drawdown (%) | -10.4% | -5.5% | -3.0% | -1.2%
Sharpe Ratio | 3.28 | 2.64 | 4.49 | 6.46
表格四列的含义:
- Original:策略在真实历史数据上的实际表现
- Worst 5%:1000 次模拟中最差的 50 次结果的平均值,代表坏运气下的表现
- Median:所有模拟结果的中位数,代表典型情况
- Best 5%:最好的 50 次结果的平均值,代表好运气下的表现
解读性能位置
原始结果在分布中的位置揭示了策略的本质:
- 优于 Best 5%:⚠️ 警告:可能过拟合。策略业绩高度依赖特定的历史路径,实盘重复的可能性低
- Median 与 Best 5% 之间:表现不错但需谨慎。策略有一定运气成分,需要监控是否退化
- 接近 Median:✅ 理想范围。策略稳健,不依赖运气。即使市场条件不利,也能获得尚可的结果
为什么接近中位数反而是最好的?因为这说明策略的盈利能力来自核心逻辑而非偶然。在实盘中,市场不会完全复制历史,但也不会永远处于最坏情况。一个在中位数附近的策略,在正常市场条件下很可能表现更好。
关键指标观察
重点关注几个核心指标在模拟中的稳定性:
- 最大回撤:如果 Worst 5% 的回撤远大于 Original,说明策略在不利市场条件下风控不足
- 夏普比率:观察其在模拟中的分布范围。范围越窄,策略越稳定
- 胜率:某些策略在打乱交易后胜率会大幅变化,这通常意味着依赖特定的盈亏序列
- 总交易数:如果模拟中交易数大幅减少,说明策略对市场条件敏感,容易陷入长时间不交易
综合判断
最终决策需要结合两种蒙特卡洛方法:
- 交易打乱模拟显示原始结果接近 Median,说明交易选择能力强
- K线自助模拟显示原始结果也在 Median 附近,说明适应市场变化能力强
如果两者都表现良好,策略的稳健性就得到了双重确认。如果一种方法显示过拟合而另一种没有,需要深入分析原因。比如,策略可能选择了好交易,但这些交易只在特定市场环境下出现。
性能与资源考量
蒙特卡洛分析是计算密集型任务,合理使用资源很重要。
CPU核心利用
默认情况下,Jesse 会使用 80% 的可用 CPU 核心。可以通过 cpu_cores 参数手动控制:
# 使用4个核心,留有余力给系统
results = monte_carlo_trades(..., cpu_cores=4)
交易打乱模拟可以高度并行,因为每个场景的计算是独立的。K线自助模拟的并行度稍低,因为每个场景都要运行完整回测,内存占用更大。
内存管理
大规模数据集会消耗大量内存。如果处理 1 分钟 K 线的多年数据,建议:
- 在
fast_mode=True下运行,减少中间数据存储 - 适当减少
num_scenarios,优先保证场景质量而非数量 - 监控内存使用,必要时分批运行
运行时间参考
在 12 核 CPU、16GB 内存的机器上,测试一个中等复杂度的策略:
- 交易打乱模拟:1000 场景,5 分钟数据 2 年,约 30-60 秒
- K线自助模拟:100 场景,同样数据,约 5-10 分钟
时间主要取决于策略复杂度和数据量。简单策略(只用几个指标)会快很多,复杂策略(多时间框架、动态仓位)会慢一些。
实践建议
将蒙特卡洛分析融入日常工作流:
- 开发初期:策略雏形完成后,先跑 100 次交易打乱模拟,快速检验交易选择能力
- 参数优化后:用三段式验证选出候选参数,然后对它们进行完整的蒙特卡洛测试
- 实盘前:在尽可能长的历史数据上运行蒙特卡洛,确保策略经历过多种市场周期
- 定期复检:市场结构会演变,每季度或每半年重新运行测试,观察策略是否退化
记住,蒙特卡洛分析不是一次性任务,而是持续验证的工具。它不能替代对策略逻辑的理解,但能帮助我们避免被漂亮的回测曲线欺骗。
通过三段式验证和蒙特卡洛分析,我们能更自信地判断策略的真实能力。这些方法虽然增加了工作量,但避免了实盘中的昂贵教训。下一章将深入探讨如何在 Jupyter 环境中使用 Jesse 的研究工具,进行更灵活的数据分析和蒙特卡洛实验。