4、配对交易实战:价差序列计算、Z-score标准化、交易信号生成

好,咱们直接进入正题。

前面聊了那么多理论,什么协整检验、什么均值回归,听起来都挺美。但理论归理论,真到了写代码的时候,你会发现——嗯,坑还挺多的。

这一章,我就带你手把手把配对交易的整个流程跑一遍。从拿到两个股票的价格序列开始,到最终生成买卖信号,每一步我都会拆开来讲。代码我会贴出来,但更重要的是背后的逻辑——为什么这么做?不这么做会怎样?

4.1 价差序列的计算

配对交易的核心,说白了就是赌两个股票的价格差会回归均值。那第一步,自然就是把这个「价差」算出来。

价差不是简单的价格相减。你想想看,如果A股票100块,B股票50块,直接相减得到50,这有意义吗?没意义。因为它们的价格尺度不一样。

正确的做法是:先做线性回归,找到两个股票之间的均衡关系。然后用这个关系去算价差。

具体来说,假设我们找到了一对协整的股票,比如浦发银行和招商银行。我们用浦发银行的价格去回归招商银行的价格,得到回归系数β。那么价差就是:

spread = price_A - β * price_B

这个价差,才是真正有统计意义的「偏离程度」。

核心要点:价差序列必须基于协整关系计算,而不是简单相减。否则你看到的「机会」可能只是幻觉。

我在项目中遇到过一件事。有次我直接用价格差去做回测,结果发现信号特别多,但一实盘就亏钱。后来一查,原来是两只股票的价格尺度差太多,价差序列根本不平稳。从那以后,我再也不敢偷懒了。

代码实现其实很简单:

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

# 假设 df_A 和 df_B 是两只股票的价格序列
def calculate_spread(df_A, df_B):
    # 做线性回归,得到 beta
    X = sm.add_constant(df_B)
    model = sm.OLS(df_A, X).fit()
    beta = model.params[1]
    
    # 计算价差
    spread = df_A - beta * df_B
    return spread, beta

这里要注意一点:回归用的数据窗口要多长?我个人习惯用60个交易日,也就是大概3个月。太短了,回归结果不稳定;太长了,股票的基本面可能已经变了。

小技巧:你可以试试滚动回归,也就是每天重新算一次beta。这样能捕捉到两只股票关系的动态变化。不过计算量会大一些。

4.2 Z-score标准化

好,价差算出来了。但问题来了——价差的数值本身没有标准。今天价差是2,明天价差是5,这算大还是小?

这就需要标准化了。

Z-score,说白了就是把价差转换成「偏离均值多少个标准差」。公式很简单:

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

当Z=2的时候,意味着当前价差已经偏离均值2个标准差。按照正态分布,这种情况发生的概率只有5%左右。嗯,这就是我们等待的「极端机会」。

代码实现:

def calculate_zscore(spread, window=60):
    # 滚动计算均值和标准差
    mean = spread.rolling(window=window).mean()
    std = spread.rolling(window=window).std()
    
    # 计算Z-score
    zscore = (spread - mean) / std
    return zscore

这里有个细节很多人会忽略:窗口大小怎么选?

窗口太小,比如20天,Z-score会频繁触发信号,但很多都是假信号。窗口太大,比如120天,信号会变得稀少,可能一年都等不到几次交易机会。

我个人的经验是:60天是个不错的起点。你可以根据股票的波动特性去调,波动大的股票用短窗口,波动小的用长窗口。

避坑指南:我曾经犯过一个错误——直接用全样本的均值和标准差去算Z-score。结果回测曲线漂亮得不行,一实盘就崩了。为什么?因为全样本包含了未来的信息,这叫「未来函数」。记住,Z-score必须用滚动窗口,只用历史数据。

4.3 交易信号生成

有了Z-score,信号生成就水到渠成了。

典型的规则是这样的:

  • 当Z-score > 2.0 时,做空价差(卖A买B)
  • 当Z-score < -2.0 时,做多价差(买A卖B)
  • 当Z-score 回归到 0 附近时,平仓

为什么是2.0?这其实是个经验值。理论上,Z-score超过2的概率只有5%,这意味着我们只捕捉最极端的偏离。如果你觉得信号太少,可以放宽到1.5;如果你怕假信号多,可以收紧到2.5。

代码实现:

def generate_signals(zscore, entry_threshold=2.0, exit_threshold=0.5):
    signals = pd.Series(index=zscore.index, dtype=int)
    signals[:] = 0  # 0表示空仓
    
    position = 0  # 当前持仓状态:1=多头,-1=空头,0=空仓
    
    for i in range(1, len(zscore)):
        if position == 0:
            # 开仓条件
            if zscore.iloc[i] > entry_threshold:
                position = -1  # 做空价差
            elif zscore.iloc[i] < -entry_threshold:
                position = 1   # 做多价差
        else:
            # 平仓条件:回归到均值附近
            if abs(zscore.iloc[i]) < exit_threshold:
                position = 0
        
        signals.iloc[i] = position
    
    return signals

你可能会问:为什么平仓阈值设成0.5,而不是0?

嗯,这里有个小门道。如果设成0,意味着你必须等到价差完全回归均值才平仓。但现实中,价差往往会在均值附近来回震荡,你可能永远等不到精确的0。设成0.5,相当于给了个「缓冲区」,让交易更平滑。

重要提醒:信号生成只是第一步。真正赚钱的关键在于——你能否严格执行信号。很多人在Z-score达到2.5的时候不敢开仓,觉得「这次不一样」。结果呢?往往就是这次「不一样」的行情,最后回归得最猛烈。

4.4 完整流程可视化

说了这么多,咱们用一张图把整个流程串起来:

配对交易信号生成流程 1. 获取价格数据 2. 协整检验 3. 计算价差序列 4. Z-score标准化 5. 生成交易信号 关键参数说明 • 回归窗口:60个交易日 • 开仓阈值:|Z-score| > 2.0 • 平仓阈值:|Z-score| < 0.5 • 止损:|Z-score| > 3.0 或 持仓超过20天 • 仓位管理:等权重分配资金

这张图把整个流程串起来了。从数据获取到信号生成,每一步都环环相扣。你想想看,如果哪一步出了问题,后面的信号质量都会受影响。

4.5 实战中的注意事项

最后,我总结几个实战中容易踩的坑:

问题 表现 解决方案
价差不平稳 信号频繁触发,但无法回归 重新做协整检验,换配对
Z-score窗口太小 信号太多,交易成本高 增大窗口到80-100天
Z-score窗口太大 信号太少,错过机会 减小窗口到40-50天
未来函数 回测漂亮,实盘亏损 严格使用滚动窗口
忽略交易成本 回测盈利,实盘亏损 每笔交易扣除0.1%成本

我的建议:刚开始做配对交易,别急着上实盘。先用历史数据跑一遍回测,看看信号的质量。重点关注两个指标:信号频率(一年多少次)和胜率(赚钱的比例)。如果一年只有5次信号,那这个策略可能太保守了;如果胜率低于60%,那可能需要调整参数。

好了,这一章的内容就到这里。代码有了,逻辑清楚了,剩下的就是动手去试。记住,量化交易不是纸上谈兵,代码跑起来才是硬道理。


公众号:蓝海资料掘金营,微信deep3321