第四章:协整与配对交易

配对交易,说白了就是找两个「长得像」的品种,等它们价格走偏了再出手。我做了这么多年套利,最深的体会就是——相关性高不代表能套利。真正能赚钱的,是协整关系。

这一章,咱们就把协整检验、配对选择、对冲比率、交易框架这几个核心问题,一个一个拆开讲。

4.1 协整检验:ADF与Johansen

先问个问题:为什么不能用相关性选配对?

我举个例子。茅台和五粮液,相关性可能高达0.9。但如果你直接做多茅台、做空五粮液,很可能亏得底裤都不剩。为什么?因为它们的价差可能不回归。

协整检验,就是判断两个(或多个)时间序列的线性组合是否平稳。说白了,就是看它们的价差会不会「跑偏了再回来」。

4.1.1 ADF检验(单对协整)

ADF检验是最常用的单对协整检验方法。我个人习惯用两步法:

  1. 第一步:用OLS回归估计协整系数(对冲比率)
  2. 第二步:对残差做ADF检验,看是否平稳

代码实现很简单:

import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller

# 假设price_x和price_y是两个价格序列
# 第一步:OLS回归
X = sm.add_constant(price_x)
model = sm.OLS(price_y, X).fit()
residuals = model.resid

# 第二步:ADF检验残差
adf_result = adfuller(residuals, maxlag=1)
print(f'ADF统计量: {adf_result[0]:.4f}')
print(f'p值: {adf_result[1]:.4f}')

# p值小于0.05,说明残差平稳,存在协整关系
if adf_result[1] < 0.05:
    print('存在协整关系,可以配对交易')
else:
    print('不存在协整关系,放弃这个配对')
避坑指南:我曾经在2018年用ADF检验选了一堆配对,回测漂亮得不行,实盘却亏了。后来发现,ADF检验的滞后阶数选择很关键。我建议用AIC或BIC自动选滞后阶数,别硬设maxlag=1。

4.1.2 Johansen检验(多对协整)

如果你手里有3个以上品种,想找它们之间的协整关系,ADF就不够用了。这时候要用Johansen检验。

Johansen检验的好处是:一次能检验多个协整向量。比如你选了4只银行股,Johansen检验能告诉你它们之间有几个稳定的线性组合。

from statsmodels.tsa.vector_ar.vecm import coint_johansen

# 假设data是包含多个价格序列的DataFrame
# 注意:Johansen检验要求数据是DataFrame格式
result = coint_johansen(data, det_order=0, k_ar_diff=1)

# 输出迹统计量和最大特征值统计量
print('迹统计量:', result.lr1)
print('迹统计量临界值(95%):', result.cvt)
print('最大特征值统计量:', result.lr2)
print('最大特征值临界值(95%):', result.cvm)

# 判断协整关系个数
r = 0
for i in range(len(result.lr1)):
    if result.lr1[i] > result.cvt[i, 1]:  # 95%置信水平
        r += 1
print(f'存在{r}个协整关系')
注意:Johansen检验对数据长度敏感。我建议至少用500个以上的数据点,否则结果不稳定。另外,确定阶数k_ar_diff也很重要,我一般用信息准则来选。

4.2 配对选择方法

选对了配对,交易就成功了一半。我见过太多人随便选两个品种就开干,结果亏得稀里哗啦。

这里分享几个我实战中验证过的方法:

4.2.1 距离法

最简单粗暴的方法。把两个价格序列标准化,然后算欧氏距离。距离越小,说明走势越像。

from sklearn.preprocessing import StandardScaler
import numpy as np

# 标准化
scaler = StandardScaler()
price_x_scaled = scaler.fit_transform(price_x.values.reshape(-1, 1)).flatten()
price_y_scaled = scaler.fit_transform(price_y.values.reshape(-1, 1)).flatten()

# 计算欧氏距离
distance = np.sqrt(np.sum((price_x_scaled - price_y_scaled) ** 2))
print(f'距离: {distance:.4f}')

距离法快是快,但有个问题:它不考虑时间序列的动态特性。两个品种可能距离很近,但价差根本不回归。

4.2.2 协整法

这才是正路子。用ADF或Johansen检验,选出真正有协整关系的配对。

我个人习惯的做法是:

  1. 先用距离法初筛,选出距离最小的前50对
  2. 再用ADF检验,选出p值小于0.05的配对
  3. 最后用回测验证,看策略表现
核心经验:我做过一个统计,在A股市场,同行业的股票更容易形成协整关系。比如银行股、保险股、券商股,它们之间的协整关系往往比较稳定。跨行业的配对,比如茅台和格力,协整关系就不太靠谱。

4.2.3 最小化价差方差法

这个方法比较冷门,但我个人觉得很好用。思路是:找到使价差方差最小的对冲比率

from scipy.optimize import minimize

def hedge_ratio_variance(ratio, price_x, price_y):
    spread = price_y - ratio * price_x
    return np.var(spread)

# 优化对冲比率
result = minimize(hedge_ratio_variance, x0=1.0, args=(price_x, price_y))
optimal_ratio = result.x[0]
print(f'最优对冲比率: {optimal_ratio:.4f}')

4.3 对冲比率计算

对冲比率,就是做多1单位品种A,同时做空多少单位品种B。这个比率算错了,整个策略就废了。

常用的计算方法有三种:

方法 原理 适用场景
OLS回归 用最小二乘法估计系数 最常用,简单快速
最小方差法 使价差方差最小 价差波动大时效果好
Kalman滤波 动态估计对冲比率 协整关系随时间变化时

我一般先用OLS算一个基准比率,然后用滚动窗口更新。比如:

# 滚动窗口计算对冲比率
window = 60  # 60个交易日
ratios = []

for i in range(window, len(price_x)):
    X = sm.add_constant(price_x[i-window:i])
    model = sm.OLS(price_y[i-window:i], X).fit()
    ratios.append(model.params[1])  # 斜率就是对冲比率

# 用最近的对冲比率
current_ratio = ratios[-1]
print(f'当前对冲比率: {current_ratio:.4f}')
经验之谈:我建议不要用全样本算一个固定的对冲比率。市场在变,协整关系也在变。我一般用60-90天的滚动窗口,每天更新一次对冲比率。

4.4 配对交易策略框架

好了,前面讲了检验、选对、算比率,现在该说怎么交易了。一个完整的配对交易框架,包含以下几个步骤:

4.4.1 策略流程

  1. 数据准备:获取两个品种的价格数据,清洗、对齐
  2. 协整检验:用ADF或Johansen确认协整关系
  3. 计算价差:spread = price_y - hedge_ratio * price_x
  4. 标准化价差:z-score = (spread - mean) / std
  5. 设置阈值:通常用±1.5或±2个标准差
  6. 开仓信号:z-score超过上阈值,做空价差;低于下阈值,做多价差
  7. 平仓信号:z-score回归到0附近
  8. 止损:z-score超过±3个标准差,强制平仓

4.4.2 代码实现

import pandas as pd
import numpy as np

class PairTrading:
    def __init__(self, entry_z=1.5, exit_z=0.5, stop_z=3.0):
        self.entry_z = entry_z
        self.exit_z = exit_z
        self.stop_z = stop_z
        self.position = 0  # 1: 多头价差, -1: 空头价差, 0: 空仓
        
    def generate_signals(self, price_x, price_y, hedge_ratio):
        # 计算价差
        spread = price_y - hedge_ratio * price_x
        # 标准化
        z_score = (spread - spread.rolling(60).mean()) / spread.rolling(60).std()
        
        signals = pd.Series(index=price_x.index, data=0)
        
        for i in range(1, len(z_score)):
            if self.position == 0:
                # 开仓信号
                if z_score.iloc[i] > self.entry_z:
                    signals.iloc[i] = -1  # 做空价差
                    self.position = -1
                elif z_score.iloc[i] < -self.entry_z:
                    signals.iloc[i] = 1   # 做多价差
                    self.position = 1
                    
            elif self.position == -1:
                # 平仓或止损
                if z_score.iloc[i] < self.exit_z:
                    signals.iloc[i] = 1   # 平空仓
                    self.position = 0
                elif z_score.iloc[i] > self.stop_z:
                    signals.iloc[i] = 1   # 止损
                    self.position = 0
                    
            elif self.position == 1:
                if z_score.iloc[i] > -self.exit_z:
                    signals.iloc[i] = -1  # 平多仓
                    self.position = 0
                elif z_score.iloc[i] < -self.stop_z:
                    signals.iloc[i] = -1  # 止损
                    self.position = 0
                    
        return signals

4.4.3 风险控制

配对交易不是稳赚不赔的。我吃过亏,所以特别强调风控:

  • 仓位管理:单笔配对交易不超过总资金的5%
  • 止损:价差超过3个标准差,无条件平仓
  • 再检验:每季度重新做一次协整检验,如果协整关系消失,立即停止交易
  • 多配对分散:同时持有5-10个配对,降低单一配对风险
血的教训:我曾经在2015年股灾期间做配对交易,以为银行股之间协整关系稳定,结果市场流动性枯竭,价差直接崩了。从那以后,我每次开仓前都会检查市场流动性,流动性差的品种坚决不做。

知识体系总览

下面这张图,把本章的核心逻辑串起来了:

配对交易知识体系 数据准备 协整检验(ADF / Johansen) 配对选择方法 对冲比率计算 交易策略框架 开仓 → 持仓监控 → 平仓 / 止损 风险控制(仓位、止损、再检验)

这张图把整个流程串起来了。从数据准备开始,到协整检验,再到配对选择、对冲比率、交易框架,最后落到风控。每一步都环环相扣,缺一不可。

嗯,这一章的内容就到这儿。协整与配对交易,说白了就是找规律、算比率、设阈值、控风险。你把这些吃透了,多市场套利就算入门了。


专注资料整理