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。
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
4.4 知识体系总览
说了这么多,咱们用一张图把整个价差序列构建的逻辑串起来:
这张图把整个流程串起来了。从Tick数据输入,到合成价差、滚动窗口计算、动态阈值设定,最后生成交易信号。注意那个虚线反馈箭头——如果风控检测到均值漂移,要回退到第一步重新校准β和阈值。
嗯,这就是价差序列构建的全部内容。说实话,这部分是套利策略的灵魂。前面数据清洗做得再好,价差序列建得不对,后面全是白搭。我见过太多人在这上面栽跟头,所以今天特意多花了些篇幅。
下一章咱们聊信号生成与执行——有了价差序列和阈值,怎么生成实盘可用的交易信号?到时候我会分享一些实盘中的细节,比如滑点处理、订单簿深度判断等等。
- 合成价差要用滚动OLS估计β,别用固定值
- 滚动窗口推荐EWMA,计算快、反应灵敏
- 动态阈值要平滑,避免被噪音干扰
- 加一个均值漂移检测,防止结构性变化导致失效
公众号:蓝海资料掘金营,微信deep3321