4. 价差计算与标准化:价差序列构建、Z-score标准化、滚动窗口计算

好,咱们进入第四章。价差计算与标准化。

说实话,很多做配对交易的朋友,最后亏钱不是因为策略不对,而是价差算错了。我见过有人直接用价格相减,结果两个品种价格差了几百块,Z-score 算出来全是异常值。嗯,这其实是个坑。

这一章,我会把价差序列怎么构建、怎么标准化、怎么用滚动窗口动态更新,掰开揉碎了讲清楚。你跟着我走一遍,后面写策略心里就有底了。

4.1 价差序列的构建:从价格到价差

价差,说白了就是两个品种之间的“距离”。但怎么量这个距离,有讲究。

常见的价差构建方式有三种:

方法 公式 适用场景
简单价差 spread = P₁ - P₂ 同品种、同面值(如股指期货近远月)
比率价差 spread = P₁ / P₂ 价格量级差异大(如股票 vs 股票)
回归残差 spread = P₁ - β × P₂ 协整关系明确,需要对冲比率

我个人习惯用回归残差。为什么?因为简单价差受价格绝对值影响太大。举个例子,茅台和五粮液,价格差几百块,你拿简单价差做 Z-score,均值漂移一下你就懵了。

核心原则:价差序列必须满足“均值回归”的前提假设。如果序列本身是随机游走的,那后面所有标准化都是白搭。

构建价差序列的代码,我一般这样写:

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

# 假设 df 包含两列:'asset1' 和 'asset2'
def build_spread(df, method='ols'):
    if method == 'simple':
        spread = df['asset1'] - df['asset2']
    elif method == 'ratio':
        spread = df['asset1'] / df['asset2']
    elif method == 'ols':
        # 用 OLS 回归估计对冲比率
        X = sm.add_constant(df['asset2'])
        model = sm.OLS(df['asset1'], X).fit()
        beta = model.params['asset2']
        spread = df['asset1'] - beta * df['asset2']
    return spread

小提示:用 OLS 回归时,记得检查残差是否平稳。我一般会顺手跑个 ADF 检验,p 值小于 0.05 才放心用。

4.2 Z-score 标准化:让价差变得可比较

价差序列建好了,但不同时间段的价差均值、标准差都不一样。你没法直接说“价差大于 5 就开仓”,因为 5 在某个时段可能是极端值,在另一个时段可能就是正常波动。

这时候就需要 Z-score 标准化。公式很简单:

Z = (当前价差 - 均值) / 标准差

Z-score 的含义是:当前价差偏离均值多少个标准差。一般 Z > 2 或 Z < -2 就认为是极端值,可以考虑反向开仓。

我曾经在回测时犯过一个错:用全量数据算均值和标准差。结果呢?Z-score 在早期全是 0 附近,后期全是极端值。为什么?因为均值漂移了。所以,标准化一定要用滚动窗口。

def zscore_spread(spread, window=60):
    rolling_mean = spread.rolling(window=window).mean()
    rolling_std = spread.rolling(window=window).std()
    zscore = (spread - rolling_mean) / rolling_std
    return zscore

注意:滚动窗口的前 window-1 个数据点会返回 NaN。我一般会直接 dropna() 掉,或者用 expanding() 先填充一部分。

4.3 滚动窗口计算:动态更新,适应市场变化

滚动窗口是价差标准化的灵魂。市场在变,价差的均值和波动率也在变。你用半年前的均值来算今天的 Z-score,那跟刻舟求剑没区别。

窗口大小怎么选? 我一般遵循两个原则:

  • 统计显著性:窗口至少要有 30-60 个数据点,否则均值和标准差不稳定。
  • 交易频率:日内交易用 20-30 期,日线交易用 60-120 期。

举个例子,如果你做 5 分钟级别的配对交易,窗口设 20 期(约 1.6 小时)就够用了。太长的话,市场结构变了你还用旧参数,容易吃大亏。

滚动窗口计算的完整流程,我画了个图,你一看就明白:

价差标准化滚动窗口计算流程 步骤1:构建价差序列 spread = P₁ - β×P₂ 步骤2:滚动窗口计算 窗口大小 N = 60 步骤3:Z-score标准化 Z = (spread - μ) / σ 滚动窗口内部逻辑 当前窗口(最近 N 个数据点) μ μ+σ μ-σ 当前

你看,滚动窗口的核心就是:每次只取最近 N 个数据点,算均值和标准差,然后对当前价差做标准化。窗口不断向前滑动,参数实时更新。

4.4 实战中的几个坑

嗯,这里我要多说几句。实战中价差标准化有几个容易踩的坑:

  • 窗口太短:统计不稳定,Z-score 频繁触发假信号。我曾经用 10 期窗口做 1 分钟线,结果一天交易了 50 次,手续费都亏麻了。
  • 窗口太长:对市场变化反应迟钝,价差已经回归了你还以为在极端位置。
  • 忽略数据质量:价差序列里如果有异常跳空,Z-score 会瞬间爆表。我一般会先做一次中位数滤波,把 3 倍标准差以外的点替换掉。

我的经验:窗口大小最好做一次参数扫描。比如从 20 到 200,步长 10,看哪个窗口的回测夏普比最高。别拍脑袋定。

4.5 完整代码示例

最后,给你一个完整的价差计算与标准化函数。我平时做研究就用这个模板:

def spread_pipeline(df, col1='asset1', col2='asset2', window=60, method='ols'):
    """
    完整的价差计算与标准化流程
    返回:原始价差、Z-score、滚动均值、滚动标准差
    """
    # 1. 构建价差
    if method == 'simple':
        spread = df[col1] - df[col2]
    elif method == 'ratio':
        spread = df[col1] / df[col2]
    elif method == 'ols':
        X = sm.add_constant(df[col2])
        model = sm.OLS(df[col1], X).fit()
        beta = model.params[col2]
        spread = df[col1] - beta * df[col2]
    
    # 2. 滚动窗口标准化
    rolling_mean = spread.rolling(window=window).mean()
    rolling_std = spread.rolling(window=window).std()
    zscore = (spread - rolling_mean) / rolling_std
    
    # 3. 返回结果
    result = pd.DataFrame({
        'spread': spread,
        'zscore': zscore,
        'rolling_mean': rolling_mean,
        'rolling_std': rolling_std
    })
    return result

# 使用示例
result = spread_pipeline(df, window=60, method='ols')
print(result.tail())

好了,价差计算与标准化就讲到这里。你把这个流程跑通,后面做信号生成和回测就顺了。


专注资料整理