配对交易全攻略:用 Python 实现"市场中性"策略,年化收益 15% 最大回撤仅 8%(完整代码)

1 阅读1分钟

本文仅为技术分享,代码仅供学习参考,不构成投资建议。量化交易有风险,入市需谨慎。


开头:为什么对冲基金偏爱"配对交易"?

2008 年金融危机,美股暴跌 50%,无数投资者血本无归。

但有一家对冲基金,却在那一年实现了正收益

他们的秘密武器,就是配对交易(Pairs Trading)

配对交易是一种市场中性策略

  • 不管牛市还是熊市,只要两只股票的价格关系保持稳定,就能赚钱
  • 做多一只股票,同时做空另一只股票,对冲掉市场风险
  • 赚的是"相对价格回归"的钱,不是"市场涨跌"的钱

本文将带你:

  1. 理解配对交易的核心原理(用"保险箱"比喻)
  2. 用 Python 实现完整的配对交易策略
  3. 协整检验:如何找到真正的"配对"
  4. 回测实战:年化收益 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 股量化回测工具

你在用什么量化策略?对配对交易有什么看法?欢迎在评论区交流讨论!