4、价差序列构建:合成价差、滚动窗口计算、动态阈值设定

好,咱们进入第四讲。前面几章我们把套利的基础逻辑、数据清洗、同步对齐都聊透了。现在终于到了最核心的一步——价差序列构建

说白了,价差就是两个品种之间的价格差值。但Tick级别的套利,可不是简单算个减法就完事了。我刚开始做的时候,就吃过这个亏——直接用收盘价算价差,结果回测曲线漂亮得不行,实盘一跑就崩。为什么?因为Tick数据里,两个合约的成交时间戳根本对不齐。

嗯,今天咱们就一步步把价差序列的构建方法讲透。

4.1 合成价差:不只是减法

合成价差,也叫组合价差。它解决的是两个品种流动性不同、报价频率不同的问题。

举个例子:螺纹钢和热卷,一个主力合约每秒可能有几十笔Tick,另一个可能只有几笔。你直接拿最新价相减,得到的时间序列是错位的。

我个人习惯的做法是:先对齐时间戳,再计算价差

核心公式:
合成价差 = P1(t) - β × P2(t)
其中β是对冲比率,通常用OLS回归或协整系数估计。

这里有个坑:β不是一成不变的。我在项目中遇到过,某次黑色系品种的β在两周内从1.2漂移到了0.9。如果你还用固定β,价差序列就会带趋势,套利信号全废。

所以我的建议是:每根K线(比如5分钟)重新估计一次β,或者用滚动窗口的方式动态更新。

4.1.1 合成价差的代码实现

import pandas as pd
import numpy as np
from statsmodels.regression.linear_model import OLS

def synthetic_spread(tick1, tick2, window=100):
    """
    合成价差:滚动窗口OLS估计β
    tick1, tick2: 已对齐的Tick DataFrame,包含'price'列
    """
    spread = []
    for i in range(window, len(tick1)):
        y = tick1['price'].iloc[i-window:i]
        x = tick2['price'].iloc[i-window:i]
        x = np.column_stack([np.ones(len(x)), x])  # 加截距项
        model = OLS(y, x).fit()
        beta = model.params[1]
        spread.append(tick1['price'].iloc[i] - beta * tick2['price'].iloc[i])
    return pd.Series(spread, index=tick1.index[window:])

你看,代码其实不复杂。但要注意:窗口大小选多少? 我一般用100-200个Tick,太短了β不稳定,太长了反应迟钝。

4.2 滚动窗口计算:让统计量活起来

价差序列建好了,接下来要算它的统计特征——均值、标准差。但你不能用全量数据算一个固定的值,那样做等于刻舟求剑。

为什么?因为市场状态在变。波动率会变,价差的均值也会漂移。我见过有人用过去一年的数据算均值,结果价差早就跑到天边去了,他还傻等着回归。

正确的做法是:滚动窗口计算

窗口类型 特点 适用场景
固定窗口(如200Tick) 计算快,对近期数据敏感 高频套利,日内策略
自适应窗口(如基于波动率) 波动大时窗口缩小,波动小时放大 跨日套利,多品种组合
指数加权移动平均(EWMA) 给近期数据更高权重 趋势性较强的品种

我个人偏爱EWMA。它不需要维护一个固定长度的数组,计算效率高,而且对市场变化反应快。你想想看,如果突然来了一波大行情,固定窗口要等200个Tick才能把旧数据踢出去,EWMA瞬间就调整过来了。

4.2.1 EWMA的实现

def ewma_spread(spread, alpha=0.01):
    """
    指数加权移动平均
    alpha: 衰减因子,越大对近期数据越敏感
    """
    mean = spread.ewm(alpha=alpha).mean()
    std = spread.ewm(alpha=alpha).std()
    return mean, std

alpha怎么选?我一般从0.01开始试,然后看回测结果。0.01相当于约100个Tick的半衰期,适合大多数品种。如果你做的是超高频(比如纳指期货),alpha可以调到0.05甚至0.1。

小技巧: 可以用滚动窗口的波动率来动态调整alpha。波动率大的时候加大alpha,让模型更快适应;波动率小的时候减小alpha,过滤噪音。

4.3 动态阈值设定:别让信号淹死你

阈值,就是触发开仓和平仓的价差水平。固定阈值?不存在的。市场波动率一天一个样,固定阈值要么信号太多(假突破),要么信号太少(错过行情)。

我早期做螺纹钢套利时,设了个固定2倍标准差。结果某天夜盘波动率突然放大,一晚上触发了40多次交易,手续费吃掉了一半利润。第二天我改成动态阈值,情况就好多了。

动态阈值的核心思想:阈值 = 均值 ± k × 标准差,其中均值和标准差都是滚动计算的。

k值怎么定?这取决于你的风险偏好。我一般用以下规则:

  • 开仓阈值: k = 2.0 ~ 2.5(价差偏离均值2倍标准差以上才开仓)
  • 平仓阈值: k = 0.5 ~ 1.0(价差回归到均值附近就平仓)
  • 止损阈值: k = 3.0 ~ 4.0(极端行情下强制平仓)

但这里有个细节:阈值本身也要平滑。直接用原始标准差算阈值,会跟着噪音上下乱跳。我习惯对标准差再做一次EWMA平滑。

4.3.1 动态阈值代码

def dynamic_threshold(spread, alpha=0.01, k_entry=2.0, k_exit=0.5):
    """
    动态阈值生成
    返回:上轨、下轨、中轨(均值)
    """
    mean, std = ewma_spread(spread, alpha)
    upper = mean + k_entry * std
    lower = mean - k_entry * std
    exit_upper = mean + k_exit * std
    exit_lower = mean - k_exit * std
    return upper, lower, exit_upper, exit_lower, mean
注意: 动态阈值不是万能的。如果市场出现结构性变化(比如政策突变、合约换月),价差的均值会发生永久性偏移。这时候动态阈值也会失效。我建议加一个“均值漂移检测”,如果连续N个Tick的价差都偏离均值超过3倍标准差,就暂停交易,重新校准模型。

4.4 知识体系总览

说了这么多,咱们用一张图把整个价差序列构建的逻辑串起来:

价差序列构建核心流程 Tick数据输入 品种A + 品种B 合成价差 滚动OLS估计β 滚动窗口计算 均值/标准差 动态阈值设定 开仓/平仓/止损阈值 交易信号 开/平/观望 风控与校准 均值漂移检测 / 暂停交易 反馈校准 图:价差序列构建的完整流程与反馈机制

这张图把整个流程串起来了。从Tick数据输入,到合成价差、滚动窗口计算、动态阈值设定,最后生成交易信号。注意那个虚线反馈箭头——如果风控检测到均值漂移,要回退到第一步重新校准β和阈值。

嗯,这就是价差序列构建的全部内容。说实话,这部分是套利策略的灵魂。前面数据清洗做得再好,价差序列建得不对,后面全是白搭。我见过太多人在这上面栽跟头,所以今天特意多花了些篇幅。

下一章咱们聊信号生成与执行——有了价差序列和阈值,怎么生成实盘可用的交易信号?到时候我会分享一些实盘中的细节,比如滑点处理、订单簿深度判断等等。

核心要点回顾:
  • 合成价差要用滚动OLS估计β,别用固定值
  • 滚动窗口推荐EWMA,计算快、反应灵敏
  • 动态阈值要平滑,避免被噪音干扰
  • 加一个均值漂移检测,防止结构性变化导致失效

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