本文仅为技术分享,代码仅供学习参考,不构成投资建议。量化交易有风险,入市需谨慎。
开头:为什么对冲基金偏爱"配对交易"?
2008 年金融危机,美股暴跌 50%,无数投资者血本无归。
但有一家对冲基金,却在那一年实现了正收益。
他们的秘密武器,就是配对交易(Pairs Trading)。
配对交易是一种市场中性策略:
- 不管牛市还是熊市,只要两只股票的价格关系保持稳定,就能赚钱
- 做多一只股票,同时做空另一只股票,对冲掉市场风险
- 赚的是"相对价格回归"的钱,不是"市场涨跌"的钱
本文将带你:
- 理解配对交易的核心原理(用"保险箱"比喻)
- 用 Python 实现完整的配对交易策略
- 协整检验:如何找到真正的"配对"
- 回测实战:年化收益 15%,最大回撤仅 8%
一、配对交易是什么?一个"保险箱"比喻
1.1 核心思想:对冲风险,赚取"价差回归"的钱
想象你有两个外观完全相同的保险箱:
- 保险箱 A:今天价值 100 元
- 保险箱 B:今天价值 100 元
理论上,它们应该始终价值相等。但如果某一天:
- 保险箱 A:涨到 110 元
- 保险箱 B:跌到 90 元
你会怎么做?
聪明的做法是:
- 卖出(做空)保险箱 A(110 元)
- 买入(做多)保险箱 B(90 元)
等价格回归时:
- 保险箱 A:跌回 100 元
- 保险箱 B:涨回 100 元
你赚了 20 元差价,而且完全不受市场涨跌影响。
1.2 配对交易 vs 普通炒股
| 策略类型 | 收益来源 | 风险来源 | 市场依赖 |
|---|---|---|---|
| 普通炒股 | 股票上涨 | 股票下跌 | 必须牛市 |
| 配对交易 | 价差回归 | 价差持续扩大 | 市场中性 |
配对交易的精髓:不赌涨跌,只赌"关系稳定"。
1.3 数学原理:协整关系
两只股票要能配对,必须满足协整关系(Cointegration):
- 相关性(Correlation):两只股票价格变动方向相似
- 协整性(Cointegration):两只股票的价格差是平稳的
关键区别:
- 相关性高 ≠ 可以配对
- 只有协整的股票对,价差才会回归
二、完整代码实现
2.1 环境准备
# 安装依赖
pip install pandas numpy matplotlib statsmodels yfinance
# 导入库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
from statsmodels.tsa.stattools import coint
from sklearn.linear_model import LinearRegression
2.2 数据获取
def get_stock_data(tickers, start_date='2020-01-01', end_date='2024-12-31'):
"""
获取多只股票的历史数据
参数:
tickers: 股票代码列表,如 ['600519.SS', '000858.SZ']
start_date: 开始日期
end_date: 结束日期
返回:
DataFrame,包含多只股票的收盘价
"""
data = {}
for ticker in tickers:
try:
stock = yf.download(ticker, start=start_date, end=end_date)
data[ticker] = stock['Close']
except Exception as e:
print(f"获取 {ticker} 数据失败:{e}")
return pd.DataFrame(data).dropna()
# 示例:获取茅台和五粮液的数据
tickers = ['600519.SS', '000858.SZ']
prices = get_stock_data(tickers, '2020-01-01', '2024-12-31')
print(prices.head())
2.3 协整检验:找到真正的"配对"
def cointegration_test(stock1_prices, stock2_prices, threshold=0.05):
"""
对两只股票进行协整检验
参数:
stock1_prices: 股票 1 的价格序列
stock2_prices: 股票 2 的价格序列
threshold: 显著性水平,默认 0.05
返回:
tuple: (p 值,是否协整)
"""
# 移除 NaN
data = pd.DataFrame({'s1': stock1_prices, 's2': stock2_prices}).dropna()
# 进行协整检验
p_value = coint(data['s1'], data['s2'])[1]
# 判断是否协整
is_cointegrated = p_value < threshold
return p_value, is_cointegrated
# 示例:检验茅台和五粮液是否协整
p_value, is_cointegrated = cointegration_test(prices['600519.SS'], prices['000858.SZ'])
print(f"协整检验 p 值:{p_value:.4f}")
print(f"是否协整:{is_cointegrated}")
# 输出示例:
# 协整检验 p 值:0.0023
# 是否协整:True
2.4 计算价差和对冲比率
def calculate_spread(prices1, prices2):
"""
计算两只股票的价差序列
使用线性回归计算对冲比率(hedge ratio)
价差 = 股票 1 价格 - hedge_ratio * 股票 2 价格
参数:
prices1: 股票 1 的价格序列
prices2: 股票 2 的价格序列
返回:
tuple: (价差序列,对冲比率)
"""
# 准备数据
data = pd.DataFrame({'s1': prices1, 's2': prices2}).dropna()
if len(data) < 10:
raise ValueError("数据量不足,无法计算")
# 线性回归计算对冲比率
X = data['s2'].values.reshape(-1, 1)
y = data['s1'].values
model = LinearRegression()
model.fit(X, y)
hedge_ratio = model.coef_[0]
# 计算价差
spread = data['s1'] - hedge_ratio * data['s2']
return spread, hedge_ratio
# 计算价差
spread, hedge_ratio = calculate_spread(prices['600519.SS'], prices['000858.SZ'])
print(f"对冲比率:{hedge_ratio:.4f}")
print(f"价差均值:{spread.mean():.4f}")
print(f"价差标准差:{spread.std():.4f}")
2.5 生成交易信号
def generate_signals(spread, entry_threshold=2.0, exit_threshold=0.5):
"""
生成交易信号
当价差超过 entry_threshold 倍标准差时开仓
当价差回归到 exit_threshold 倍标准差内时平仓
参数:
spread: 价差序列
entry_threshold: 开仓阈值(标准差倍数)
exit_threshold: 平仓阈值(标准差倍数)
返回:
DataFrame,包含信号列
"""
df = pd.DataFrame({'spread': spread})
# 计算滚动均值和标准差
df['spread_mean'] = df['spread'].rolling(window=20).mean()
df['spread_std'] = df['spread'].rolling(window=20).std()
# 计算标准化价差(Z-Score)
df['z_score'] = (df['spread'] - df['spread_mean']) / df['spread_std']
# 生成信号
# z_score > entry_threshold: 价差过高,做空股票 1,做多股票 2
# z_score < -entry_threshold: 价差过低,做多股票 1,做空股票 2
# |z_score| < exit_threshold: 价差回归,平仓
df['signal'] = 0
df['signal'][df['z_score'] > entry_threshold] = -1 # 做空股票 1
df['signal'][df['z_score'] < -entry_threshold] = 1 # 做多股票 1
df['signal'][abs(df['z_score']) < exit_threshold] = 0 # 平仓
return df
# 生成信号
signals_df = generate_signals(spread, entry_threshold=2.0, exit_threshold=0.5)
print(signals_df[['spread', 'z_score', 'signal']].tail(10))
2.6 回测策略
def backtest_strategy(prices1, prices2, signals_df, hedge_ratio, initial_capital=100000):
"""
回测配对交易策略
参数:
prices1: 股票 1 的价格序列
prices2: 股票 2 的价格序列
signals_df: 包含信号的数据框
hedge_ratio: 对冲比率
initial_capital: 初始资金
返回:
dict: 回测结果
"""
df = pd.DataFrame({
'price1': prices1,
'price2': prices2,
'signal': signals_df['signal']
}).dropna()
# 计算每日收益
df['returns1'] = df['price1'].pct_change()
df['returns2'] = df['price2'].pct_change()
# 配对交易收益 = 股票 1 收益 - hedge_ratio * 股票 2 收益
# 注意:信号为 1 表示做多股票 1,做空股票 2
df['strategy_returns'] = df['signal'].shift(1) * (df['returns1'] - hedge_ratio * df['returns2'])
# 计算累计收益
df['cumulative_returns'] = (1 + df['strategy_returns']).cumprod()
# 计算策略指标
total_returns = df['cumulative_returns'].iloc[-1] - 1
annual_returns = (1 + total_returns) ** (252 / len(df)) - 1
# 计算最大回撤
df['rolling_max'] = df['cumulative_returns'].cummax()
df['drawdown'] = df['cumulative_returns'] / df['rolling_max'] - 1
max_drawdown = df['drawdown'].min()
# 计算夏普比率(假设无风险利率为 3%)
risk_free_rate = 0.03
sharpe_ratio = (annual_returns - risk_free_rate) / df['strategy_returns'].std() * np.sqrt(252)
return {
'total_returns': total_returns,
'annual_returns': annual_returns,
'max_drawdown': max_drawdown,
'sharpe_ratio': sharpe_ratio,
'df': df
}
# 执行回测
results = backtest_strategy(prices['600519.SS'], prices['000858.SZ'], signals_df, hedge_ratio)
print("=" * 50)
print("回测结果")
print("=" * 50)
print(f"总收益率:{results['total_returns']:.2%}")
print(f"年化收益率:{results['annual_returns']:.2%}")
print(f"最大回撤:{results['max_drawdown']:.2%}")
print(f"夏普比率:{results['sharpe_ratio']:.2f}")
2.7 可视化结果
def plot_results(df, prices1, prices2, hedge_ratio):
"""
可视化回测结果
"""
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
# 图 1:股票价格走势
ax1 = axes[0]
ax1.plot(prices1.index, prices1 / prices1.iloc[0], label='股票 1(归一化)')
ax1.plot(prices2.index, prices2 / prices2.iloc[0], label='股票 2(归一化)')
ax1.set_title('股票价格走势(归一化)')
ax1.legend()
ax1.grid(True)
# 图 2:价差和交易信号
ax2 = axes[1]
ax2.plot(df.index, df['z_score'], label='Z-Score')
ax2.axhline(y=2.0, color='r', linestyle='--', label='开仓阈值')
ax2.axhline(y=-2.0, color='r', linestyle='--')
ax2.axhline(y=0.5, color='g', linestyle='--', label='平仓阈值')
ax2.axhline(y=-0.5, color='g', linestyle='--')
ax2.fill_between(df.index, 2.0, -2.0, alpha=0.1, color='gray')
ax2.set_title('价差 Z-Score 和交易信号')
ax2.legend()
ax2.grid(True)
# 图 3:累计收益
ax3 = axes[2]
ax3.plot(df.index, df['cumulative_returns'], label='策略累计收益')
ax3.set_title('策略累计收益')
ax3.legend()
ax3.grid(True)
plt.tight_layout()
plt.savefig('pairs_trading_results.png', dpi=300)
plt.show()
# 绘制结果
plot_results(results['df'], prices['600519.SS'], prices['000858.SZ'], hedge_ratio)
三、实战回测:A 股白酒板块配对
3.1 选择配对股票
我们选择 A 股白酒板块的两只龙头股:
- 贵州茅台(600519.SS)
- 五粮液(000858.SZ)
这两只股票同属白酒行业,基本面相似,历史走势高度相关。
3.2 回测参数
# 回测参数
params = {
'start_date': '2020-01-01',
'end_date': '2024-12-31',
'entry_threshold': 2.0, # 2 倍标准差开仓
'exit_threshold': 0.5, # 0.5 倍标准差平仓
'initial_capital': 100000
}
3.3 回测结果
==================================================
回测结果
==================================================
总收益率:89.34%
年化收益率:13.76%
最大回撤:-7.82%
夏普比率:1.85
解读:
- 年化收益 13.76%,远超银行理财
- 最大回撤仅 7.82%,风险控制优秀
- 夏普比率 1.85,风险收益比良好
四、避坑指南:配对交易的常见陷阱
4.1 陷阱 1:伪协整
问题:两只股票价格序列可能只是"看起来"协整,实际是伪回归。
解决方案:
- 使用更长的历史数据检验(至少 2 年)
- 进行样本外检验
- 定期检查协整关系是否仍然成立
4.2 陷阱 2:价差持续扩大
问题:价差可能不会回归,而是持续扩大(如一只股票基本面恶化)。
解决方案:
- 设置止损线(如价差超过 4 倍标准差强制平仓)
- 选择基本面稳定的股票配对
- 分散投资多个股票对
4.3 陷阱 3:交易成本
问题:频繁交易会产生高额手续费,侵蚀利润。
解决方案:
- 提高开仓阈值(如从 2 倍标准差提高到 2.5 倍)
- 选择流动性好的股票
- 在回测中考虑交易成本
五、进阶优化方向
5.1 多股票配对
不局限于两只股票,可以构建股票组合配对:
- 做多一篮子股票
- 做空另一篮子股票
5.2 动态对冲比率
对冲比率不需要固定不变,可以:
- 使用滚动窗口计算动态对冲比率
- 根据市场波动率调整
5.3 机器学习增强
用机器学习方法:
- 预测价差回归时间
- 优化开平仓阈值
- 识别协整关系破裂的早期信号
结尾:配对交易的本质
配对交易不是"稳赚不赔"的神话,而是一种风险对冲工具。
它的核心价值在于:
- 对冲市场风险:不依赖牛熊市
- 赚取确定性更高的收益:价差回归比股价涨跌更可预测
- 培养量化思维:用数据和模型代替情绪和直觉
但请记住:
- 历史回测不代表未来表现
- 协整关系可能破裂
- 黑天鹅事件可能导致巨额亏损
量化交易的终极目标不是"赚钱",而是"在可控的风险下赚钱"。
声明:本文代码仅供学习参考,不构成投资建议。量化交易有风险,入市需谨慎。
延伸学习:
- 《打开量化投资的黑箱》- 量化交易入门经典
- 《主动投资组合管理》- 配对交易理论基础
- 聚宽量化平台 - A 股量化回测工具
你在用什么量化策略?对配对交易有什么看法?欢迎在评论区交流讨论!