第十三章:策略参数调优
做市策略写好了,回测也跑通了。然后呢?
你会发现一个尴尬的事实——策略能不能赚钱,很大程度上取决于那几个参数。比如订单簿深度用多少档?报价更新频率设多快?库存上限设多少?
这些参数,说白了就是策略的「旋钮」。拧对了,收益曲线漂亮得像艺术品。拧错了,回撤能让你怀疑人生。
我见过太多人,策略逻辑写得挺好,结果参数随便填了几个数,上线就亏。嗯,今天我们就聊聊怎么科学地调这些参数。
13.1 参数调优的本质
先想清楚一个问题:我们到底在调什么?
做市策略的参数,通常分三类:
- 风险控制类:最大持仓、止损线、报价偏离度
- 策略行为类:报价频率、订单生命周期、价差倍数
- 市场感知类:波动率窗口、成交量阈值、订单簿深度
每一类参数,都对应着策略的某个行为模式。调参的本质,就是在「激进」和「保守」之间找到平衡点。
核心原则:参数调优不是找「最优解」,而是找「稳健区间」。
最优解往往在过拟合的边缘。稳健区间才是能上线赚钱的。
13.2 网格搜索:最笨但最可靠的方法
网格搜索,说白了就是穷举。你把每个参数的可能取值列出来,然后遍历所有组合。
比如你有两个参数:
- 参数A:报价深度,取值范围 [1, 3, 5, 10]
- 参数B:价差倍数,取值范围 [0.5, 1.0, 1.5, 2.0]
总共 4×4 = 16 种组合。全部跑一遍回测,选收益最高的那组。
# 网格搜索示例
import itertools
param_grid = {
'depth': [1, 3, 5, 10],
'spread_mult': [0.5, 1.0, 1.5, 2.0]
}
best_score = -float('inf')
best_params = None
for depth, spread in itertools.product(param_grid['depth'], param_grid['spread_mult']):
score = backtest_strategy(depth, spread)
if score > best_score:
best_score = score
best_params = {'depth': depth, 'spread_mult': spread}
print(f"最优参数: {best_params}, 得分: {best_score}")
网格搜索的优点很明显:简单、直观、可解释。缺点也很致命:参数多了就爆炸。5个参数,每个取10个值,就是10万次回测。我有个项目,网格搜索跑了整整三天三夜。
我的习惯:先用粗网格找到大致区域,再用细网格精调。别一上来就搞精细搜索,那是浪费电。
13.3 随机搜索:用概率换效率
随机搜索的思路很简单:既然穷举太慢,那就在参数空间里随机采样。
你想想看,网格搜索在某个维度上取了10个值,但可能最优解恰好不在这些点上。随机搜索反而有可能「蒙中」更好的点。
# 随机搜索示例
import random
def random_search(param_ranges, n_iterations=100):
best_score = -float('inf')
best_params = None
for _ in range(n_iterations):
params = {
'depth': random.choice([1, 3, 5, 10]),
'spread_mult': random.uniform(0.5, 2.0),
'vol_window': random.randint(10, 100)
}
score = backtest_strategy(**params)
if score > best_score:
best_score = score
best_params = params
return best_params, best_score
随机搜索有个数学上的优势:当参数空间维度很高时,随机采样的覆盖效率远高于网格搜索。我记得有一篇论文专门论证过这个,结论是随机搜索在大多数情况下比网格搜索更划算。
注意:随机搜索的结果不稳定。同样的代码跑两次,可能得到不同的「最优参数」。我建议多跑几次,取出现频率最高的参数组合。
13.4 贝叶斯优化:用历史信息指导搜索
网格搜索和随机搜索,本质上都是「盲搜」。它们没有利用已经跑过的结果来指导下一步搜索。
贝叶斯优化就不一样了。它会建立一个「代理模型」,预测参数空间里哪些区域可能更优。然后每次采样都选最有潜力的点。
# 使用 scikit-optimize 进行贝叶斯优化
from skopt import gp_minimize
from skopt.space import Integer, Real
def objective(params):
depth, spread_mult, vol_window = params
return -backtest_strategy(depth, spread_mult, vol_window) # 取负值,因为我们要最小化
space = [
Integer(1, 10, name='depth'),
Real(0.5, 2.0, name='spread_mult'),
Integer(10, 100, name='vol_window')
]
result = gp_minimize(objective, space, n_calls=50, random_state=42)
print(f"最优参数: {result.x}, 最优得分: {-result.fun}")
贝叶斯优化的好处是收敛快。通常只需要网格搜索 10%-20% 的计算量,就能找到差不多的结果。我在一个高频做市项目里用过,原本网格搜索要跑 2000 次,贝叶斯优化 200 次就搞定了。
但要注意,贝叶斯优化也有坑。它容易陷入局部最优,尤其是目标函数有很多「尖峰」的时候。我建议先用随机搜索跑几轮,把结果作为贝叶斯优化的初始点。
13.5 过拟合防范:调参最大的坑
调参最大的敌人,不是参数多,而是过拟合。
什么叫过拟合?就是你找到的参数,在历史数据上表现完美,一到实盘就崩。
为什么会这样?因为市场数据里有很多「噪声」。你调参的时候,可能恰好拟合了这些噪声,而不是真正的市场规律。
过拟合的典型症状:
- 回测夏普比率 > 5(除非你是文艺复兴基金)
- 参数稍微变一点,收益就大幅波动
- 不同时间段回测结果差异巨大
怎么防范?我总结了几个实用方法:
13.5.1 交叉验证
别只用一段数据调参。把数据分成几段,轮流做训练集和测试集。比如用 2023 年 1-6 月的数据调参,然后用 7-12 月的数据验证。
# 时间序列交叉验证示例
def time_series_cv(data, n_splits=5):
scores = []
for i in range(n_splits):
train_end = int(len(data) * (i + 1) / (n_splits + 1))
test_end = int(len(data) * (i + 2) / (n_splits + 1))
train_data = data[:train_end]
test_data = data[train_end:test_end]
params = optimize_params(train_data)
score = evaluate_params(params, test_data)
scores.append(score)
return np.mean(scores), np.std(scores)
13.5.2 参数稳定性检查
找到最优参数后,别急着用。在最优参数附近随机采样,看看收益变化是否剧烈。如果稍微动一下参数收益就暴跌,说明这个「最优」很可能是过拟合的。
13.5.3 样本外测试
这是最笨但最有效的方法。调参的时候留出一段数据完全不碰,等参数定好了,用这段「新鲜」数据跑一次。如果结果还行,那参数大概率靠谱。
我曾经犯过一个错误:用 2022 年全年的数据调参,结果参数在 2023 年 1 月就失效了。后来才发现,2022 年市场波动率异常高,参数完全被带偏了。从那以后,我坚持用不同市场环境的数据做验证。
13.6 实战建议:调参流程
说了这么多,到底怎么落地?我分享一下自己的调参流程:
- 先定范围:根据经验,给每个参数设定合理的取值范围。别太宽,也别太窄。
- 粗筛:用随机搜索跑 100-200 次,找到几个有潜力的区域。
- 精调:在潜力区域用贝叶斯优化跑 50-100 次。
- 验证:用交叉验证和样本外数据做双重检验。
- 稳定性测试:在最优参数附近做敏感性分析。
- 实盘小试:用小资金上线跑一周,观察实际表现。
这套流程下来,虽然不能保证找到「最优解」,但至少能找到一个「靠谱解」。做市交易,稳定比什么都重要。
最后提醒一句:参数调优是锦上添花,不是雪中送炭。如果你的策略逻辑本身有问题,调再多参数也没用。先把策略逻辑打磨好,再来调参。