🚀 AI 一键生成 ptrade 策略代码

如何在 PTrade 平台实现 ETF 一二级市场溢价套利策略?

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

问题描述

如何实现一个 ETF 溢价套利策略?

解决方案

ETF 溢价套利策略实现

ETF 溢价套利(Premium Arbitrage)是指当 ETF 的二级市场交易价格(Price)显著高于其参考净值(IOPV/NAV)时,通过买入一篮子成分股 -> 申购 ETF 份额 -> 二级市场卖出 ETF 的方式来获取无风险或低风险收益的策略。

在 PTrade 中,实现该策略的核心在于利用 etf_basket_order(篮子买入)和 etf_purchase_redemption(申购赎回)接口。

以下是详细的策略逻辑与代码实现。

1. 策略逻辑

  1. 获取数据:获取 ETF 的实时盘口价格(Ask1/Bid1 或 最新价)和实时参考净值(IOPV)。
  2. 计算溢价率溢价率 = (ETF价格 - IOPV) / IOPV
  3. 判断阈值:如果 溢价率 > 交易成本阈值(如 0.2%),触发套利信号。
  4. 执行套利
    • 第一步:使用 etf_basket_order 买入一篮子成分股。
    • 第二步:使用 etf_purchase_redemption 提交 ETF 申购请求(将股票换成 ETF 份额)。
    • 第三步:使用 orderorder_target 在二级市场卖出 ETF 份额(T+0 ETF 可当日卖出)。

2. 关键 API 说明

  • get_etf_info(etf_code): 获取 ETF 的最小申赎单位(report_unit),这是计算下单数量的基础。
  • get_snapshot(security): 获取 ETF 的实时行情快照。
  • etf_basket_order(etf_code, amount, ...): 专门用于 ETF 成分股的篮子交易。
  • etf_purchase_redemption(etf_code, amount): 执行申购(正数)或赎回(负数)。

3. 策略代码实现

def initialize(context):
    """
    策略初始化函数
    """
    # 1. 设置要套利的ETF代码 (例如: 510050.SS 上证50ETF)
    g.etf_code = '510050.SS'
    
    # 2. 设置股票池
    set_universe([g.etf_code])
    
    # 3. 套利参数设置
    g.threshold = 0.002  # 溢价阈值,例如 0.2% (需覆盖佣金、印花税、过户费等)
    g.basket_num = 1     # 每次套利的篮子数量
    
    # 4. 获取ETF基础信息 (主要是最小申赎单位 report_unit)
    # 注意:实际交易中建议加异常处理,防止获取失败
    etf_info = get_etf_info(g.etf_code)
    if etf_info and g.etf_code in etf_info:
        g.report_unit = int(etf_info[g.etf_code]['report_unit'])
        log.info("ETF: %s, 最小申赎单位: %d" % (g.etf_code, g.report_unit))
    else:
        g.report_unit = 0
        log.error("未获取到ETF信息,策略无法运行")

    # 5. 开启盘中定时运行 (例如每分钟检测一次,高频策略可使用 tick_data)
    # 这里为了演示逻辑清晰,使用 handle_data 或 run_interval
    # run_interval(context, arbitrage_logic, seconds=5) 

def handle_data(context, data):
    """
    盘中运行函数 (分钟级)
    """
    if g.report_unit == 0:
        return

    # 执行套利逻辑
    arbitrage_logic(context)

def arbitrage_logic(context):
    """
    套利核心逻辑
    """
    etf = g.etf_code
    
    # 1. 获取行情快照
    snapshot = get_snapshot(etf)
    if not snapshot or etf not in snapshot:
        return
    
    tick_data = snapshot[etf]
    
    # 2. 获取关键价格
    # 获取买一价 (作为卖出ETF的参考价格,保守估计)
    # 注意:snapshot['bid_grp'] 格式为 {1: [价格, 量, 笔数], ...}
    bid_grp = tick_data.get('bid_grp')
    if not bid_grp:
        return
        
    # 取买一价
    etf_market_price = bid_grp[1][0]
    
    # 3. 获取 IOPV (净值)
    # 注意:PTrade 的 get_snapshot 返回字段中通常包含 IOPV,
    # 但具体字段名需根据券商实盘环境确认 (常见为 'iopv' 或在扩展字段中)。
    # 如果 snapshot 中没有直接提供 iopv,需要通过 get_trend_data 或外部数据源获取。
    # 此处假设 snapshot 中包含 'iopv' 字段,或者我们用昨收盘估算演示(实际必须用实时IOPV)
    
    # --- 模拟 IOPV 获取 (实盘请替换为真实 IOPV 获取代码) ---
    # 假设当前没有直接的IOPV字段,这里仅做代码结构演示
    # 实际中可能是: current_iopv = tick_data.get('iopv')
    # 这里为了代码不报错,我们模拟一个比市价低的价格来触发逻辑
    current_iopv = etf_market_price * 0.995 
    # --------------------------------------------------

    if current_iopv <= 0 or etf_market_price <= 0:
        return

    # 4. 计算溢价率
    premium_rate = (etf_market_price - current_iopv) / current_iopv
    
    # 5. 判断是否满足套利条件
    if premium_rate > g.threshold:
        log.info("发现溢价套利机会: 市价 %.3f, IOPV %.3f, 溢价率 %.2f%%" % 
                 (etf_market_price, current_iopv, premium_rate * 100))
        
        # 检查资金是否足够 (此处省略详细资金检查,实盘需添加)
        
        # --- 执行套利三步走 ---
        
        # 步骤 A: 买入一篮子股票
        # price_style='S5' 表示用卖五价买入,保证成交速度
        # position=False 表示不使用持仓替代,直接从市场买入
        log.info("Step 1: 买入成分股篮子")
        etf_basket_order(etf, g.basket_num, price_style='S5', position=False)
        
        # 步骤 B: 申购 ETF
        # amount 为 篮子数量 * 最小申赎单位
        creation_amount = g.basket_num * g.report_unit
        log.info("Step 2: 申购 ETF 份额: %d" % creation_amount)
        etf_purchase_redemption(etf, creation_amount)
        
        # 步骤 C: 卖出 ETF
        # 注意:T+0 的 ETF (如债券、黄金、跨境或部分单市场ETF) 允许申购后当日卖出。
        # 普通股票 ETF 可能需要确认份额到账时间 (部分券商支持实时可用,部分需T+1)。
        # 假设是支持 T+0 变现的品种:
        log.info("Step 3: 卖出 ETF 份额")
        order(etf, -creation_amount)

4. 注意事项与风险提示

  1. IOPV 获取
    • 文档中的 get_snapshot 返回字段列表未明确列出 iopv。在实盘环境中,IOPV 通常包含在 Level-2 行情推送中。如果 get_snapshot 无法获取,可能需要自行根据成分股价格计算(计算量大)或咨询券商是否有专门的 IOPV 接口。
  2. T+0 规则
    • 并非所有 ETF 都支持“买入股票->申购->卖出ETF”的日内 T+0 回转。通常单市场 ETF(如沪市 ETF 买沪市股票)支持较高的效率,但跨市场 ETF(如沪深300ETF)涉及两个交易所的交收,申购的份额可能需要 T+1 或 T+2 才能卖出。请务必确认标的 ETF 的交易规则。
  3. 现金替代(Cash Replacement)
    • etf_basket_order 中的 info 参数可以控制现金替代标志。如果某些成分股停牌或涨停买不到,必须使用“允许现金替代”或“必须现金替代”模式,这会带来额外的资金成本(现金差额)。
  4. 并发与时序
    • 上述代码是顺序执行的。在极速交易场景下,买入篮子和申购通常需要极短的时间间隔。如果买入篮子部分成交而部分未成交,直接申购可能会失败或转为现金替代,导致成本不可控。
  5. 资金占用
    • ETF 申购通常需要较大的资金量(一个单位通常对应 100万-300万人民币市值),请确保账户有足够的 cash

5. 进阶优化 (Tick 级)

如果需要更高频的监控,可以将逻辑放入 tick_data 函数中:

def tick_data(context, data):
    etf = g.etf_code
    # 检查是否有该ETF的tick数据
    if etf not in data:
        return
        
    tick = data[etf]['tick']
    # 获取盘口价格和IOPV进行计算...
    # 逻辑同上

使用 tick_data 时,请确保在 initializeset_parameters 中配置了接收 L2 行情(如果券商支持)。

Q&A

Q: etf_basket_order 会自动帮我申购 ETF 吗?
A: 不会etf_basket_order 的作用是根据 ETF 清单及其权重,帮你批量买入底层的股票。买入成功后,你持有的是一堆股票。你必须紧接着调用 etf_purchase_redemption 将这些股票“交换”成 ETF 份额。

Q: 为什么代码里用 price_style='S5'
A: 套利追求速度和成交确定性。用卖五价(S5)买入成分股是为了确保能立即成交,防止因价格变动导致篮子买不齐。由此产生的滑点成本需要包含在你的 g.threshold(溢价阈值)中。