Skip to content

第 11 章:参数优化与稳健性检验

量化策略通常包含若干参数(如均线周期、止损阈值)。参数的选择对策略表现有着决定性影响。本章将探讨如何通过科学的参数优化 (Parameter Optimization) 寻找最优解,并深入分析过拟合 (Overfitting) 的成因与防范措施。

11.1 参数优化理论

11.1.1 目标函数 (Objective Function)

参数优化的本质是一个数学规划问题:

\[ \max_{\theta \in \Theta} f(\theta | D_{train}) \]

其中:

  • \(\theta\):策略参数向量。
  • \(\Theta\):参数搜索空间。
  • \(D_{train}\):训练集数据(样本内数据)。
  • \(f\):目标函数,通常为夏普比率、卡玛比率或净利润。

11.1.2 搜索算法

  1. 网格搜索 (Grid Search):穷举所有可能的参数组合。
    • 优点:能找到全局最优解(在离散网格上)。
    • 缺点:计算量随参数数量指数级增长(维数灾难)。
  2. 随机搜索 (Random Search):在参数空间内随机采样。
    • 优点:在高维空间中效率通常高于网格搜索。
  3. 遗传算法 (Genetic Algorithm):模拟生物进化过程,通过变异和交叉寻找最优解。适合非凸优化问题。

11.2 过拟合:量化交易的隐形杀手

过拟合 (Overfitting) 指策略在样本内 (In-Sample) 表现极佳,但在样本外 (Out-of-Sample) 迅速失效的现象。

11.2.1 统计学原理

过拟合的本质是多重假设检验 (Multiple Hypothesis Testing) 的谬误。 假设我们在随机生成的噪声数据上测试 100 组参数,即使没有任何真实规律,我们也大概率能找到一组在 95% 置信水平下“显著有效”的参数。

\[ P(\text{至少一次伪显著}) = 1 - (1 - 0.05)^{100} \approx 99.4\% \]

这意味着,尝试的参数越多,找到“伪规律”的概率就越大。这被称为数据窥探偏差 (Data Snooping Bias)

11.2.2 偏差-方差权衡 (Bias-Variance Tradeoff)

  • 欠拟合 (High Bias):模型太简单,无法捕捉市场规律(如:买入持有)。
  • 过拟合 (High Variance):模型太复杂,记住了历史数据的噪音(如:用 10 个参数拟合 100 天的数据)。

11.3 稳健性检验 (Robustness Testing)

为了检验策略是否过拟合,我们需要进行严格的稳健性测试。

11.3.1 样本外测试 (Out-of-Sample Testing)

将历史数据分为训练集 (Train Set)测试集 (Test Set)

  • 原则:训练集用于优化参数,测试集仅用于验证。测试集数据在优化过程中必须严格不可见。
  • 标准:如果测试集夏普比率显著低于训练集(如衰减超过 50%),则存在过拟合。

11.3.2 参数敏感性分析 (Parameter Sensitivity)

优秀的策略应该在参数平原 (Parameter Plateau) 上,而不是参数尖峰 (Parameter Peak) 上。

  • 参数平原:参数微小变化(如均线从 20 变 21),绩效指标保持稳定。
  • 参数尖峰:参数微小变化,绩效断崖式下跌。这通常意味着过拟合。

可以通过绘制参数热力图 (Heatmap) 来可视化参数敏感性。

11.3.3 滚动回测 (Walk-Forward Analysis)

模拟真实交易中“定期重新优化”的过程。

  1. \(T_0 \sim T_1\) 优化参数,在 \(T_1 \sim T_2\) 使用该参数交易。
  2. \(T_1 \sim T_2\) 重新优化参数,在 \(T_2 \sim T_3\) 使用新参数交易。
  3. 拼接所有测试段的资金曲线。

这是检验策略真实生命力的“金标准”。

11.4 AKQuant 优化实战

akquant 提供了 run_grid_search 函数,支持并行计算。

11.4.1 代码示例

"""
第 8 章:参数优化与过拟合 (Optimization & Overfitting).

本示例展示了如何使用 AKQuant 的网格搜索 (Grid Search) 功能来寻找最优的策略参数。
同时,我们也会探讨过度优化带来的风险。

策略逻辑:
- 依然使用双均线策略 (MA_Short vs MA_Long)
- 优化目标:寻找夏普比率 (Sharpe Ratio) 最高的参数组合
    - short_window: [3, 5, 10]
    - long_window: [15, 20, 30, 60]

AKQuant 特性:
- `run_grid_search`: 自动多进程并行回测,极大提高优化效率。
"""

from typing import Any, List

import akquant as aq
import numpy as np
import pandas as pd
from akquant import Bar, Strategy


# 模拟数据生成
def generate_mock_data(length: int = 1000) -> pd.DataFrame:
    """生成模拟数据."""
    np.random.seed(42)
    dates = pd.date_range(start="2020-01-01", periods=length, freq="D")
    prices = 100 + np.cumsum(np.random.randn(length))
    df = pd.DataFrame(
        {
            "date": dates,
            "open": prices,
            "high": prices + 1,
            "low": prices - 1,
            "close": prices,
            "volume": 100000,
            "symbol": "MOCK",
        }
    )
    return df


class OptStrategy(Strategy):
    """参数优化演示策略."""

    def __init__(self, short_window: int = 5, long_window: int = 20) -> None:
        """初始化策略."""
        super().__init__()
        self.short_window = short_window
        self.long_window = long_window
        # 动态设置 warmup_period,确保足够计算最长的均线
        self.warmup_period = long_window + 1

    def on_bar(self, bar: Bar) -> None:
        """收到 Bar 事件的回调."""
        symbol = bar.symbol
        closes = self.get_history(
            count=self.long_window + 1, symbol=symbol, field="close"
        )
        if len(closes) < self.long_window + 1:
            return

        history_closes = closes[:-1]
        ma_short = history_closes[-self.short_window :].mean()
        ma_long = history_closes[-self.long_window :].mean()

        pos = self.get_position(symbol)

        if ma_short > ma_long and pos == 0:
            self.order_target_percent(0.95, symbol)
        elif ma_short < ma_long and pos > 0:
            self.close_position(symbol)


if __name__ == "__main__":
    df = generate_mock_data()

    print("开始运行第 8 章参数优化示例...")
    print("正在进行网格搜索 (Grid Search)...")

    # 定义参数网格
    # 键名必须与策略 __init__ 中的参数名一致
    param_grid = {"short_window": [3, 5, 10], "long_window": [15, 20, 30, 60]}

    # 运行网格搜索
    # max_workers: 并行进程数,默认根据 CPU 核心数自动设置
    # metric: 优化目标指标,默认为 sharpe_ratio
    results: Any = aq.run_grid_search(
        strategy=OptStrategy,
        data=df,
        param_grid=param_grid,
        initial_cash=100_000,
        commission_rate=0.0003,
        max_workers=4,  # 限制为 4 个进程
    )

    print("\n" + "=" * 40)
    print("优化结果 (按夏普比率排序)")
    print("=" * 40)

    # OptimizationResult 对象包含所有参数组合的回测结果
    # 我们可以将其转换为 DataFrame 方便查看
    df_results = pd.DataFrame(results)

    # 按照 sharpe_ratio 降序排列
    # 注意:AKQuant 的结果中,metrics 是一个字典
    # 我们需要展开它

    # 提取关键指标
    summary: List[Any] = []
    for res in results:
        params = res.params
        metrics = res.metrics

        # metrics 可能是 BacktestResult 对象或者字典,视版本而定
        # run_grid_search 通常返回一个包含 params 和 metrics 的轻量级对象
        # 这里假设 metrics 是一个字典,包含 sharpe_ratio 等

        # 实际上 aq.run_grid_search 返回的是 List[OptimizationResult]
        # OptimizationResult.metrics 是一个 PerformanceMetrics 对象或字典

        # 让我们直接打印最优结果
        pass

    # 简单起见,我们直接打印前 3 名
    # run_grid_search 返回的结果通常已经按默认指标排序了 (如果内部实现了的话)
    # 但为了保险,我们手动排序

    # 假设 results 是 List[OptimizationResult]
    # OptimizationResult(params={'short_window': 3, 'long_window': 15}, metrics=...)

    sorted_results = sorted(
        results,
        key=lambda x: x.metrics.sharpe_ratio
        if hasattr(x.metrics, "sharpe_ratio")
        else x.metrics.get("sharpe_ratio", -999),
        reverse=True,
    )

    print(f"{'Short':<6} {'Long':<6} {'Sharpe':<10} {'Return':<10} {'MaxDD':<10}")
    print("-" * 50)

    for res in sorted_results[:5]:
        p = res.params
        m = res.metrics

        # 兼容不同版本的属性访问
        sharpe = getattr(m, "sharpe_ratio", m.get("sharpe_ratio", 0))
        ret = getattr(m, "total_return_pct", m.get("total_return_pct", 0))
        dd = getattr(m, "max_drawdown_pct", m.get("max_drawdown_pct", 0))

        print(
            f"{p['short_window']:<6} {p['long_window']:<6} "
            f"{sharpe:<10.2f} {ret:<10.2f}% {dd:<10.2f}%"
        )

    print("\n" + "=" * 40)
    print("最佳参数组合:")
    best = sorted_results[0]
    print(best.params)

11.4.2 结果分析

运行上述代码后,我们会得到一个按夏普比率排序的参数表。

  • 观察前 10 名:如果前 10 名参数比较集中(如均线都在 20-25 之间),说明策略比较稳健。
  • 观察分布:如果最优参数东一榔头西一棒子,说明策略可能在拟合噪音。

11.5 组合净化交叉验证 (Combinatorial Purged Cross-Validation, CPCV)

这是由 De Prado 提出的目前最先进的回测框架。

11.5.1 为什么需要 CPCV?

  1. 传统 K-Fold:由于时间序列的相关性,导致信息泄露。
  2. Walk-Forward:虽然避免了信息泄露,但只测试了一条历史路径。如果历史重演的方式略有不同,策略可能就失效了。

11.5.2 CPCV 原理

CPCV 将数据切分为 \(N\) 组,每次选取 \(k\) 组作为测试集(共有 \(C_N^k\) 种组合)。在训练集和测试集之间进行净化 (Purging)隔离 (Embargo)

通过这种方式,我们可以生成大量可能的“历史路径”。

  • 路径生成:将所有测试集的预测结果按时间拼接,可以重组出 \(C_N^k\) 条完整的资金曲线。
  • 概率分布:我们可以得到策略夏普比率的分布,而不是单一的数值。这让我们能回答:“在 95% 的概率下,该策略的夏普比率大于 1.0 吗?”

11.6 调整后的夏普比率 (Deflated Sharpe Ratio, DSR)

如果你尝试了 1000 组参数,终于找到了一组夏普比率为 2.0 的参数。这个 2.0 是真实的吗?

Deflated Sharpe Ratio (DSR) 用于修正多重测试偏差 (Multiple Testing Bias)。它在概率夏普比率 (PSR) 的基础上,进一步考虑了尝试次数 (Number of Trials) 的影响。

\[ DSR = PSR(\widehat{SR}, SR_{benchmark}) \]

其中基准夏普 \(SR_{benchmark}\) 不再是 0,而是根据尝试次数 \(K\) 和由于尝试次数增多而导致的预期最大夏普比率 \(E[\max(SR)_K]\) 计算得出的。

\[ E[\max(SR)_K] \approx E[SR] + \sigma_{SR} \sqrt{2 \ln K} \]

这意味着:尝试的次数越多,你就应该要求越高的夏普比率,才能确信这不是运气。

11.7 高级优化算法

除了简单的网格搜索,量化领域还常用以下高级算法:

11.7.1 遗传算法 (Genetic Algorithm, GA)

模拟生物进化过程,适用于参数空间巨大且非凸的优化问题。

  1. 种群初始化:随机生成 \(N\) 个策略个体(参数组合)。
  2. 适应度评估:回测每个个体,计算夏普比率作为适应度。
  3. 选择 (Selection):优胜劣汰,保留高夏普的个体。
  4. 交叉 (Crossover):交换两个父代个体的参数片段,生成子代。
  5. 变异 (Mutation):随机微调某些参数,引入多样性,防止陷入局部最优。
  6. 迭代:重复步骤 2-5,直到满足停止条件。

11.7.2 贝叶斯优化 (Bayesian Optimization)

适用于计算昂贵的优化问题(例如每次回测需要 1 小时)。

它通过构建一个代理模型 (Surrogate Model)(通常是高斯过程 Gaussian Process)来拟合目标函数 \(f(\theta)\)

  • 采集函数 (Acquisition Function):根据代理模型的预测值和不确定性,决定下一步去哪里采样(尝试哪组参数)。它在开发 (Exploitation)(去已知表现好的地方)和 探索 (Exploration)(去未知的地方)之间通过算法进行平衡。

贝叶斯优化通常能比网格搜索快 10-100 倍找到全局最优解。

11.8 目标函数的选择

优化什么指标,决定了你会得到什么样的策略。

  1. 最大化夏普比率:追求风险调整后收益。最常用。
  2. 最大化净利润:追求绝对收益。容易导致高波动、大回撤的策略。
  3. 最大化卡玛比率:追求低回撤。适合风险厌恶型资金。
  4. 多目标优化 (Multi-Objective Optimization):寻找帕累托前沿 (Pareto Frontier)。例如,同时追求高收益和低回撤。这种方法不会给出一个“最优解”,而是一组“非劣解”,供基金经理根据当前的市场观点和风险偏好进行选择。

11.9 聚类分析 (Cluster Analysis)

De Prado 提出了一种名为 ONC (Optimal Number of Clusters) 的算法,用于从一堆策略中筛选出真正互补的策略。

  1. 相关性矩阵:计算 \(N\) 个策略回测收益率的相关性矩阵。
  2. 聚类:将高相关性的策略聚为一类(例如所有的趋势策略聚在一起,所有的反转策略聚在一起)。
  3. 筛选:在每一类中只保留表现最好的一个策略。

这样构建出来的投资组合,其夏普比率通常远高于简单的等权组合,因为我们真正实现了风险分散

11.10 回测长度与显著性 (Minimum Backtest Length)

你需要多少年的数据才能证明策略有效?

\[ MinTRL \approx \frac{1.25}{\widehat{SR}^2} \left( \frac{\Phi^{-1}(1-\alpha) - \Phi^{-1}(\beta) \sqrt{1-\rho}}{1-\rho} \right)^2 \]

简单来说,策略的夏普比率越低,所需的验证时间就越长。

  • 夏普 0.5 的策略,可能需要 50 年的数据才能通过统计检验。
  • 夏普 2.0 的策略,可能只需要 2-3 年的数据。

这意味着,对于低频策略(通常夏普较低),你需要极长的历史数据;而对于高频策略(通常夏普较高),短期的验证可能就足够了。

11.11 “抽屉问题” (The File Drawer Problem)

学术界和业界都存在一种严重的发表偏差 (Publication Bias)

  • 研究员尝试了 100 个策略,其中 95 个失败了,被扔进了“抽屉”。
  • 只有 5 个成功的策略被写进了论文或展示给客户。
  • 由于读者看不到那 95 个失败的案例,他们会误以为这 5 个策略非常完美。

Deflated Sharpe Ratio 正是为了解决这个问题,它要求你在评估那 5 个成功策略时,必须考虑到背后还有 95 个失败的尝试。


本章小结: 参数优化是一把双刃剑。它能提升策略表现,也能导致严重的过拟合。量化投资的艺术在于在欠拟合与过拟合之间寻找平衡。记住:宁要模糊的正确,不要精确的错误