问题描述
解决方案
这是一个基于 PTrade 量化交易平台的**配对交易(Pairs Trading)**策略示例。
策略原理
配对交易是一种统计套利策略。它寻找两只历史价格走势高度相关(协整)的股票。
- 计算价差/比率:计算两只股票价格的比率(Ratio = Price A / Price B)。
- 均值回归:假设这个比率会围绕一个均值波动。
- 交易信号:
- 当比率过高(偏离均值 +N 倍标准差)时,说明 A 贵 B 便宜,因此卖出 A,买入 B。
- 当比率过低(偏离均值 -N 倍标准差)时,说明 A 便宜 B 贵,因此买入 A,卖出 B。
- 当比率回归均值时,平仓或平衡持仓。
PTrade 策略代码
为了确保策略在普通股票账户(非融资融券账户)也能回测运行,本示例采用**“轮动持仓”**模式(即全仓持有相对低估的那一只股票,或者在均值附近各持一半),这是一种适合A股普通账户的配对交易变体。
import numpy as np
import pandas as pd
def initialize(context):
"""
初始化函数,设置股票池和参数
"""
# 1. 选取两只相关性较高的股票(这里以浦发银行和华夏银行为例)
# 在实际实盘中,这对股票需要通过协整性检验来筛选
g.stock1 = '600000.SS' # 浦发银行
g.stock2 = '600015.SS' # 华夏银行
g.security = [g.stock1, g.stock2]
# 设置股票池
set_universe(g.security)
# 2. 策略参数设置
g.window = 20 # 计算均值和标准差的移动窗口(过去20天)
g.entry_std = 1.5 # 开仓阈值:偏离 1.5 倍标准差
g.exit_std = 0.5 # 平仓/平衡阈值:回归到 0.5 倍标准差以内
# 设置手续费(可选,回测默认有设置)
# set_commission(commission_ratio=0.0003, min_commission=5.0)
def handle_data(context, data):
"""
盘中运行函数,每日或每分钟执行
"""
# 1. 获取历史数据
# 分别获取两只股票过去 g.window 天的收盘价
# include=True 表示包含当前周期的最新价格,保证信号实时性
hist1 = get_history(g.window, '1d', 'close', g.stock1, include=True)
hist2 = get_history(g.window, '1d', 'close', g.stock2, include=True)
# 如果数据长度不足(例如刚上市或停牌),则不操作
if len(hist1) < g.window or len(hist2) < g.window:
return
# 2. 计算统计指标
# 提取收盘价序列 (numpy array)
s1 = hist1['close'].values
s2 = hist2['close'].values
# 计算比率序列 (Stock1 / Stock2)
ratios = s1 / s2
# 计算比率的均值和标准差
mean_ratio = ratios.mean()
std_ratio = ratios.std()
# 获取当前最新的比率
current_ratio = ratios[-1]
# 防止标准差为0导致除零错误
if std_ratio == 0:
return
# 计算 Z-Score (标准分),衡量当前比率偏离均值的程度
z_score = (current_ratio - mean_ratio) / std_ratio
# 获取当前账户总资产价值
total_value = context.portfolio.portfolio_value
# 3. 交易逻辑 (基于 Z-Score 进行仓位轮动)
# 情况 A: Z-Score > 1.5
# 说明 Stock1 相对 Stock2 价格过高(Stock1 贵,Stock2 便宜)
# 策略:清仓 Stock1,全仓买入 Stock2
if z_score > g.entry_std:
# 卖出 Stock1 (如果持有)
order_target_value(g.stock1, 0)
# 买入 Stock2
order_target_value(g.stock2, total_value)
log.info("Z-Score: %.2f > %.2f. Stock1偏贵. 持仓切换至: %s" % (z_score, g.entry_std, g.stock2))
# 情况 B: Z-Score < -1.5
# 说明 Stock1 相对 Stock2 价格过低(Stock1 便宜,Stock2 贵)
# 策略:清仓 Stock2,全仓买入 Stock1
elif z_score < -g.entry_std:
# 卖出 Stock2 (如果持有)
order_target_value(g.stock2, 0)
# 买入 Stock1
order_target_value(g.stock1, total_value)
log.info("Z-Score: %.2f < -%.2f. Stock1偏宜. 持仓切换至: %s" % (z_score, g.entry_std, g.stock1))
# 情况 C: -0.5 < Z-Score < 0.5
# 说明比率回归正常区间
# 策略:平衡持仓,各持有一半仓位 (或者可以选择空仓观望)
elif abs(z_score) < g.exit_std:
target_cash = total_value / 2
order_target_value(g.stock1, target_cash)
order_target_value(g.stock2, target_cash)
# log.info("Z-Score: %.2f 回归正常区间. 平衡持仓." % z_score)
def before_trading_start(context, data):
"""
盘前处理(可选)
"""
pass
代码关键点解析
- 数据获取 (
get_history):- 我们分别获取两只股票的历史收盘价。这里使用了
include=True,这意味着计算 Z-Score 时使用的是包含当前最新价格的数据,这对于日内回测或实盘非常重要。
- 我们分别获取两只股票的历史收盘价。这里使用了
- Z-Score 计算:
- 公式:$Z = \frac{\text{当前比率} - \text{均值}}{\text{标准差}}$
- 这是配对交易的核心。它将价格比率标准化,告诉我们当前两只股票的价差是否处于“异常”状态。
- 交易执行 (
order_target_value):- 使用
order_target_value是最方便的调仓方式。它会自动计算需要买入或卖出的数量,将持仓调整到目标金额。 - 非融券模式:上述代码是“做多相对低估品种,卖出相对高估品种”。如果是融资融券账户,可以将逻辑修改为:Z分过高时,
marginsec_open(融券卖出) Stock1 并margincash_open(融资买入) Stock2。
- 使用
Q&A
Q1: 如何在 PTrade 中进行真正的“多空”配对交易(即同时做多一只和做空另一只)?
A1: 如果你的账户开通了融资融券权限,可以使用信用交易接口。
- 融券卖出:使用
marginsec_open(security, amount)。 - 买券还券:使用
marginsec_close(security, amount)。 - 融资买入:使用
margincash_open(security, amount)。 - 卖券还款:使用
margincash_close(security, amount)。
在handle_data中,你需要判断 Z-Score 方向,然后同时下达一笔融券卖出指令和一笔融资买入指令。
Q2: 为什么代码中分别调用两次 get_history 而不是一次获取所有股票?
A2: PTrade 的 get_history 接口在不同版本(Python 3.5 vs Python 3.11)以及传入单个股票 vs 股票列表时,返回的数据结构(DataFrame 的列索引结构)可能略有不同。为了保证代码在各种 PTrade 环境下的健壮性,分别获取单只股票的数据是最稳妥的做法。
Q3: 如何选择配对的股票?
A3: 在 initialize 中硬编码股票代码仅用于演示。在实际研究中,通常需要先在研究环境(Notebook)中进行协整性检验 (Cointegration Test)。常用的方法是使用 statsmodels 库中的 coint 函数来测试两只股票的历史价格序列是否具有长期稳定的线性关系。只有通过检验的股票对才适合做配对交易。