问题描述
解决方案
这是一个非常经典的**期现套利(Arbitrage)**策略需求。
策略逻辑分析
在编写代码之前,我们需要明确套利的核心逻辑:
- 标的对:
- 期货:例如沪深300股指期货(
IF主力合约)。 - 现货(ETF):例如华泰柏瑞沪深300ETF(
510300.SH)。
- 期货:例如沪深300股指期货(
- 价差计算:
- ETF的价格通常是指数的 1/1000 左右。
- 基差/价差率 = $(期货价格 - ETF价格 \times 1000) / (ETF价格 \times 1000)$。
- 交易信号:
- 正套开仓(开多现货,开空期货):当价差率大于设定的开仓阈值(例如 1%),说明期货明显贵,买入 ETF,卖空期货。
- 平仓(获利了结):当价差率回归到平仓阈值(例如 0.1%)以内,卖出 ETF,买入平仓期货。
- 对冲比例:
- IF 合约乘数为 300。
- 1 手 IF 期货价值 $\approx$ 指数点位 $\times$ 300。
- 对应需要的 ETF 数量 $\approx$ (期货价格 $\times$ 300) / ETF价格。
QMT 策略代码实现
以下是一个完整的 Python 策略代码。该策略会实时监控价差,并执行正向套利(买 ETF,空期货)。
注意:为了简化演示,本策略假设你已经持有 ETF 或资金充足,且主要演示正套逻辑(因为国内融券卖出 ETF 难度较大,反套较难实现)。
# -*- coding: gbk -*-
import time
def init(ContextInfo):
# ================= 策略参数设置 =================
# 股票资金账号 (请修改为你的实际账号)
ContextInfo.stock_account = 'YOUR_STOCK_ACCOUNT'
# 期货资金账号 (请修改为你的实际账号)
ContextInfo.future_account = 'YOUR_FUTURE_ACCOUNT'
# 标的设置
ContextInfo.etf_code = '510300.SH' # 沪深300 ETF
ContextInfo.future_code = 'IF00.IF' # IF 主力合约
# 合约乘数 (IF为300, IC为200, IH为300)
ContextInfo.multiplier = 300
# 阈值设置 (百分比)
ContextInfo.open_threshold = 0.015 # 价差超过 1.5% 开仓 (正套)
ContextInfo.close_threshold = 0.002 # 价差回归到 0.2% 平仓
# 交易数量设置
ContextInfo.trade_lot = 1 # 每次交易 1 手期货
# 状态标记
ContextInfo.is_holding = False # 是否持有套利仓位
# 绑定账号 (实盘必须)
ContextInfo.set_account(ContextInfo.stock_account)
ContextInfo.set_account(ContextInfo.future_account)
print("策略初始化完成: 监控 {} 与 {}".format(ContextInfo.future_code, ContextInfo.etf_code))
def handlebar(ContextInfo):
# 只在实时行情的最后一根K线运行,避免历史回测重复触发
if not ContextInfo.is_last_bar():
return
# ================= 获取实时行情 =================
# 获取最新的 Tick 数据
tick_data = ContextInfo.get_full_tick([ContextInfo.etf_code, ContextInfo.future_code])
if ContextInfo.etf_code not in tick_data or ContextInfo.future_code not in tick_data:
return
etf_tick = tick_data[ContextInfo.etf_code]
future_tick = tick_data[ContextInfo.future_code]
# 获取最新价
etf_price = etf_tick['lastPrice']
future_price = future_tick['lastPrice']
# 异常值过滤
if etf_price <= 0 or future_price <= 0:
return
# ================= 计算价差 =================
# 假设 ETF 价格约为指数的 1/1000,将其折算为指数同级价格进行比较
# 注意:不同 ETF 的折算比例不同,510300 通常接近 1/1000
adjusted_etf_price = etf_price * 1000
# 计算价差率 (Premium Rate)
spread_rate = (future_price - adjusted_etf_price) / adjusted_etf_price
# 打印监控日志 (可选,避免日志过多可注释掉)
# print(f"期货: {future_price}, ETF折算: {adjusted_etf_price}, 价差率: {spread_rate:.4%}")
# ================= 仓位查询与状态更新 =================
# 在实盘中,建议通过 get_trade_detail_data 校验真实持仓,这里为了演示使用变量控制
# 如果重启策略,ContextInfo.is_holding 会重置,实盘需增加持仓恢复逻辑
# ================= 交易逻辑 =================
# 1. 开仓逻辑:价差过大,执行正套 (卖期货,买 ETF)
if not ContextInfo.is_holding and spread_rate > ContextInfo.open_threshold:
print(">>> 触发开仓信号: 价差率 {:.2%} > 阈值 {:.2%}".format(spread_rate, ContextInfo.open_threshold))
# 计算需要买入的 ETF 数量
# 1手期货价值 = 期货价格 * 300
# 对应 ETF 股数 = (期货价格 * 300) / ETF价格
# 向下取整到 100 的倍数
hedge_value = future_price * ContextInfo.multiplier
etf_volume = int(hedge_value / etf_price / 100) * 100
# 下单:买入 ETF
print(f"买入 ETF: {ContextInfo.etf_code}, 数量: {etf_volume}")
order_shares(ContextInfo.etf_code, etf_volume, 'fix', etf_price, ContextInfo, ContextInfo.stock_account)
# 下单:卖出开仓 期货
print(f"卖出开仓 期货: {ContextInfo.future_code}, 数量: {ContextInfo.trade_lot}")
sell_open(ContextInfo.future_code, ContextInfo.trade_lot, 'fix', future_price, ContextInfo, ContextInfo.future_account)
ContextInfo.is_holding = True
# 2. 平仓逻辑:价差回归,获利了结 (买平期货,卖 ETF)
elif ContextInfo.is_holding and spread_rate < ContextInfo.close_threshold:
print(">>> 触发平仓信号: 价差率 {:.2%} < 阈值 {:.2%}".format(spread_rate, ContextInfo.close_threshold))
# 获取当前持仓以确定平仓数量 (这里简化为使用开仓时的计算逻辑,实盘应查询实际持仓)
# 查询期货空单持仓
pos_list = get_trade_detail_data(ContextInfo.future_account, 'future', 'position')
future_hold_vol = 0
for pos in pos_list:
if pos.m_strInstrumentID == ContextInfo.future_code and pos.m_nDirection == 49: # 49代表卖方向
future_hold_vol = pos.m_nVolume
# 查询 ETF 持仓
pos_list_stock = get_trade_detail_data(ContextInfo.stock_account, 'stock', 'position')
etf_hold_vol = 0
for pos in pos_list_stock:
if pos.m_strInstrumentID == ContextInfo.etf_code:
etf_hold_vol = pos.m_nVolume
# 执行平仓
if future_hold_vol > 0:
print(f"买入平仓 期货: {ContextInfo.future_code}, 数量: {future_hold_vol}")
# 平仓优先平今 (buy_close_tdayfirst) 或 平昨 (buy_close_ydayfirst),视交易所规则而定
# 这里使用通用买入平仓
buy_close_tdayfirst(ContextInfo.future_code, future_hold_vol, 'fix', future_price, ContextInfo, ContextInfo.future_account)
if etf_hold_vol > 0:
print(f"卖出 ETF: {ContextInfo.etf_code}, 数量: {etf_hold_vol}")
order_shares(ContextInfo.etf_code, -etf_hold_vol, 'fix', etf_price, ContextInfo, ContextInfo.stock_account)
ContextInfo.is_holding = False
代码关键点解析
-
数据获取 (
get_full_tick):- 套利策略对时效性要求极高,因此使用
get_full_tick获取最新的盘口数据(Tick级),而不是使用 K 线数据。 ContextInfo.is_last_bar()确保代码只在最新的时间点运行,防止在回测加载历史数据时重复下单。
- 套利策略对时效性要求极高,因此使用
-
价差计算:
- 代码中使用了
adjusted_etf_price = etf_price * 1000。这是因为 510300 ETF 的价格大约是沪深300指数点位的千分之一。如果交易的是 IC(中证500)和 510500,比例也是类似的,但如果是其他品种,请务必确认折算比例。
- 代码中使用了
-
对冲比例计算:
hedge_value = future_price * ContextInfo.multiplier:计算 1 手期货的合约价值。etf_volume = int(hedge_value / etf_price / 100) * 100:计算等市值的 ETF 股数,并向下取整到 100 股(A股最小交易单位)。这是为了保持市值中性。
-
下单函数:
- 股票:使用
order_shares,正数买入,负数卖出。 - 期货:使用
sell_open(卖出开仓)和buy_close_tdayfirst(买入平仓)。 - 价格类型:示例中使用了
'fix'(指定价) 配合最新价下单。在实盘高频套利中,为了保证成交,通常会使用'COMPETE'(对手价) 或者在最新价基础上加减滑点。
- 股票:使用
-
账号设置:
- QMT 中股票和期货通常是两个不同的资金账号。需要在
init中分别设置,并在下单函数中指定对应的accId。
- QMT 中股票和期货通常是两个不同的资金账号。需要在
实盘注意事项
- 滑点与冲击成本:ETF 和期货都有买卖价差,实际成交价格可能不如预期。在计算阈值(
open_threshold)时,必须覆盖掉双边的手续费和预估滑点,否则会亏损。 - 资金占用:买入 ETF 需要全额资金,卖空期货需要保证金。请确保两个账户都有足够的资金。
- ETF 交易限制:ETF 是 T+1 交易(部分跨境 ETF 是 T+0)。如果是 T+1 的 ETF,当天买入后无法卖出,这意味着你无法做日内回转套利,必须持有过夜,这会面临隔夜风险。
- 解决方案:如果需要日内套利,需要底仓(即预先持有 ETF)。
- 期货合约月份:代码中写的是
IF00.IF(主力连续)。在临近交割日时,主力合约会换月,需要注意移仓换月的问题,或者明确指定合约代码(如IF2312.IF)。 - 状态恢复:示例代码用
ContextInfo.is_holding变量记录状态。如果软件崩溃重启,这个变量会重置为False,导致逻辑出错。实盘中应在init或handlebar开头通过get_trade_detail_data查询真实持仓来初始化状态。
Q&A
Q: 如何实现反向套利(期货贴水,买期货卖 ETF)?
A: 逻辑相反:当 spread_rate < -阈值 时,buy_open 期货,order_shares 卖出 ETF。但前提是你必须持有 ETF 底仓,或者拥有融券资格(融券成本通常较高,会压缩套利空间)。
Q: 为什么不用 passorder 函数?
A: passorder 是通用下单函数,功能强大但参数复杂。对于初学者或逻辑清晰的策略,使用封装好的 buy_open, order_shares 等函数可读性更好,不易出错。
Q: 如何提高成交速度?
A: 可以使用 ContextInfo.subscribe_quote 订阅 Tick 数据回调,而不是在 handlebar 里轮询,这样反应速度更快。同时,下单价格可以使用对手价(COMPETE)或超价下单。