12. 回测系统搭建(下):绩效指标计算、过拟合检测与蒙特卡洛模拟

上一章我们把回测引擎的骨架搭好了,能跑出交易信号和资金曲线。但说实话,光有曲线远远不够——你得知道这条曲线到底好不好,靠不靠谱。

我见过太多人,看到回测曲线漂亮就兴奋得不行,结果实盘一跑就崩。为什么?因为没做绩效评估,更没做过拟合检测。

这一章,咱们就把剩下的三块硬骨头啃下来:绩效指标计算过拟合检测蒙特卡洛模拟评估

回测系统评估 绩效指标计算 过拟合检测 蒙特卡洛模拟 夏普比率 最大回撤 胜率 Walk-Forward 样本外测试 稳定性检验 路径生成 置信区间 压力测试

12.1 绩效指标:别只看收益率

回测跑完了,第一件事就是算绩效。但很多人只盯着「总收益率」看,这其实是个大坑。你想想看,一个策略年化50%,但最大回撤40%,你敢实盘吗?

我个人习惯,每次回测完必看三个核心指标:夏普比率最大回撤胜率。这三个指标组合起来,基本能判断一个策略的「健康度」。

12.1.1 夏普比率

夏普比率衡量的是「每承担一单位风险,能获得多少超额收益」。公式很简单:

Sharpe = (策略年化收益率 - 无风险利率) / 年化波动率

但这里有个细节——计算频率。如果你用日收益率算,年化时记得乘以 sqrt(252)。用周数据就乘以 sqrt(52)。

实战经验:我个人觉得,夏普比率大于1算及格,大于2算优秀。但要注意,夏普比率对尾部风险不敏感——两个策略夏普都是1.5,但一个可能偶尔爆亏20%,另一个回撤很平滑。所以不能只看夏普。

12.1.2 最大回撤

最大回撤,就是从净值最高点到最低点的最大跌幅。这个指标直接反映了策略的「抗压能力」。

我曾经见过一个策略,年化收益30%,夏普1.8,看起来很完美。但最大回撤是45%——这意味着如果你在最高点入场,得忍受账户腰斩的痛苦。实盘时,很少有人能扛住这种回撤。

计算代码很简单:

def max_drawdown(equity_curve):
    peak = equity_curve.expanding().max()
    drawdown = (equity_curve - peak) / peak
    return drawdown.min()

12.1.3 胜率

胜率就是盈利交易次数占总交易次数的比例。但这里有个陷阱——胜率高不代表赚钱

举个例子:一个策略胜率80%,但每次赚1块钱就跑了;亏的时候一次亏10块钱。算下来还是亏的。所以胜率必须结合盈亏比一起看。

指标 计算公式 参考标准
夏普比率 (年化收益 - 无风险利率) / 年化波动率 >1.0 及格,>2.0 优秀
最大回撤 max(峰值 - 谷值) / 峰值 <20% 较好,<10% 优秀
胜率 盈利交易数 / 总交易数 结合盈亏比看,一般40%-60%
盈亏比 平均盈利 / 平均亏损 >1.5 较好,>2.0 优秀

小技巧:我习惯把夏普、回撤、胜率三个指标画成一个雷达图,一眼就能看出策略的短板在哪。比如回撤角缩进去了,说明风控需要加强。

12.2 过拟合检测:Walk-Forward 分析

好了,绩效指标算完了,看起来不错。但别高兴太早——你确定这个策略不是「过拟合」出来的?

说白了,过拟合就是你拿着历史数据反复调参,调出一个完美拟合过去走势的策略。但一到未来数据就失效。这就像考试前背答案,换一套题就懵了。

怎么检测?我推荐Walk-Forward 分析(滚动窗口测试)。

12.2.1 Walk-Forward 原理

Walk-Forward 的核心思想是:把数据分成多个时间段,每次用前一段数据训练(优化参数),用后一段数据测试(验证效果)。然后滚动窗口,重复这个过程。

举个例子:

  • 第1次:用 2018-2019 数据训练,用 2020 数据测试
  • 第2次:用 2019-2020 数据训练,用 2021 数据测试
  • 第3次:用 2020-2021 数据训练,用 2022 数据测试

如果每次样本外测试的结果都还不错,说明策略有泛化能力。如果样本外一塌糊涂,那就是过拟合了。

注意:Walk-Forward 的窗口大小很关键。窗口太短,训练数据不够;窗口太长,又可能包含市场结构变化。我一般用 2 年训练 + 1 年测试,或者 3 年训练 + 半年测试,具体看数据量。

12.2.2 代码实现思路

def walk_forward_test(data, train_years=2, test_months=6):
    results = []
    start = data.index[0]
    end = data.index[-1]
    
    current = start + pd.DateOffset(years=train_years)
    while current < end:
        train_end = current
        test_end = current + pd.DateOffset(months=test_months)
        
        train_data = data[(data.index >= start) & (data.index < train_end)]
        test_data = data[(data.index >= train_end) & (data.index < test_end)]
        
        # 在训练集上优化参数
        best_params = optimize_params(train_data)
        # 在测试集上回测
        perf = backtest(test_data, best_params)
        results.append(perf)
        
        current = test_end
    
    return results

跑完之后,看看每个测试窗口的夏普比率、最大回撤。如果大部分窗口的夏普都为正,且回撤可控,那这个策略就比较靠谱了。

12.3 蒙特卡洛模拟:给策略做「压力测试」

Walk-Forward 测的是时间维度上的稳定性。但还有一个问题:你的回测结果,有多少运气成分?

比如你回测了3年,年化收益20%。但如果把交易顺序打乱,或者随机去掉一些交易,结果会怎样?

这就是蒙特卡洛模拟要做的事。

12.3.1 核心思路

蒙特卡洛模拟,说白了就是「随机重采样」。我们把回测产生的交易记录(每笔交易的盈亏、持仓时间)拿出来,然后随机打乱顺序,重新组合成新的资金曲线。重复几千次,看看结果分布。

如果大部分随机路径都赚钱,说明策略本身有正期望。如果只有少数路径赚钱,那你的回测结果可能就是运气好。

12.3.2 具体步骤

  1. 提取交易记录:从回测结果中,提取每笔交易的盈亏百分比和持仓时间。
  2. 随机重采样:有放回地随机抽取 N 笔交易(N 等于原交易次数),组成一条新的交易序列。
  3. 生成资金曲线:按顺序执行这些交易,计算新的净值曲线。
  4. 重复:重复 1000 次或 5000 次,得到一组资金曲线。
  5. 统计分析:看这些曲线的最终收益分布、最大回撤分布。

我的经验:有一次我做一个期权波动率策略,回测夏普1.6,看起来很稳。但蒙特卡洛模拟一做,发现只有60%的路径是赚钱的——也就是说,有40%的概率会亏钱。后来我仔细检查,发现策略在特定市场环境下表现极差。这个发现帮我避免了一次实盘灾难。

12.3.3 结果解读

模拟完成后,你会得到一组数据:

  • 收益分布:比如90%的路径年化收益在5%-25%之间。如果这个区间包含负数,说明策略有亏损风险。
  • 回撤分布:比如95%的路径最大回撤不超过15%。如果有些路径回撤超过30%,那就要小心了。
  • 夏普比率分布:看看夏普的稳定性。如果夏普波动很大,说明策略表现不稳定。

我一般会画一个直方图,把模拟结果和原始回测结果对比。如果原始结果落在分布的边缘(比如前5%),那基本可以断定——你的回测结果有运气成分。

建议:蒙特卡洛模拟的路径数量,至少1000条。太少的话统计意义不大。我习惯跑5000条,虽然慢一点,但结果更可靠。

12.4 本章小结

这一章我们干了三件事:

  • 算了绩效指标——夏普、回撤、胜率,别只看收益率。
  • 做了过拟合检测——Walk-Forward 分析,看看策略在样本外是否还管用。
  • 跑了蒙特卡洛模拟——随机重采样,看看策略的运气成分有多大。

这三步走完,你对策略的「底细」基本就摸清了。下一章,我们会把这些模块整合起来,搭建一个完整的回测系统框架。到时候,你就能一键跑完回测、算指标、做检测,效率翻倍。

嗯,今天就到这儿。代码部分我建议你动手敲一遍,光看是学不会的。遇到问题随时来问我。


公众号:蓝海资料掘金营,微信deep3321