3. 价差计算与标准化:价差公式、Z-score、滚动窗口标准化
好,咱们进入第三个核心环节。价差计算与标准化。
说实话,很多新手做跨品种套利,上来就盯着价格差看。价格差涨了,做空;价格跌了,做多。结果呢?亏得莫名其妙。为什么?因为价格本身是有趋势的,两个品种的价格水平可能差好几倍,直接相减得到的价差,根本没法用。
我早年做螺纹钢和热卷的套利时,就吃过这个亏。螺纹钢每吨3000多,热卷每吨3200多,价差200块。后来螺纹涨到5000,热卷涨到5200,价差还是200。但你能说套利机会一样吗?显然不能。因为波动率变了,绝对价差的含义完全变了。
所以,价差必须标准化。今天咱们就聊透这件事。
3.1 价差公式:从原始价差到对数价差
先看最基础的价差公式。
假设你有两个品种,价格序列分别是 P1 和 P2。最简单的价差就是直接相减:
spread = P1 - P2
这个公式在什么情况下能用?我个人的经验是:只有当两个品种的价格水平长期稳定在同一个数量级,且波动率相对稳定时,才勉强能用。比如同一交易所的同一板块品种,像豆粕和菜粕,价格都在3000-4000之间晃悠,直接相减问题不大。
但更多时候,咱们需要的是对数价差:
spread = log(P1) - log(P2) = log(P1 / P2)
为什么用对数?说白了,对数价差衡量的是两个品种的相对比例关系,而不是绝对差值。你想想看,P1=100,P2=90,价差是10;P1=1000,P2=990,价差还是10。但这两个场景的套利意义完全不同。对数价差就能消除这种量纲影响。
我在做股指期货和ETF套利时,一直用对数价差。因为IF合约和ETF的价格水平差很多,直接相减根本没法看。用对数价差后,信号稳定多了。
3.2 Z-score:让价差变得可比较
好,价差算出来了。但问题又来了:不同时间段的价差,波动幅度不一样。怎么判断当前价差是偏高还是偏低?
答案是Z-score。
Z-score的公式很简单:
z = (当前价差 - 价差均值) / 价差标准差
它告诉你:当前价差偏离均值多少个标准差。z=2,意味着当前价差比均值高两个标准差,属于极端情况。z=-1.5,意味着比均值低1.5个标准差。
我一般用±2作为阈值。z>2,做空价差;z<-2,做多价差。当然,这个阈值可以调,后面讲参数优化时会细说。
但这里有个坑——均值和标准差怎么算?
如果你用全量历史数据算,那均值和标准差是固定的。但市场在变,固定参数很快会失效。我见过有人用过去5年的数据算Z-score,结果最近一年市场结构变了,Z-score天天在±3以上晃悠,信号完全乱套。
所以,咱们需要滚动窗口。
3.3 滚动窗口标准化:让参数自适应
滚动窗口标准化,说白了就是:只取最近N个数据点,计算均值和标准差,然后算Z-score。
公式长这样:
窗口内均值 = mean(spread[-N:])
窗口内标准差 = std(spread[-N:])
滚动Z-score = (当前价差 - 窗口内均值) / 窗口内标准差
N怎么选?我个人的经验是:
- 短线套利(日内或隔夜): N=20到60,相当于1-3个月的数据
- 中线套利(几周到几个月): N=60到120,相当于3-6个月
- 长线套利(半年以上): N=120到250,相当于半年到一年
我曾经在原油和燃料油的套利中,试过N=10。结果呢?窗口太小,均值和标准差跟着价格乱跳,Z-score频繁触发信号,手续费都亏光了。后来改成N=60,信号稳定多了。
嗯,这里要注意:滚动窗口的N值,本质上是一个平滑参数。N越小,对市场变化反应越快,但噪声也越大。N越大,信号越平滑,但可能滞后。
下面这张图,展示了滚动窗口标准化的核心逻辑:
3.4 实战中的避坑指南
讲几个我踩过的坑。
坑一:窗口内数据不足。 刚启动策略时,滚动窗口里可能只有几个数据点。这时候算出来的均值和标准差极不稳定。我建议:窗口数据少于N/2时,不产生交易信号。
坑二:极端值污染。 如果窗口内出现一个极端价差(比如数据错误或突发事件),它会拉高标准差,导致后续的Z-score偏小,信号被压制。我曾经在橡胶和20号胶的套利中遇到过,一个异常数据让Z-score连续一周没出信号。解决办法:对窗口内的价差做截尾处理,去掉最大和最小的5%。
坑三:参数过拟合。 滚动窗口的N值,不要用历史数据来回测到最优。因为最优N值往往是对历史噪声的过拟合。我一般用N=60作为默认值,然后根据品种波动特性微调,不会为了追求回测收益去死磕参数。
3.5 代码实现示例
最后,给一段Python代码。这是我在实盘中用过的简化版:
import numpy as np
import pandas as pd
def rolling_zscore(spread, window=60):
"""
滚动窗口Z-score计算
spread: 价差序列(建议用对数价差)
window: 滚动窗口大小
"""
# 计算滚动均值和标准差
rolling_mean = spread.rolling(window=window, min_periods=window//2).mean()
rolling_std = spread.rolling(window=window, min_periods=window//2).std()
# 计算Z-score
zscore = (spread - rolling_mean) / rolling_std
# 数据不足时不产生信号
zscore[:window//2] = np.nan
return zscore
# 使用示例
# spread = log(P1) - log(P2)
# z = rolling_zscore(spread, window=60)
# 当z > 2时,做空价差;当z < -2时,做多价差
这段代码里,我用了 min_periods=window//2,意思是窗口数据至少达到一半才开始计算。这就是刚才说的避坑措施。
好了,价差计算与标准化就聊到这儿。核心就三句话:用对数价差、用Z-score、用滚动窗口。把这三点吃透了,跨品种套利的信号质量能上一个台阶。