问题描述
解决方案
这是一个基于 Z-Score(标准分数) 模型的配对交易(Pairs Trading)策略。
策略原理
配对交易是一种基于均值回归的市场中性策略。
- 选取标的:选取两只历史走势高度相关(协整)的股票(例如:浦发银行 和 招商银行)。
- 计算价差比率:计算两只股票价格的比值(Ratio = Price_A / Price_B)。
- 计算 Z-Score:基于过去 N 天的历史数据,计算当前比值的 Z-Score。
- $Z = \frac{\text{当前比值} - \text{比值均值}}{\text{比值标准差}}$
- 交易信号:
- 做空价差(Z > 阈值):比值过高,说明 A 贵 B 便宜。卖出 A,买入 B。
- 做多价差(Z < -阈值):比值过低,说明 A 便宜 B 贵。买入 A,卖出 B。
- 平仓(|Z| < 平仓阈值):比值回归正常水平,平掉所有仓位。
策略代码
import numpy as np
import pandas as pd
def initialize(context):
"""
初始化函数
"""
# 设定两只拟合度较高的股票(这里以银行股为例:浦发银行 和 招商银行)
g.stock1 = '600000.SS'
g.stock2 = '600036.SS'
g.security_list = [g.stock1, g.stock2]
# 设置股票池
set_universe(g.security_list)
# 策略参数
g.window = 20 # 计算均值和标准差的窗口期(天)
g.entry_threshold = 2.0 # 开仓阈值(Z-Score绝对值)
g.exit_threshold = 0.5 # 平仓阈值(Z-Score绝对值)
# 每次交易使用的资金比例(每只股票占总资金的比例)
g.position_pct = 0.4
def handle_data(context, data):
"""
盘中运行函数,每日或每分钟调用
"""
# 1. 获取历史收盘价数据
# 注意:获取数量需要包含窗口期,这里取 g.window
# 当 field 为单个字段时,返回的 DataFrame 列索引为股票代码
hist = get_history(g.window, '1d', 'close', g.security_list, fq='pre')
# 检查数据长度是否足够
if len(hist) < g.window:
log.info("历史数据不足,跳过计算")
return
# 2. 提取两只股票的价格序列
# 注意:需确保数据对齐,处理停牌导致的 NaN
price_s1 = hist[g.stock1].values
price_s2 = hist[g.stock2].values
# 简单的清洗:如果有 NaN (停牌),则不进行计算
if np.isnan(price_s1).any() or np.isnan(price_s2).any():
log.info("历史数据包含无效值(可能停牌),跳过")
return
# 3. 计算比价 (Ratio)
ratios = price_s1 / price_s2
# 4. 计算 Z-Score
# Z = (当前比值 - 均值) / 标准差
current_ratio = ratios[-1]
mean_ratio = np.mean(ratios)
std_ratio = np.std(ratios)
if std_ratio == 0:
return
z_score = (current_ratio - mean_ratio) / std_ratio
# 打印日志方便调试
log.info("当前 Z-Score: %.4f, 比值: %.4f" % (z_score, current_ratio))
# 5. 获取当前持仓和资金情况
position_s1 = get_position(g.stock1)
position_s2 = get_position(g.stock2)
total_value = context.portfolio.portfolio_value
target_value = total_value * g.position_pct
# 6. 交易逻辑
# --- 平仓逻辑 (回归均值) ---
if abs(z_score) < g.exit_threshold:
# 如果持有仓位,则平仓
if position_s1.amount != 0 or position_s2.amount != 0:
log.info("Z-Score 回归 (%.4f),执行平仓" % z_score)
order_target_value(g.stock1, 0)
order_target_value(g.stock2, 0)
# --- 开仓逻辑 ---
# 情况 A: Z-Score > 阈值 (Stock1 相对 Stock2 贵) -> 卖 S1,买 S2
elif z_score > g.entry_threshold:
# 如果当前没有持有正确的方向(即没有持有S2),则开仓
# 这里简化逻辑:只要不符合目标持仓就调整
if position_s2.amount == 0:
log.info("Z-Score > 阈值 (%.4f),做空价差:卖 %s,买 %s" % (z_score, g.stock1, g.stock2))
# 卖出 S1 (做空或清仓) -> PTrade A股通常不能直接融券做空,这里模拟为清仓S1
# 如果是两融账户可以使用 marginsec_open / margincash_open
# 此处演示普通账户逻辑:持有S1则卖出,买入S2
order_target_value(g.stock1, 0)
order_target_value(g.stock2, target_value)
# 情况 B: Z-Score < -阈值 (Stock1 相对 Stock2 便宜) -> 买 S1,卖 S2
elif z_score < -g.entry_threshold:
# 如果当前没有持有正确的方向(即没有持有S1)
if position_s1.amount == 0:
log.info("Z-Score < -阈值 (%.4f),做多价差:买 %s,卖 %s" % (z_score, g.stock1, g.stock2))
# 买入 S1,卖出 S2
order_target_value(g.stock1, target_value)
order_target_value(g.stock2, 0)
def after_trading_end(context, data):
"""
盘后处理
"""
log.info("今日交易结束")
代码要点解析
-
数据获取 (
get_history):- 我们使用了
get_history(g.window, '1d', 'close', g.security_list)。 - 在 PTrade 中,当
field为单个字符串且security_list为列表时,返回的 DataFrame 索引是时间,列名是股票代码。这非常方便直接进行向量化计算。
- 我们使用了
-
Z-Score 计算:
- 使用了
numpy库来计算均值 (np.mean) 和标准差 (np.std)。 - 这是统计套利中最核心的指标,用于衡量当前价差偏离历史均值的程度。
- 使用了
-
交易执行 (
order_target_value):- 使用
order_target_value可以让系统自动计算需要买卖的数量以达到目标持仓金额。 - 注意:上述代码是基于普通股票账户编写的(只能做多,不能做空)。
- 当信号提示“卖出 A,买入 B”时,代码逻辑是:清空 A 的持仓,买入 B。
- 如果你使用的是两融账户(信用账户),可以将
order_target_value替换为marginsec_open(融券卖出) 和margincash_open(融资买入) 来实现真正的多空对冲。
- 使用
-
兼容性:
- 代码遵循 Python 3.5 语法规范(例如使用
%进行字符串格式化,而不是 f-string),确保在 PTrade 旧版本环境中也能运行。
- 代码遵循 Python 3.5 语法规范(例如使用
常见问题 (Q&A)
Q: 如何选择配对的股票?
A: 通常选择同行业、业务相似、市值接近的股票。可以通过协整检验(Cointegration Test)来筛选标的,但在 PTrade 策略中,通常在 initialize 之前通过研究环境选好,直接填入代码。
Q: 为什么使用 order_target_value 而不是 order?
A: order_target_value 更适合这种策略,因为它会自动处理调仓。例如,如果当前已经持有 10000 元的股票 A,策略判断需要继续持有,该函数会自动忽略或微调,避免重复下单导致仓位过重。
Q: 这个策略在回测和实盘中有什么区别?
A: 在回测中,get_history 获取的是复权后的历史数据。在实盘中,如果遇到除权除息,价格比率可能会发生跳跃,导致 Z-Score 异常。建议在实盘代码中加入对除权除息的处理,或者使用复权数据(fq='pre')。上述代码已默认使用前复权。