一、从生活现象理解"均值回归"
先抛开金融,看两个生活例子:
- 天气:夏天再热,也不会一直超过40℃,总会回到该季节的平均温度附近;冬天再冷,也不会一直低于-20℃,最终会向冬季均值回归。
- 身高:父母身高很高的孩子,身高可能接近但很难超过父母(向人群平均身高回归);父母身高较矮的孩子,身高也可能向平均水平靠近(这是统计学中的"高尔顿回归"现象)。
核心规律:事物的发展会围绕一个"长期均值"波动,短期偏离后,有很大概率向均值靠拢。
二、金融市场中的"均值回归"
金融资产(股票、期货等)的价格或"价差"也存在类似规律:
- 单只股票:短期可能因情绪炒作暴涨/暴跌,但长期会向其内在价值(如盈利、行业地位决定的合理价格)回归。
- 两只相关资产的价差:若A和B高度相关(如同行业股票),它们的价格差(或比值)会长期围绕一个均值波动,短期偏离后大概率回归。
配对交易就是利用"两只资产的价差均值回归"获利的策略。
均值回归,特别是配对交易,是量化投资中一种经典的"市场中性"策略。它的迷人之处在于,试图从资产价格的暂时性失衡中获利,而非预测市场整体方向。下面这个表格能让你快速抓住它的核心要点。
| 策略要素 | 核心解释 | 简单理解 |
|---|---|---|
| 核心思想 | 两个高度相关的资产,其价差(相对关系)会围绕一个长期均值波动。当价差显著偏离时,预期它会回归均值。 | 好比两个并肩走路的人,绳子突然拉长,我们赌绳子会缩回正常长度。 |
| 盈利逻辑 | 买入相对低估的资产,卖出相对高估的资产,待价差回归后,进行反向平仓获利。 | "削高填低",赚取价格修正的钱。 |
| 关键特性 | 市场中立 | 同时持有多头和空头头寸,对冲掉市场整体涨跌的风险,收益主要来源于资产间的相对表现。 |
| 核心前提 | 均值回归 | 坚信价差像橡皮筋,拉得越开,回弹的力量越大。暂时的偏离终将回归长期均衡关系。 |
| 理论基础 | 协整关系 | 比简单相关性更高级的统计概念,确保两只股票的价格序列之间存在长期稳定的均衡关系,避免跟错"伙伴"。 |
理解了这些核心概念后,我们来看看如何一步步实际构建一个配对交易策略。
配对交易的核心逻辑:"找对子→等偏离→做对冲"
配对交易的本质是"市场中性策略"------不赌大盘涨跌,只赚两只资产"相对价格回归"的钱。
三 策略实战四步法
一个完整的配对交易策略,从构想变成实际的交易信号,通常需要经历以下四个步骤。
第一步:寻找"天生一对"的资产
这是策略成功的基础。你不能随意找两只股票配对。理想的选择是:
- 基本面相似 :属于同一行业(如两家大银行)、业务模式接近、受相同宏观经济因素影响的公司。这是因为它们的基本面驱动因素相似,价格关系更可能稳定。
- 历史数据验证 :需要通过统计检验来证实这种直观判断。通常先计算历史价格的相关性作为初筛,但更关键的是进行协整检验。
协整检验(常用Engle-Granger两步法或Johansen检验)可以判断两个非平稳的价格序列(股票价格通常是非平稳的)的某种线性组合是否是平稳的。如果检验通过(通常要求p值小于0.05),就认为它们存在协整关系,即具备了进行配对交易的统计基础。
为什么需要协整?
比如两只股票A和B,若只是相关但不协整:A涨10元时B涨8元,下次A涨10元时B可能涨15元,价差(A-B)会越来越大,永远不回归,策略会亏损。而协整能保证"价差最终会回来"。
第二步:量化"距离"与计算价差
找到配对后,需要精确量化它们之间的"距离",即价差。
- 简单价差 :
价差 = 资产A价格 - 资产B价格。适用于价格水平非常接近的资产。 - 比率价差 :
价差 = 资产A价格 / 资产B价格。更常用,消除了绝对价格差异的影响。 - 回归残差价差 :这是更精确的方法。通过线性回归(
资产A价格 = α + β × 资产B价格 + 误差)得到一个对冲比率β,然后计算价差 = 资产A价格 - β × 资产B价格。这个价差序列就是我们需要盯住的"橡皮筋"。
假设我们选了股票A和股票B,它们的价格关系可以用线性回归表示:
B的价格 = a + b×A的价格 + 残差
这里的"残差"就是我们要的价差 (Spread):
价差 = B的实际价格 - (a + b×A的价格)
简单说:价差是"B相对于A的合理价格"的偏离值。长期来看,这个价差会围绕"均值0"波动(若回归模型准确)。
第三步:捕捉交易信号(Z-Score标准化)
直接观察价差绝对值的变化是不科学的,因为价格本身在波动。我们需要将价差标准化 ,使其在不同时期和不同资产间具有可比性。这里就要用到Z-Score 。
Z = (当前价差 - 价差的N日移动平均) / 价差的N日标准差
- 含义:当前价差偏离均值多少个"标准差"(标准差是衡量波动的指标)。
- 举例:若Z-score=2,说明当前价差比均值高2个标准差,属于"显著偏离";若Z-score=0,说明刚好在均值附近。
Z-Score的数值单位是标准差,它清楚地告诉我们当前价差偏离其近期平均水平的程度。交易信号的触发就基于设定Z-Score的阈值:
- 开仓信号 :
- 做多价差 (买入A,卖出B):当
Z < -Z阈值(例如-1.5),意味着价差过小,A相对B被显著低估。 - 做空价差 (卖出A,买入B):当
Z > +Z阈值(例如+1.5),意味着价差过大,A相对B被显著高估。
- 做多价差 (买入A,卖出B):当
- 平仓信号 :当
|Z| < 平仓阈值(例如0.5),意味着价差已回归至均值附近,此时平仓了结利润。
根据Z-score的大小决定操作:
| Z-score情况 | 含义(价差状态) | 交易操作(核心:对冲风险) |
|---|---|---|
| Z-score > 阈值(如+2) | 价差过大(B相对于A太贵了) | 做空B(卖空高估的),做多A(买入低估的) |
| Z-score < -阈值(如-2) | 价差过小(B相对于A太便宜了) | 做多B(买入低估的),做空A(卖空高估的) |
| Z-score接近0(如±0.5) | 价差回归均值附近 | 平仓(卖出持有的,买回做空的,获利了结) |
为什么要"对冲"?
比如同时做多A、做空B:若大盘暴跌,A和B可能一起跌,但A跌得少、B跌得多(或A涨、B跌),价差回归时,做多A的收益能覆盖做空B的亏损,甚至整体盈利。这就是"市场中性"------不依赖大盘方向。
第四步:设置严格的风险控制
这是策略能否长期存活的关键。必须清醒认识到,均值回归的假设可能被打破。
- 止损规则 :
- 偏离止损 :当价差不仅没回归,反而继续反向偏离,
|Z|超过一个更大的阈值(如2.5或3.0)时,应坚决止损,承认错误。 - 时间止损:如果开仓后价差长期(如10个交易日)没有回归迹象,也应平仓离场,避免资金占用和机会成本。
- 偏离止损 :当价差不仅没回归,反而继续反向偏离,
- 关系再验证:协整关系不是一劳永逸的。需要定期(如每季度或每年)重新检验资产对是否仍存在协整关系。如果关系破裂,应立即停止使用该配对。
⚠️ 策略风险与注意事项
配对交易并非"稳赚不赔"的圣杯,它主要面临以下几类风险:
- 模型风险/关系破裂风险 :这是最大的风险。如果两只资产的基本面发生永久性改变(如一家公司技术突破或爆出丑闻),其长期均衡关系可能永久性失效,导致价差永不回归,造成巨大亏损。
- 执行风险 :策略要求多空两头同时成交。在快速变动的市场中,可能无法以理想价格同时完成两笔交易,造成滑点,侵蚀利润。
- 交易成本:由于可能频繁交易,佣金、手续费等成本会显著影响最终收益。
- 趋势市风险:在强劲的单边牛市或熊市中,所有资产同涨同跌,价差可能长期不回归,导致策略持续小幅亏损或需要频繁止损。
💡 给初学者的建议
- 从模拟开始 :在投入真金白银前,使用历史数据进行充分的回测,验证你的策略逻辑和参数是否有效。
- 借助工具:Python(Pandas, NumPy, Statsmodels库)是实施此类策略的绝佳工具,可以方便地进行数据处理、统计检验和回测。
- 先理解后优化:不要一开始就沉迷于优化参数(如Z-Score阈值)。先彻底理解策略的基本原理,再逐步尝试优化和改进。
- 分散投资:不要只押注于一两个资产对。可以尝试构建一个由多个不相关的配对组成的投资组合,以分散风险。
希望这份详细的解释能帮助你迈出量化交易实践的第一步。如果你对其中的某个步骤,比如协整检验的具体Python代码实现,特别感兴趣,我们可以继续深入探讨。
四、实例:用两只白酒股演示配对交易
选A股的贵州茅台(600519)和五粮液(000858):同属白酒行业,消费场景、政策影响高度一致,适合配对。
步骤1:验证协整关系
用过去3年数据计算:
- 回归公式:五粮液价格 = 5.2 + 0.12×茅台价格(a=5.2,b=0.12)
- 价差 = 五粮液实际价格 - (5.2 + 0.12×茅台价格)
- 协整检验:p值=0.03(<0.05),说明价差会回归均值,适合配对。
步骤2:计算Z-score
用60天滚动窗口计算价差的均值和标准差:
- 价差均值=0(长期围绕0波动),标准差=8元。
- 某天价差=18元 → Z-score=(18-0)/8=2.25(>2,触发信号)。
步骤3:交易操作
- 当Z-score=2.25时:五粮液相对于茅台"太贵"(价差过大),于是做空五粮液,做多茅台。
- 1个月后,价差回归到2元(Z-score=0.25):平仓,获利=(茅台涨幅 - 五粮液涨幅)×持仓金额。
五、如何用代码实现(简化逻辑)
对初学者来说,核心是理解"信号生成"和"收益计算",以下是简化流程:
python
# 1. 获取数据(茅台和五粮液收盘价)
import pandas as pd
# 假设已获取数据,data包含"茅台"和"五粮液"列,索引为日期
data = pd.read_csv("白酒数据.csv", index_col="date")
# 2. 计算价差(用线性回归)
import statsmodels.api as sm
x = sm.add_constant(data["茅台"]) # 加入常数项
model = sm.OLS(data["五粮液"], x).fit() # 五粮液 = a + b×茅台
a, b = model.params["const"], model.params["茅台"]
spread = data["五粮液"] - (a + b * data["茅台"]) # 价差
# 3. 计算Z-score(60天滚动)
window = 60
spread_mean = spread.rolling(window).mean()
spread_std = spread.rolling(window).std()
z_score = (spread - spread_mean) / spread_std
# 4. 生成信号
signal = 0 # 0=平仓,1=多五粮液空茅台,-1=多茅台空五粮液
if z_score[-1] > 2:
signal = -1 # 价差过大,多茅台空五粮液
elif z_score[-1] < -2:
signal = 1 # 价差过小,多五粮液空茅台
else:
signal = 0 # 平仓
# 5. 计算收益(每日收益=持仓信号×(五粮液涨跌幅 - 茅台涨跌幅))
returns = signal * (data["五粮液"].pct_change() - data["茅台"].pct_change())
以下是使用baostock获取A股数据实现配对交易策略的完整代码。baostock主要提供A股数据,因此选取两只相关性较高的A股银行股(招商银行和浦发银行)作为示例:
量化交易策略:均值回归(配对交易)(baostock版本)
以两只银行股为例:招商银行和浦发银行。
两者同属银行业,受利率、宏观经济等因素影响高度一致,价格走势长期相关。
假设通过历史数据计算,两者价差的均值为 0,标准差为 1.5。
当价差 Z-score=2.5(大幅高于均值)时,认为招商银行 被高估、浦发银行 被低估,此时做空 招商银行,做多浦发银行。
当价差回归到 Z-score=0 时,平仓获利(做空 招商银行 的收益 + 做多 浦发银行 的收益)。


一、依赖库安装
bash
pip install baostock pandas numpy statsmodels matplotlib
二、完整代码实现
python
import baostock as bs
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.stattools import coint # 协整检验
from statsmodels.regression.linear_model import OLS # 线性回归
# ----------------------
# 步骤1:通过baostock获取A股数据
# ----------------------
# 登录baostock
lg = bs.login()
# 显示登录返回信息
print('登录返回代码:' + lg.error_code)
print('登录返回信息:' + lg.error_msg)
# 选择两只A股银行股:招商银行(sh.600036)和浦发银行(sh.600000)
stock_codes = {
"招商银行": "sh.600036",
"浦发银行": "sh.600000"
}
start_date = "2018-01-01"
end_date = "2023-01-01"
# 定义函数获取单只股票的收盘价数据
def get_stock_data(code, start, end):
# 获取K线数据(日线),返回格式为DataFrame
rs = bs.query_history_k_data_plus(
code,
"date,close", # 只获取日期和收盘价
start_date=start,
end_date=end,
frequency="d", # 日线
adjustflag="3" # 复权类型:3为后复权
)
# 转为DataFrame
data = rs.get_data()
# 日期列转为datetime格式,收盘价转为浮点数
data["date"] = pd.to_datetime(data["date"])
data["close"] = data["close"].astype(float)
# 以日期为索引
data = data.set_index("date")
return data["close"]
# 获取两只股票的数据并合并
df_list = []
for name, code in stock_codes.items():
close = get_stock_data(code, start_date, end_date)
df_list.append(close.rename(name)) # 列名改为股票名称
# 合并为一个DataFrame(按日期对齐)
data = pd.concat(df_list, axis=1)
data = data.dropna() # 去除缺失值
# 退出baostock登录
bs.logout()
print("数据形状:", data.shape)
print("前5行数据:")
print(data.head())
# ----------------------
# 步骤2:协整检验(验证配对有效性)
# ----------------------
# 协整检验:原假设为"不协整",p值<0.05则拒绝原假设(存在协整关系)
score, pvalue, _ = coint(data["招商银行"], data["浦发银行"])
print(f"\n协整检验p值:{pvalue:.4f}") # p<0.05说明适合配对交易
# ----------------------
# 步骤3:计算价差(残差)和Z-score
# ----------------------
# 用OLS回归计算两只股票的线性关系:浦发银行 = a + b*招商银行 + 残差
x = data["招商银行"]
y = data["浦发银行"]
model = OLS(y, sm.add_constant(x)).fit() # 加入常数项
a = model.params["const"] # 截距
b = model.params["招商银行"] # 斜率
# 价差 = 实际值 - 回归预测值(残差)
spread = y - (a + b * x)
# 计算Z-score(标准化价差):用滚动窗口(60天)计算均值和标准差
window = 60
spread_mean = spread.rolling(window=window).mean()
spread_std = spread.rolling(window=window).std()
z_score = (spread - spread_mean) / spread_std
# 可视化股价、价差和Z-score
plt.figure(figsize=(12, 10))
# 子图1:两只股票的价格走势
plt.subplot(3, 1, 1)
data["招商银行"].plot(label="招商银行")
data["浦发银行"].plot(label="浦发银行")
plt.title("股价走势(后复权)")
plt.legend()
plt.grid(alpha=0.3)
# 子图2:价差及其均值
plt.subplot(3, 1, 2)
spread.plot(label="价差(残差)", color="g")
spread_mean.plot(label=f"{window}天均值", linestyle="--", color="r")
plt.title("价差及其滚动均值")
plt.legend()
plt.grid(alpha=0.3)
# 子图3:Z-score(交易信号触发指标)
plt.subplot(3, 1, 3)
z_score.plot(label="Z-score", color="b")
plt.axhline(2, color="r", linestyle="--", label="上阈值(+2)")
plt.axhline(-2, color="r", linestyle="--", label="下阈值(-2)")
plt.axhline(0, color="k", linestyle="--")
plt.title("Z-score(触发交易信号)")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
# ----------------------
# 步骤4:生成交易信号
# ----------------------
# 信号规则:
# - Z-score > 2:价差过大,认为招商银行高估、浦发银行低估 → 做空招商银行,做多浦发银行(信号=1)
# - Z-score < -2:价差过小,认为招商银行低估、浦发银行高估 → 做多招商银行,做空浦发银行(信号=-1)
# - |Z-score| < 0.5:回归均值附近,平仓(信号=0)
threshold_high = 2.0
threshold_low = -2.0
exit_threshold = 0.5
signal = pd.Series(0, index=z_score.index)
signal[z_score > threshold_high] = 1 # 多浦发,空招商
signal[z_score < threshold_low] = -1 # 多招商,空浦发
signal[abs(z_score) < exit_threshold] = 0 # 平仓
# 保持持仓(未触发平仓信号时,延续上一日的持仓)
signal = signal.ffill() # 前向填充
# ----------------------
# 步骤5:回测策略收益
# ----------------------
# 每日收益计算逻辑:
# - 信号=1时:收益 = 浦发银行涨跌幅 - 招商银行涨跌幅(做多浦发+做空招商)
# - 信号=-1时:收益 = 招商银行涨跌幅 - 浦发银行涨跌幅(做多招商+做空浦发)
# - 信号=0时:无持仓,收益=0
returns = pd.Series(0.0, index=data.index)
returns = signal.shift(1) * (data["浦发银行"].pct_change() - data["招商银行"].pct_change())
# 计算累计收益
cum_returns = (1 + returns).cumprod()
# 可视化累计收益
plt.figure(figsize=(10, 6))
cum_returns.plot(label="策略累计收益", color="g")
plt.axhline(1, color="k", linestyle="--", label="初始资金")
plt.title("配对交易策略累计收益(2018-2023)")
plt.xlabel("日期")
plt.ylabel("累计收益倍数")
plt.legend()
plt.grid(alpha=0.3)
plt.show()
# 输出关键收益指标
total_return = (cum_returns.iloc[-1] - 1) * 100
print(f"\n策略总收益:{total_return:.2f}%")
print(f"策略最终累计收益倍数:{cum_returns.iloc[-1]:.2f}倍")
三、代码说明
-
数据获取 :
使用baostock的
query_history_k_data_plus接口获取A股后复权收盘价(复权更贴近实际持仓收益),选择招商银行(sh.600036)和浦发银行(sh.600000)作为配对标的(同属银行业,业务相似度高)。 -
核心逻辑不变 :
保持协整检验、价差计算(回归残差)、Z-score信号生成、收益回测等步骤与原逻辑一致,仅替换数据来源为baostock。
-
注意事项:
- baostock需要网络连接,首次使用需注册(免费),登录后才能获取数据。
- 若协整检验p值>0.05,说明两只股票不适合配对交易,需更换标的(如其他银行股或同行业股票)。
- 实际交易需考虑A股做空限制(普通投资者需开通融资融券才能做空)。
运行代码前请确保网络通畅,baostock登录成功后即可获取数据并回测策略效果。