3、参数调优入门:参数空间、过拟合与欠拟合、回测框架搭建
各位同学,今天我们进入套利策略最核心也最磨人的环节——参数调优。
说实话,我刚入行那会儿,觉得策略写好了就能躺着赚钱。结果呢?回测曲线漂亮得像教科书,实盘一跑直接给我上一课。后来我才明白,参数调优才是区分「玩具策略」和「可实盘策略」的分水岭。
这一章,我们不讲虚的。直接聊三个核心问题:参数空间怎么设计?过拟合和欠拟合怎么防?回测框架怎么搭才靠谱?
3.1 参数空间:你的策略有多少种可能?
先问个问题:一个简单的双均线策略,快线参数从5到50,慢线参数从20到200,步长都是1。你算算有多少种组合?
答案是:(50-5+1) × (200-20+1) = 46 × 181 = 8326种。这还只是两个参数。
我见过有人把参数加到七八个,参数空间直接爆炸到几百万种。你想想看,这得跑到猴年马月?
参数空间,说白了就是所有参数可能取值的集合。设计参数空间时,我个人习惯遵循三个原则:
- 范围合理:别从0开始,也别跑到无穷大。比如ATR周期,20到100就差不多了,搞个500周期,你是在做长线投资吗?
- 步长适中:步长太小,相邻参数结果几乎一样,白费算力。步长太大,可能错过最优区域。我一般先粗搜(大步长),再细搜(小步长)。
- 参数独立:两个参数如果高度相关,比如开仓阈值A和平仓阈值B,A+B=常数这种,你搜出来的结果其实是一条线,不是一片区域。
核心观点:参数空间不是越大越好。好的参数空间,是「覆盖有效区域,排除无效区域」。我见过有人把参数空间设得巨大,结果最优参数全在边界上——这明显是设计有问题。
3.2 过拟合与欠拟合:回测漂亮,实盘完蛋
这两个概念,做量化的应该都听过。但我敢说,真正理解的人不多。
欠拟合,就是策略太简单,连历史数据都拟合不好。回测曲线跟心电图似的,忽上忽下。这种情况反而好办——加特征、调参数、换模型,总能改善。
过拟合才是真正的杀手。策略在历史数据上表现完美,但一到新数据就崩。为什么?因为你把历史数据里的「噪音」当成了「信号」来学习。
我记得有一次,一个同事兴冲冲地跑过来说他的策略年化收益80%。我一看回测曲线,好家伙,完美避开所有大跌。但仔细一查,他的参数里有一个「持仓时间不超过3天」的硬编码。说白了,这个参数就是针对某几次大跌专门调的。
怎么防过拟合?我分享几个实战经验:
- 样本外测试:永远留一段数据不参与调参。我习惯用前70%做训练,后30%做验证。
- 参数稳定性检查:最优参数附近的参数,表现应该差不多。如果稍微动一下参数,收益就暴跌,那基本是过拟合。
- 交叉验证:把数据分成几段,轮流做训练和验证。如果某一段上表现特别差,说明策略不稳定。
避坑指南:我曾经犯过一个错误——用全部数据调参,然后用全部数据回测。结果看起来年化50%,实盘直接亏了20%。后来我才明白,这叫「未来函数」,是过拟合的典型表现。切记:调参用的数据和验证用的数据,必须严格分开。
3.3 回测框架搭建:从零开始,稳扎稳打
回测框架,说白了就是模拟历史交易的工具。但别小看它,框架搭不好,你的策略再牛也是白搭。
我个人习惯用Python,因为生态好,pandas、numpy这些库用起来顺手。下面是一个极简回测框架的核心逻辑:
import pandas as pd
import numpy as np
class BacktestEngine:
def __init__(self, data, strategy, initial_capital=100000):
self.data = data
self.strategy = strategy
self.capital = initial_capital
self.positions = []
self.trades = []
def run(self):
# 遍历每一根K线
for i in range(len(self.data)):
# 获取当前行情
current_bar = self.data.iloc[i]
# 调用策略,生成信号
signal = self.strategy.generate_signal(
self.data.iloc[:i+1],
self.positions
)
# 执行交易
if signal == 'buy' and not self.positions:
self._open_long(current_bar)
elif signal == 'sell' and self.positions:
self._close_long(current_bar)
def _open_long(self, bar):
# 开仓逻辑
cost = bar['close'] * 100 # 假设每手100股
if cost <= self.capital:
self.positions.append({
'entry_price': bar['close'],
'entry_time': bar['time'],
'size': 100
})
self.capital -= cost
def _close_long(self, bar):
# 平仓逻辑
pos = self.positions.pop()
pnl = (bar['close'] - pos['entry_price']) * pos['size']
self.capital += bar['close'] * pos['size']
self.trades.append({
'entry_time': pos['entry_time'],
'exit_time': bar['time'],
'pnl': pnl
})
def get_performance(self):
# 计算绩效指标
total_pnl = sum(t['pnl'] for t in self.trades)
win_rate = len([t for t in self.trades if t['pnl'] > 0]) / len(self.trades)
return {
'total_return': total_pnl / 100000,
'win_rate': win_rate,
'trade_count': len(self.trades)
}
这个框架虽然简单,但核心要素都有了:数据遍历、信号生成、交易执行、绩效计算。实际项目中,你还需要加上滑点、手续费、保证金等细节。
小技巧:我建议你在回测框架里加一个「日志模式」。每次交易都记录当时的参数、行情、信号。这样出了问题,可以回溯到具体哪一笔交易、哪个参数导致的。这个习惯帮我排过不少雷。
3.4 知识体系总览
下面这张图,是我对本章知识体系的总结。你可以把它当成一张「地图」,随时回来对照。
嗯,这张图把本章的三个核心内容串起来了。你写代码的时候,可以把它贴在旁边,随时提醒自己:参数空间别乱搞,过拟合要严防,回测框架要扎实。
最后说一句:参数调优没有银弹。我做了这么多年,依然会在某些策略上栽跟头。但只要你把上面这些原则刻在脑子里,至少能避开80%的坑。
记住:回测是用来发现问题的,不是用来证明策略牛逼的。如果你回测结果好到不敢相信,那多半是哪里出了问题。