🚀 AI 一键生成 qmt 策略代码

QMT 期现套利策略:如何利用股指期货和 ETF 实现无风险套利?

qmt | 发布时间: 2025/12/11
以下内容由 EasyQuant 生成。

问题描述

如何编写一个监控股指期货和对应 ETF 价差,并在价差过大时自动下单的套利策略?

解决方案

这是一个非常经典的**期现套利(Arbitrage)**策略需求。

策略逻辑分析

在编写代码之前,我们需要明确套利的核心逻辑:

  1. 标的对
    • 期货:例如沪深300股指期货(IF 主力合约)。
    • 现货(ETF):例如华泰柏瑞沪深300ETF(510300.SH)。
  2. 价差计算
    • ETF的价格通常是指数的 1/1000 左右。
    • 基差/价差率 = $(期货价格 - ETF价格 \times 1000) / (ETF价格 \times 1000)$。
  3. 交易信号
    • 正套开仓(开多现货,开空期货):当价差率大于设定的开仓阈值(例如 1%),说明期货明显贵,买入 ETF,卖空期货。
    • 平仓(获利了结):当价差率回归到平仓阈值(例如 0.1%)以内,卖出 ETF,买入平仓期货。
  4. 对冲比例
    • 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

代码关键点解析

  1. 数据获取 (get_full_tick)

    • 套利策略对时效性要求极高,因此使用 get_full_tick 获取最新的盘口数据(Tick级),而不是使用 K 线数据。
    • ContextInfo.is_last_bar() 确保代码只在最新的时间点运行,防止在回测加载历史数据时重复下单。
  2. 价差计算

    • 代码中使用了 adjusted_etf_price = etf_price * 1000。这是因为 510300 ETF 的价格大约是沪深300指数点位的千分之一。如果交易的是 IC(中证500)和 510500,比例也是类似的,但如果是其他品种,请务必确认折算比例。
  3. 对冲比例计算

    • hedge_value = future_price * ContextInfo.multiplier:计算 1 手期货的合约价值。
    • etf_volume = int(hedge_value / etf_price / 100) * 100:计算等市值的 ETF 股数,并向下取整到 100 股(A股最小交易单位)。这是为了保持市值中性
  4. 下单函数

    • 股票:使用 order_shares,正数买入,负数卖出。
    • 期货:使用 sell_open(卖出开仓)和 buy_close_tdayfirst(买入平仓)。
    • 价格类型:示例中使用了 'fix' (指定价) 配合最新价下单。在实盘高频套利中,为了保证成交,通常会使用 'COMPETE' (对手价) 或者在最新价基础上加减滑点。
  5. 账号设置

    • QMT 中股票和期货通常是两个不同的资金账号。需要在 init 中分别设置,并在下单函数中指定对应的 accId

实盘注意事项

  1. 滑点与冲击成本:ETF 和期货都有买卖价差,实际成交价格可能不如预期。在计算阈值(open_threshold)时,必须覆盖掉双边的手续费和预估滑点,否则会亏损。
  2. 资金占用:买入 ETF 需要全额资金,卖空期货需要保证金。请确保两个账户都有足够的资金。
  3. ETF 交易限制:ETF 是 T+1 交易(部分跨境 ETF 是 T+0)。如果是 T+1 的 ETF,当天买入后无法卖出,这意味着你无法做日内回转套利,必须持有过夜,这会面临隔夜风险。
    • 解决方案:如果需要日内套利,需要底仓(即预先持有 ETF)。
  4. 期货合约月份:代码中写的是 IF00.IF(主力连续)。在临近交割日时,主力合约会换月,需要注意移仓换月的问题,或者明确指定合约代码(如 IF2312.IF)。
  5. 状态恢复:示例代码用 ContextInfo.is_holding 变量记录状态。如果软件崩溃重启,这个变量会重置为 False,导致逻辑出错。实盘中应在 inithandlebar 开头通过 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)或超价下单。