策略优化:参数调优、滚动窗口、动态阈值

各位同学,欢迎来到第十五讲。前面我们讲了基差交易的逻辑、统计检验、协整建模,这些都属于「搭框架」的阶段。但框架搭好了,策略就能赚钱吗?

说实话,不一定。我见过太多人,模型跑回测时曲线漂亮得不行,一上实盘就崩。问题出在哪?出在参数上。今天我们就来聊聊策略优化这件事——参数调优、滚动窗口、动态阈值,这三个东西,说白了就是让你的策略「活起来」。

一、参数调优:别让策略死在「死参数」上

先问大家一个问题:你写了一个均值回归策略,阈值设成2倍标准差。为什么是2倍?不是1.5倍?不是2.5倍?

很多人回答:因为教科书上写的。嗯,教科书没错,但市场会变。我2018年做螺纹钢基差套利时,一开始也用的2倍阈值,回测漂亮,实盘前三个月也还行。结果第四个月开始连续亏损,亏得我头皮发麻。后来一查,是市场波动率变了,2倍阈值已经不合适了。

核心观点:参数不是一成不变的。你需要用历史数据去「训练」出最优参数,并且定期更新。

参数调优的常见方法有两种:

  • 网格搜索:把参数范围分成网格,遍历所有组合,选回测效果最好的。
  • 贝叶斯优化:用概率模型指导搜索,效率更高,适合参数多的情况。

我个人习惯先用网格搜索摸个底,再用贝叶斯精调。下面给个网格搜索的代码示例:

import numpy as np
import pandas as pd
from itertools import product

def grid_search(spread, entry_thresholds, exit_thresholds, window_sizes):
    best_params = None
    best_sharpe = -np.inf
    
    for entry, exit, window in product(entry_thresholds, exit_thresholds, window_sizes):
        # 计算滚动均值和标准差
        mean = spread.rolling(window).mean()
        std = spread.rolling(window).std()
        
        # 生成信号
        z_score = (spread - mean) / std
        signals = pd.Series(0, index=spread.index)
        signals[z_score > entry] = -1  # 做空价差
        signals[z_score < -entry] = 1  # 做多价差
        signals[abs(z_score) < exit] = 0  # 平仓
        
        # 计算收益率(简化版)
        returns = signals.shift(1) * spread.pct_change()
        sharpe = returns.mean() / returns.std() * np.sqrt(252)
        
        if sharpe > best_sharpe:
            best_sharpe = sharpe
            best_params = (entry, exit, window)
    
    return best_params, best_sharpe

# 使用示例
entry_range = np.arange(1.5, 3.0, 0.25)
exit_range = np.arange(0.5, 1.5, 0.25)
window_range = [20, 30, 40, 50, 60]

best, sharpe = grid_search(spread, entry_range, exit_range, window_range)
print(f"最优参数: entry={best[0]}, exit={best[1]}, window={best[2]}")
print(f"最优夏普: {sharpe:.2f}")

注意:网格搜索容易过拟合。我曾经犯过这个错——把参数调得完美匹配历史数据,结果实盘一塌糊涂。建议用滚动窗口验证,或者留一段「未见数据」做最终测试。

二、滚动窗口:让模型跟上市场的节奏

参数调优解决了「用什么参数」的问题,但还有一个问题:参数多久更新一次?

你想想看,市场风格会变。2019年的波动率水平和2023年能一样吗?协整关系也会漂移。如果你用三年前的参数做今天的交易,那跟刻舟求剑有什么区别?

滚动窗口就是解决这个问题的。它的思路很简单:

  • 固定一个窗口长度(比如60个交易日)
  • 每天或每周,用最近N天的数据重新计算参数
  • 用新参数生成交易信号

这样做的好处是:模型始终在用「最新」的信息。坏处是:计算量大,而且窗口太短容易受噪声影响。

我的经验:窗口长度一般选20到60个交易日。做高频的可以短一些,做日频的长一些。我做过一个测试:用30天窗口 vs 固定参数,滚动窗口的年化收益高出约15%,最大回撤低了8%。

下面是一个滚动窗口的实现:

def rolling_strategy(spread, window=30, entry_threshold=2.0, exit_threshold=1.0):
    signals = pd.Series(0, index=spread.index)
    positions = pd.Series(0, index=spread.index)
    
    for i in range(window, len(spread)):
        # 取最近window个数据点
        recent = spread.iloc[i-window:i]
        mean = recent.mean()
        std = recent.std()
        
        # 计算当前z-score
        z = (spread.iloc[i] - mean) / std
        
        # 生成信号
        if z > entry_threshold:
            signals.iloc[i] = -1
        elif z < -entry_threshold:
            signals.iloc[i] = 1
        elif abs(z) < exit_threshold:
            signals.iloc[i] = 0
        else:
            signals.iloc[i] = signals.iloc[i-1]  # 持仓不变
    
    return signals

三、动态阈值:让阈值跟着波动率走

前面我们用了固定阈值(比如2倍标准差)。但市场波动率会变,牛市和熊市的波动率能差好几倍。固定阈值在低波动时可能频繁开仓,在高波动时又开不了仓。

动态阈值就是让阈值本身也「自适应」。怎么做?

  • 用近期波动率(比如20日标准差)来调整阈值
  • 波动率大时,阈值放大;波动率小时,阈值缩小
  • 或者用分位数法:取近期z-score的90%分位数作为阈值

举个例子:

def dynamic_threshold_strategy(spread, window=30, quantile=0.9):
    signals = pd.Series(0, index=spread.index)
    
    for i in range(window, len(spread)):
        recent = spread.iloc[i-window:i]
        mean = recent.mean()
        std = recent.std()
        
        z = (spread.iloc[i] - mean) / std
        
        # 用近期z-score的分位数作为动态阈值
        recent_z = (recent - mean) / std
        threshold = recent_z.quantile(quantile)
        
        if z > threshold:
            signals.iloc[i] = -1
        elif z < -threshold:
            signals.iloc[i] = 1
        else:
            signals.iloc[i] = signals.iloc[i-1]
    
    return signals

关键点:动态阈值不是万能的。如果市场结构发生根本性变化(比如从趋势市变成震荡市),阈值调整可能跟不上。我建议配合「市场状态识别」一起用,比如用波动率聚类来区分不同市场环境。

四、三者如何配合?一张图说清楚

参数调优、滚动窗口、动态阈值,这三者不是孤立的。它们应该形成一个闭环:

策略优化闭环 参数调优 网格搜索 / 贝叶斯优化 滚动窗口 定期重估参数 动态阈值 自适应调整开平仓阈值 生成交易信号 开仓 / 平仓 / 持仓 绩效评估 → 反馈到参数调优

流程是这样的:先用历史数据做参数调优,得到一组初始参数。然后进入滚动窗口模式,定期用最新数据重算参数。同时,动态阈值根据近期波动率调整开平仓阈值。最终生成交易信号,跑完一段后做绩效评估,再把结果反馈回去,继续优化参数。

这个闭环跑起来,你的策略就不再是「死」的了。它会跟着市场一起呼吸。

五、避坑指南

最后,分享几个我踩过的坑:

  • 不要过度优化:我曾经把参数调得完美拟合过去三年的数据,结果未来一年亏了20%。记住,回测是历史,不是未来。
  • 滚动窗口不是越大越好:窗口太大,模型反应慢;窗口太小,噪声太多。我一般从30天开始试,根据策略频率调整。
  • 动态阈值要设上下限:极端行情下,阈值可能变得非常大或非常小。设个上下限,比如1.5到3.0倍标准差,防止策略「抽风」。
  • 别忘了交易成本:参数调优时一定要把滑点和手续费算进去。不然你调出来的「最优参数」,实盘可能根本不赚钱。

一个小技巧:做参数调优时,别只看夏普比率。看看最大回撤、胜率、盈亏比。有时候夏普高但回撤大的策略,实盘你根本拿不住。

好了,这一讲就到这里。参数调优、滚动窗口、动态阈值,这三板斧用好了,你的基差套利策略就能从「实验室产品」变成「实战武器」。下一讲我们聊聊风险管理,到时候见。


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