问题描述
解决方案
这是一个基于 QMT 平台的 ETF 套利策略示例。
策略原理
ETF 套利主要利用 ETF 二级市场价格(Market Price)与 ETF 参考净值(IOPV - Indicative Optimized Portfolio Value)之间的价差进行交易。
- 溢价套利(Premium Arbitrage): 当
ETF价格 > IOPV + 成本时。- 操作: 买入一篮子成分股 -> 申购 ETF 份额(一级市场) -> 卖出 ETF 份额(二级市场)。
- 折价套利(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("成分股篮子卖出指令发送完毕")
代码关键点解析
-
数据获取:
get_full_tick:获取 ETF 在二级市场的实时最新成交价。get_etf_iopv:QMT 提供的专用接口,直接获取交易所推送的 ETF 实时参考净值(IOPV),这是计算价差的核心。get_etf_info:获取 ETF 的申赎清单(PCF),包含了成分股代码、现金替代标志、必须现金替代金额等重要信息。
-
交易逻辑:
- 溢价(Spread > 0):二级市场价格贵,一级市场便宜。逻辑是“买低卖高”,即在一级市场用股票换 ETF(申购),在二级市场卖 ETF。
- 折价(Spread < 0):二级市场价格便宜,一级市场贵。逻辑是“买低卖高”,即在二级市场买 ETF,在一级市场把 ETF 换成股票(赎回),然后卖掉股票。
-
下单函数 (
passorder):23/24:标准的股票/ETF 二级市场买入和卖出。60/61:在 QMT 中,通常60对应 ETF 申购(Creation),61对应 ETF 赎回(Redemption)。注意: 具体的opType数值可能因券商柜台配置略有不同,请在实盘前务必在“交易”界面的“函数交易”测试中确认。
风险与注意事项
- 门槛极高:ETF 申赎通常有最小单位限制(如 50ETF 通常为 90 万份起),资金门槛通常在 200-300 万人民币以上。
- T+0 限制:
- 沪市 ETF:通常支持“变相 T+0”。买入股票 -> 申购 ETF -> 当日卖出 ETF;或者 买入 ETF -> 赎回 ETF -> 当日卖出股票。
- 深市 ETF:规则较为复杂,部分操作可能需要 T+1,请务必确认标的 ETF 的交收规则。
- 成分股停牌:如果篮子中某只股票停牌,可能需要使用“必须现金替代”功能,这会增加计算复杂度和成本。
- 滑点与速度: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: 阈值必须覆盖所有成本,包括:
- 买卖股票/ETF 的佣金。
- 卖出股票的印花税(赎回后卖股票时)。
- ETF 申赎的费用(部分券商有)。
- 冲击成本(买卖一篮子股票时的滑点)。
通常建议从 0.2% - 0.3% 开始观察,根据实际流动性调整。