第 11 章:参数优化与稳健性检验¶
量化策略通常包含若干参数(如均线周期、止损阈值)。参数的选择对策略表现有着决定性影响。本章将探讨如何通过科学的参数优化 (Parameter Optimization) 寻找最优解,并深入分析过拟合 (Overfitting) 的成因与防范措施。
11.1 参数优化理论¶
11.1.1 目标函数 (Objective Function)¶
参数优化的本质是一个数学规划问题:
其中:
- \(\theta\):策略参数向量。
- \(\Theta\):参数搜索空间。
- \(D_{train}\):训练集数据(样本内数据)。
- \(f\):目标函数,通常为夏普比率、卡玛比率或净利润。
11.1.2 搜索算法¶
- 网格搜索 (Grid Search):穷举所有可能的参数组合。
- 优点:能找到全局最优解(在离散网格上)。
- 缺点:计算量随参数数量指数级增长(维数灾难)。
- 随机搜索 (Random Search):在参数空间内随机采样。
- 优点:在高维空间中效率通常高于网格搜索。
- 遗传算法 (Genetic Algorithm):模拟生物进化过程,通过变异和交叉寻找最优解。适合非凸优化问题。
11.2 过拟合:量化交易的隐形杀手¶
过拟合 (Overfitting) 指策略在样本内 (In-Sample) 表现极佳,但在样本外 (Out-of-Sample) 迅速失效的现象。
11.2.1 统计学原理¶
过拟合的本质是多重假设检验 (Multiple Hypothesis Testing) 的谬误。 假设我们在随机生成的噪声数据上测试 100 组参数,即使没有任何真实规律,我们也大概率能找到一组在 95% 置信水平下“显著有效”的参数。
这意味着,尝试的参数越多,找到“伪规律”的概率就越大。这被称为数据窥探偏差 (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)¶
模拟真实交易中“定期重新优化”的过程。
- 在 \(T_0 \sim T_1\) 优化参数,在 \(T_1 \sim T_2\) 使用该参数交易。
- 在 \(T_1 \sim T_2\) 重新优化参数,在 \(T_2 \sim T_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?¶
- 传统 K-Fold:由于时间序列的相关性,导致信息泄露。
- 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) 的影响。
其中基准夏普 \(SR_{benchmark}\) 不再是 0,而是根据尝试次数 \(K\) 和由于尝试次数增多而导致的预期最大夏普比率 \(E[\max(SR)_K]\) 计算得出的。
这意味着:尝试的次数越多,你就应该要求越高的夏普比率,才能确信这不是运气。
11.7 高级优化算法¶
除了简单的网格搜索,量化领域还常用以下高级算法:
11.7.1 遗传算法 (Genetic Algorithm, GA)¶
模拟生物进化过程,适用于参数空间巨大且非凸的优化问题。
- 种群初始化:随机生成 \(N\) 个策略个体(参数组合)。
- 适应度评估:回测每个个体,计算夏普比率作为适应度。
- 选择 (Selection):优胜劣汰,保留高夏普的个体。
- 交叉 (Crossover):交换两个父代个体的参数片段,生成子代。
- 变异 (Mutation):随机微调某些参数,引入多样性,防止陷入局部最优。
- 迭代:重复步骤 2-5,直到满足停止条件。
11.7.2 贝叶斯优化 (Bayesian Optimization)¶
适用于计算昂贵的优化问题(例如每次回测需要 1 小时)。
它通过构建一个代理模型 (Surrogate Model)(通常是高斯过程 Gaussian Process)来拟合目标函数 \(f(\theta)\)。
- 采集函数 (Acquisition Function):根据代理模型的预测值和不确定性,决定下一步去哪里采样(尝试哪组参数)。它在开发 (Exploitation)(去已知表现好的地方)和 探索 (Exploration)(去未知的地方)之间通过算法进行平衡。
贝叶斯优化通常能比网格搜索快 10-100 倍找到全局最优解。
11.8 目标函数的选择¶
优化什么指标,决定了你会得到什么样的策略。
- 最大化夏普比率:追求风险调整后收益。最常用。
- 最大化净利润:追求绝对收益。容易导致高波动、大回撤的策略。
- 最大化卡玛比率:追求低回撤。适合风险厌恶型资金。
- 多目标优化 (Multi-Objective Optimization):寻找帕累托前沿 (Pareto Frontier)。例如,同时追求高收益和低回撤。这种方法不会给出一个“最优解”,而是一组“非劣解”,供基金经理根据当前的市场观点和风险偏好进行选择。
11.9 聚类分析 (Cluster Analysis)¶
De Prado 提出了一种名为 ONC (Optimal Number of Clusters) 的算法,用于从一堆策略中筛选出真正互补的策略。
- 相关性矩阵:计算 \(N\) 个策略回测收益率的相关性矩阵。
- 聚类:将高相关性的策略聚为一类(例如所有的趋势策略聚在一起,所有的反转策略聚在一起)。
- 筛选:在每一类中只保留表现最好的一个策略。
这样构建出来的投资组合,其夏普比率通常远高于简单的等权组合,因为我们真正实现了风险分散。
11.10 回测长度与显著性 (Minimum Backtest Length)¶
你需要多少年的数据才能证明策略有效?
简单来说,策略的夏普比率越低,所需的验证时间就越长。
- 夏普 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 个失败的尝试。
本章小结: 参数优化是一把双刃剑。它能提升策略表现,也能导致严重的过拟合。量化投资的艺术在于在欠拟合与过拟合之间寻找平衡。记住:宁要模糊的正确,不要精确的错误。