4. 价差计算与统计:价差序列计算、均值回归检验、协整检验、半衰期计算、布林带与Z-score

各位同学,欢迎来到跨交易所套利策略的核心环节。前面我们聊了数据清洗和同步,那都是准备工作。真正的硬菜,从价差计算开始。

说白了,套利交易赚的就是价差回归的钱。你想想看,同一个东西在两个地方卖不同价,你低价买、高价卖,差价就是利润。但问题来了——这个价差是随机的,还是有规律的?它会不会越走越远,让你爆仓?

嗯,这就是本章要解决的核心问题。我会带你一步步走完价差计算的完整流程,包括统计检验、参数估计,以及最终如何用布林带和Z-score来生成交易信号。

核心逻辑一句话:找到一对价格走势高度相关的品种,计算它们的价差,验证这个价差是否具有均值回归特性,然后利用统计工具捕捉回归机会。

4.1 价差序列计算

价差计算听起来简单,就是A价格减B价格嘛。但实际项目中,我踩过不少坑。

最常见的价差形式有三种:

  • 简单价差:P₁ - P₂。适用于价格水平相近的品种,比如同一只股票在不同交易所的现货价格。
  • 对数价差:ln(P₁) - ln(P₂)。适用于价格差异较大的品种,对数变换能让序列更平稳。我个人习惯用这个,尤其是在做加密货币套利时。
  • 比率价差:P₁ / P₂。适用于价格存在倍数关系的品种,比如股指期货和ETF之间。

这里有个关键点——协整系数。如果两个品种的价格不是1:1的关系,比如螺纹钢和热卷,它们的价差公式应该是 P₁ - β × P₂。这个β怎么算?用最小二乘法(OLS)回归。

import numpy as np
import pandas as pd
import statsmodels.api as sm

# 假设df包含两列:'asset_a' 和 'asset_b'
def calc_spread(df, method='simple'):
    if method == 'simple':
        spread = df['asset_a'] - df['asset_b']
        beta = 1.0
    elif method == 'log':
        spread = np.log(df['asset_a']) - np.log(df['asset_b'])
        beta = 1.0
    elif method == 'regression':
        # OLS回归求协整系数
        X = sm.add_constant(df['asset_b'])
        model = sm.OLS(df['asset_a'], X).fit()
        beta = model.params['asset_b']
        spread = df['asset_a'] - beta * df['asset_b']
    return spread, beta

避坑指南:我曾经在计算股指期货价差时,直接用简单价差,结果发现价差序列一直在漂移。后来才发现,两个合约的乘数不同,必须用回归系数调整。从那以后,我养成了先做回归再算价差的习惯。

4.2 均值回归检验

价差算出来了,然后呢?你得先确认它是不是会回归。如果价差是个随机游走,那做套利就是送钱给市场。

均值回归检验,说白了就是看价差序列是否平稳。平稳的序列会围绕一个均值上下波动,不会越走越远。

我常用的方法有两个:

  • ADF检验(Augmented Dickey-Fuller Test):最经典的单位根检验。p值小于0.05,说明序列平稳,存在均值回归特性。
  • PP检验(Phillips-Perron Test):对序列自相关和异方差更稳健。我一般两个都跑,取交集。
from statsmodels.tsa.stattools import adfuller, coint

def check_mean_reversion(spread):
    # ADF检验
    adf_result = adfuller(spread, autolag='AIC')
    adf_stat = adf_result[0]
    adf_pvalue = adf_result[1]
    
    # 判断是否平稳
    is_stationary = adf_pvalue < 0.05
    
    return {
        'adf_statistic': adf_stat,
        'adf_pvalue': adf_pvalue,
        'is_stationary': is_stationary
    }

注意:ADF检验的滞后阶数选择很关键。选少了,自相关没消除;选多了,检验功效下降。我建议用AIC准则自动选择,或者根据数据频率固定一个值——日线数据用5阶,小时线用12阶。

4.3 协整检验

均值回归检验是针对价差本身的。但有时候,两个品种单独看都不平稳,它们的线性组合却可能是平稳的。这就是协整。

协整检验,说白了就是验证两个非平稳序列之间是否存在长期均衡关系。如果存在,那价差就会围绕均衡值波动,套利就有基础。

最常用的方法是Engle-Granger两步法

  1. 第一步:用OLS回归估计协整系数β
  2. 第二步:对残差(也就是价差)做ADF检验

不过,statsmodels直接提供了协整检验函数,一步到位:

def check_cointegration(price_a, price_b):
    # 协整检验
    coint_result = coint(price_a, price_b)
    coint_t = coint_result[0]
    coint_pvalue = coint_result[1]
    crit_values = coint_result[2]
    
    is_cointegrated = coint_pvalue < 0.05
    
    return {
        'coint_statistic': coint_t,
        'coint_pvalue': coint_pvalue,
        'critical_values': crit_values,
        'is_cointegrated': is_cointegrated
    }

个人经验:我在做商品期货套利时,发现豆粕和菜粕的协整关系非常稳定,p值经常在0.001以下。但换了样本区间后,协整关系可能消失。所以,我建议用滚动窗口做协整检验,比如每60个交易日重新检验一次,确保关系没有断裂。

4.4 半衰期计算

半衰期,这个概念来自物理,但在量化里指的是价差偏离均值后,回归到均值一半所需的时间。说白了,就是告诉你——这个套利机会要等多久才能赚钱。

半衰期越短,套利频率越高,资金利用率越好。半衰期太长,你可能等不起,或者中间被波动震出局。

计算半衰期的方法基于均值回归过程:

  • 对价差序列做一阶自回归:ΔSₜ = α + θ × Sₜ₋₁ + εₜ
  • 半衰期 = ln(2) / |θ|
def calc_half_life(spread):
    # 构建滞后序列
    spread_lag = spread.shift(1).dropna()
    spread_diff = spread.diff().dropna()
    
    # 对齐数据
    y = spread_diff.values
    x = spread_lag.values
    
    # OLS回归
    x = sm.add_constant(x)
    model = sm.OLS(y, x).fit()
    theta = model.params[1]
    
    # 计算半衰期
    half_life = -np.log(2) / theta if theta < 0 else np.inf
    
    return half_life

经验阈值:我个人认为,半衰期在5-20个周期(日线就是5-20天)是最理想的。太短了,交易成本占比高;太长了,资金占用成本高。如果半衰期超过60个周期,我建议放弃这对品种。

4.5 布林带与Z-score

好了,前面做了那么多检验,终于到了实战环节——生成交易信号。

布林带和Z-score,本质上都是标准化价差,告诉你当前价差处于什么位置。

布林带:以移动平均线为中心,上下各加减k倍标准差。当价差触及上轨时做空价差,触及下轨时做多价差。

Z-score:直接计算当前价差偏离均值多少个标准差。公式很简单:Z = (当前价差 - 均值) / 标准差。

我个人更偏爱Z-score,因为它更直观,而且可以灵活设置阈值。比如Z-score超过2就开仓,回到0就平仓。

def calc_bollinger_zscore(spread, window=20, num_std=2):
    # 计算移动均值和标准差
    rolling_mean = spread.rolling(window=window).mean()
    rolling_std = spread.rolling(window=window).std()
    
    # 布林带
    upper_band = rolling_mean + num_std * rolling_std
    lower_band = rolling_mean - num_std * rolling_std
    
    # Z-score
    zscore = (spread - rolling_mean) / rolling_std
    
    return {
        'upper_band': upper_band,
        'lower_band': lower_band,
        'zscore': zscore,
        'rolling_mean': rolling_mean
    }

注意:布林带的窗口长度和标准差倍数需要根据品种特性调整。我见过有人直接用20天2倍标准差,结果在波动率突变时频繁止损。我的做法是:先用半衰期作为窗口长度,再用历史回测优化标准差倍数。

4.6 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张流程图:

价差计算与统计知识体系 价格序列 A & B 价差计算:简单 / 对数 / 回归 均值回归检验 (ADF) 协整检验 (Engle-Granger) 半衰期计算 协整系数 β 布林带 / Z-score → 交易信号

这张图把整个流程串起来了。从价格输入开始,经过价差计算、统计检验、参数估计,最后到信号生成。每一步都有对应的Python代码实现,你可以直接拿去用。

嗯,本章的内容就到这里。记住,价差计算只是工具,真正的核心是理解背后的统计逻辑。只有通过了均值回归检验和协整检验,你的套利策略才有理论支撑。否则,再漂亮的布林带也只是花架子。

最后一句:我在实战中见过太多人跳过检验直接上策略,结果亏得血本无归。别急,慢就是快。把检验做扎实了,后面的路就好走了。


专注资料整理