4. 构建价差序列:从价格到价差,如何标准化价差(Z-Score)并设定交易阈值
好,咱们进入正题。
前面我们聊了配对选择,也讲了协整检验。但说实话,那些都只是准备工作。真正让策略跑起来的,是价差序列的构建和标准化。我见过不少新手,协整检验做得漂漂亮亮,结果一上实盘就亏钱——问题就出在价差处理上。
4.1 从价格到价差:这一步没那么简单
很多人以为价差就是A价格减B价格。嗯,理论上没错。但实际做起来,坑不少。
先看最基础的形式:
# 简单价差
spread = price_A - price_B
这有什么问题?你想想看,如果A是茅台,B是五粮液,价格差几百块,这个价差序列的波动范围会很大。而且随着时间推移,股票本身会除权除息,价格中枢会漂移。直接用原始价差,你会发现阈值根本没法设——今天价差在10块,明天可能就跳到50块了。
我个人习惯用对数价差。为什么?因为对数价格更接近正态分布,而且对数价差天然就是百分比关系,不受绝对价格影响。
# 对数价差(我推荐的方式)
import numpy as np
import pandas as pd
log_price_A = np.log(price_A)
log_price_B = np.log(price_B)
spread = log_price_A - log_price_B
还有一种方式是用回归残差。如果你做了OLS回归,残差本身就是一种价差:
# 回归残差作为价差
import statsmodels.api as sm
X = sm.add_constant(price_B)
model = sm.OLS(price_A, X).fit()
spread = model.resid
这三种方式各有适用场景。简单价差适合同行业、价格接近的品种。对数价差适合大多数股票对。回归残差适合协整关系比较弱的配对。
4.2 标准化价差:Z-Score 到底在做什么?
价差序列建好了,但它的数值本身没有意义。你需要知道:当前价差偏离均值有多远?这个偏离是正常的还是异常的?
Z-Score 就是干这个的。它的公式很简单:
Z = (当前价差 - 价差均值) / 价差标准差
说白了,Z-Score 告诉你当前价差距离均值有几个标准差。Z=2 意味着价差偏离均值两个标准差,这在正态分布下发生的概率只有约5%。
代码实现:
def calculate_zscore(spread, window=20):
"""
计算滚动Z-Score
window: 滚动窗口,我一般用20天
"""
mean = spread.rolling(window=window).mean()
std = spread.rolling(window=window).std()
zscore = (spread - mean) / std
return zscore
这里有个关键点:窗口大小怎么选?
我踩过这个坑。有一次我用60天窗口,结果Z-Score反应特别迟钝,价差都回归了,信号还没出来。后来改成20天,效果好很多。
| 窗口大小 | 特点 | 适用场景 |
|---|---|---|
| 10-15天 | 敏感,信号多,但假信号也多 | 高频配对、日内交易 |
| 20-30天 | 平衡,我最常用 | 日线级别配对交易 |
| 40-60天 | 稳定,信号少 | 长周期、低换手策略 |
4.3 交易阈值:到底设多少合适?
Z-Score 算出来了,接下来就是设定阈值。阈值决定了你什么时候开仓、什么时候平仓。
经典设置是这样的:
- 开仓阈值: Z-Score 达到 ±2 时开仓
- 平仓阈值: Z-Score 回归到 0 附近时平仓
- 止损阈值: Z-Score 达到 ±3 时止损
为什么是2和3?因为正态分布下,Z=2的概率约5%,Z=3的概率约0.3%。这意味着你捕捉的是小概率事件,赌它回归均值。
但实际做起来,你会发现市场并不完全服从正态分布。价差序列往往有尖峰厚尾的特征。所以阈值需要调整。
# 完整的信号生成逻辑
def generate_signals(zscore, entry_threshold=2.0, exit_threshold=0.5, stop_threshold=3.0):
"""
生成交易信号
"""
signals = pd.Series(index=zscore.index, data=0)
# 开仓信号
signals[zscore >= entry_threshold] = -1 # 做空价差
signals[zscore <= -entry_threshold] = 1 # 做多价差
# 平仓信号:回归到均值附近
signals[abs(zscore) <= exit_threshold] = 0
# 止损信号
signals[abs(zscore) >= stop_threshold] = 0
return signals
4.4 避坑指南:我踩过的三个坑
做价差标准化,有几个坑我反复踩过。分享出来,你们别走弯路。
坑一:忘记更新均值和标准差
价差的均值和标准差不是一成不变的。随着时间推移,股票的基本面会变,价差的统计特征也会漂移。我建议用滚动窗口,而不是固定全样本。
# 错误做法:用全样本均值
full_mean = spread.mean() # 别这么干!
# 正确做法:滚动均值
rolling_mean = spread.rolling(20).mean()
坑二:阈值设得太死板
我曾经在某个品种上设了±2的阈值,回测效果很好。结果换到另一个品种,完全失效。为什么?因为不同品种的价差波动性不同。我后来改用动态阈值——根据近期波动率调整。
# 动态阈值示例
def dynamic_threshold(zscore, lookback=60):
recent_vol = zscore.rolling(lookback).std()
threshold = 2.0 * (recent_vol / recent_vol.mean())
return threshold
坑三:忽略交易成本
Z-Score 在±1.8附近来回震荡时,你会频繁开平仓。每次交易都有成本。我建议加一个"信号确认"机制——连续两天Z-Score超过阈值才开仓。
4.5 知识体系总览
下面这张图,把价差构建到阈值设定的整个流程串起来了。建议保存下来,做策略时对照着看。
嗯,到这里,价差序列的构建和标准化就讲完了。记住一句话:Z-Score 是你的信号灯,阈值是你的红绿灯。灯亮了不一定就要冲,还得看看路况——也就是市场环境。
下一节,我们会把这些信号变成真正的交易指令。到时候你就知道,信号生成只是开始,资金管理才是重头戏。