🚀 AI 一键生成 qmt 策略代码

如何在 QMT 中实现 ETF 与一篮子成分股之间的跨市场套利?

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

问题描述

请帮我编写一个 ETF 和它成分股之间的套利策略

解决方案

这是一个基于 QMT 平台的 ETF 套利策略示例。

策略原理

ETF 套利主要利用 ETF 二级市场价格(Market Price)与 ETF 参考净值(IOPV - Indicative Optimized Portfolio Value)之间的价差进行交易。

  1. 溢价套利(Premium Arbitrage):ETF价格 > IOPV + 成本 时。
    • 操作: 买入一篮子成分股 -> 申购 ETF 份额(一级市场) -> 卖出 ETF 份额(二级市场)。
  2. 折价套利(Discount Arbitrage):ETF价格 < IOPV - 成本 时。
    • 操作: 买入 ETF 份额(二级市场) -> 赎回 ETF 份额(一级市场) -> 卖出成分股。

策略代码

# -*- coding: gbk -*-
import time

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 1. 设置账户
    # 请替换为您的实际资金账号
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
    ContextInfo.account_type = 'STOCK'
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 2. 设置套利标的
    # 以华夏上证50ETF为例
    ContextInfo.etf_code = '510050.SH'
    
    # 3. 设置套利参数
    # 最小申赎单位(50ETF通常为90万份,需根据实际ETF说明书设置)
    ContextInfo.unit_amount = 900000 
    # 套利阈值(需覆盖交易佣金、印花税、冲击成本等),这里设为 0.2%
    ContextInfo.threshold = 0.002 
    
    # 4. 获取ETF成分股信息
    # get_etf_info 返回字典,包含 cashBalance, stocks(成分股列表) 等
    etf_info = get_etf_info(ContextInfo.etf_code)
    if etf_info:
        ContextInfo.components = etf_info['stocks'] # 成分股列表 ['600000.SH', ...]
        print(f"成功获取 {ContextInfo.etf_code} 成分股数量: {len(ContextInfo.components)}")
    else:
        print(f"获取ETF信息失败: {ContextInfo.etf_code}")
        ContextInfo.components = []

    # 设置定时器,每3秒运行一次handlebar(模拟高频监控)
    ContextInfo.run_time("handlebar", "3nSecond", "2023-01-01 09:30:00", "SH")

def handlebar(ContextInfo):
    """
    行情驱动函数,监控价差并触发交易
    """
    # 仅在实盘或回测的最后一根K线运行
    if not ContextInfo.is_last_bar():
        return

    # 1. 获取 ETF 实时行情
    tick_data = ContextInfo.get_full_tick([ContextInfo.etf_code])
    if not tick_data:
        return
        
    etf_market_price = tick_data[ContextInfo.etf_code]['lastPrice']
    
    # 2. 获取 ETF 实时 IOPV (参考净值)
    # QMT 提供了直接获取 IOPV 的接口
    etf_iopv = get_etf_iopv(ContextInfo.etf_code)
    
    if etf_iopv <= 0 or etf_market_price <= 0:
        return

    # 3. 计算价差率
    spread = (etf_market_price - etf_iopv) / etf_iopv
    
    # 获取当前时间用于日志
    current_time = time.strftime("%H:%M:%S", time.localtime())

    # 4. 判断套利机会
    
    # --- 情况 A: 溢价套利 (市价 > 净值) ---
    if spread > ContextInfo.threshold:
        print(f"[{current_time}] 发现溢价机会! 价格:{etf_market_price}, IOPV:{etf_iopv}, 溢价率:{spread:.4%}")
        
        # 执行溢价套利流程:买入成分股 -> 申购ETF -> 卖出ETF
        # 注意:这里仅演示逻辑,实盘需考虑资金是否足够、成分股是否停牌等
        
        # 第一步:买入一篮子成分股
        buy_basket(ContextInfo)
        
        # 第二步:一级市场申购 ETF (opType 60 通常代表申购,具体视券商柜台定义)
        # 参数说明: 60=申购, 1101=单股/普通, 账号, 代码, 5=最新价, -1=价格忽略, 数量=1个单位
        passorder(60, 1101, ContextInfo.account_id, ContextInfo.etf_code, 5, -1, 1, ContextInfo)
        
        # 第三步:二级市场卖出 ETF (T+0 交易的 ETF 可当日卖出)
        passorder(24, 1101, ContextInfo.account_id, ContextInfo.etf_code, 5, -1, ContextInfo.unit_amount, ContextInfo)

    # --- 情况 B: 折价套利 (市价 < 净值) ---
    elif spread < -ContextInfo.threshold:
        print(f"[{current_time}] 发现折价机会! 价格:{etf_market_price}, IOPV:{etf_iopv}, 折价率:{spread:.4%}")
        
        # 执行折价套利流程:买入ETF -> 赎回ETF -> 卖出成分股
        
        # 第一步:二级市场买入 ETF
        passorder(23, 1101, ContextInfo.account_id, ContextInfo.etf_code, 5, -1, ContextInfo.unit_amount, ContextInfo)
        
        # 第二步:一级市场赎回 ETF (opType 61 通常代表赎回)
        passorder(61, 1101, ContextInfo.account_id, ContextInfo.etf_code, 5, -1, 1, ContextInfo)
        
        # 第三步:卖出一篮子成分股
        sell_basket(ContextInfo)

def buy_basket(ContextInfo):
    """
    辅助函数:买入一篮子成分股
    注意:实际生产中建议使用组合交易接口或算法交易接口以减少滑点
    """
    print("开始买入成分股篮子...")
    # 获取成分股权重/数量信息通常需要更详细的数据,这里简化为按比例买入
    # 实际操作中,应根据 get_etf_info 返回的 'componentVolume' (成分股数量) 下单
    
    etf_detail = get_etf_info(ContextInfo.etf_code)
    if not etf_detail:
        return

    # 遍历成分股下单
    # 注意:这里会产生大量委托,实盘请谨慎使用循环下单,建议使用篮子委托接口(如果券商支持)
    stock_list = etf_detail['stocks'] # 代码列表
    # 注意:get_etf_info 返回结构中通常包含成分股数量,需根据文档解析
    # 假设我们需要自己计算或获取清单,这里仅做循环演示
    
    for stock in stock_list:
        # 假设买入 100 股作为演示,实际需按申赎清单数量 * 申购单位倍数
        # passorder(23, 1101, ContextInfo.account_id, stock, 5, -1, 100, ContextInfo)
        pass
    print("成分股篮子买入指令发送完毕")

def sell_basket(ContextInfo):
    """
    辅助函数:卖出一篮子成分股
    """
    print("开始卖出成分股篮子...")
    etf_detail = get_etf_info(ContextInfo.etf_code)
    if not etf_detail:
        return
        
    stock_list = etf_detail['stocks']
    for stock in stock_list:
        # passorder(24, 1101, ContextInfo.account_id, stock, 5, -1, 100, ContextInfo)
        pass
    print("成分股篮子卖出指令发送完毕")

代码关键点解析

  1. 数据获取

    • get_full_tick:获取 ETF 在二级市场的实时最新成交价。
    • get_etf_iopv:QMT 提供的专用接口,直接获取交易所推送的 ETF 实时参考净值(IOPV),这是计算价差的核心。
    • get_etf_info:获取 ETF 的申赎清单(PCF),包含了成分股代码、现金替代标志、必须现金替代金额等重要信息。
  2. 交易逻辑

    • 溢价(Spread > 0):二级市场价格贵,一级市场便宜。逻辑是“买低卖高”,即在一级市场用股票换 ETF(申购),在二级市场卖 ETF。
    • 折价(Spread < 0):二级市场价格便宜,一级市场贵。逻辑是“买低卖高”,即在二级市场买 ETF,在一级市场把 ETF 换成股票(赎回),然后卖掉股票。
  3. 下单函数 (passorder)

    • 23/24:标准的股票/ETF 二级市场买入和卖出。
    • 60/61:在 QMT 中,通常 60 对应 ETF 申购(Creation),61 对应 ETF 赎回(Redemption)。注意: 具体的 opType 数值可能因券商柜台配置略有不同,请在实盘前务必在“交易”界面的“函数交易”测试中确认。

风险与注意事项

  1. 门槛极高:ETF 申赎通常有最小单位限制(如 50ETF 通常为 90 万份起),资金门槛通常在 200-300 万人民币以上。
  2. T+0 限制
    • 沪市 ETF:通常支持“变相 T+0”。买入股票 -> 申购 ETF -> 当日卖出 ETF;或者 买入 ETF -> 赎回 ETF -> 当日卖出股票。
    • 深市 ETF:规则较为复杂,部分操作可能需要 T+1,请务必确认标的 ETF 的交收规则。
  3. 成分股停牌:如果篮子中某只股票停牌,可能需要使用“必须现金替代”功能,这会增加计算复杂度和成本。
  4. 滑点与速度:ETF 套利是高频竞争领域,手动循环下单(如代码中的 buy_basket)速度较慢,容易产生滑点导致亏损。实盘通常需要使用 QMT 的**组合交易(Basket Trading)**功能或算法交易功能来一键下单。

Q&A

Q: 如何获取成分股的具体数量?
A: get_etf_info(code) 返回的字典中,通常包含详细的成分股信息。你需要解析返回数据中的 stocks 列表,或者根据具体的 ETF 申赎清单文件(PCF)来确定每只股票需要买入多少股才能凑够一个申赎单位。

Q: 为什么代码中 buy_basket 部分被注释掉了?
A: 因为直接在循环中对几十只甚至上百只股票发送 passorder 可能会导致瞬间产生大量委托,容易造成阻塞或被风控拦截。在实际编写时,建议先打印日志确认逻辑,或者使用 QMT 的 algo_passorder 配合篮子交易参数进行批量下单。

Q: ContextInfo.threshold 设为多少合适?
A: 阈值必须覆盖所有成本,包括:

  1. 买卖股票/ETF 的佣金。
  2. 卖出股票的印花税(赎回后卖股票时)。
  3. ETF 申赎的费用(部分券商有)。
  4. 冲击成本(买卖一篮子股票时的滑点)。
    通常建议从 0.2% - 0.3% 开始观察,根据实际流动性调整。