Skip to content

准点对时高频申赎ETF:毫秒窗口里发一级单(QMT / PTrade)

这是一级市场申购 / 赎回,不是二级市场买卖 ETF。QMT 库里成熟的是「交易所对时 + 准点前后各约 120ms 窗口 + 笔间隔约 10ms 轮转」。PTrade 托管机没有同一套毫秒对时稿,接口是 etf_purchase_redemption,数量口径也不一样。时刻、单位数、标的都是示例,不是操作指令。不构成投资建议,不代操盘。

手册:passorder · QMT ETF 申赎 · etf_purchase_redemption · run_time 回测不跑

逻辑简述

  1. 对的是申赎,不是买卖。QMT passorderopType 常见 60 申购、61 赎回;volume申赎单位数(篮),不是 ETF 份额。一篮对应 PCF 里的 reportUnit 份。
  2. 准点是交易所钟,不是 Windows 钟。NTP 采样算偏移,只平移触发点,不改系统时间。
  3. 窗口很窄。校正钟上 TARGET ± 120ms 才发。窗前做 PCF / 权限预检,窗内只轮转已就绪的标的。
  4. 一窗一轮转。按表序或沪深交错发,笔间隔约 10ms。资金不足废单累计到阈值就熔断,本窗停。
  5. 回测无效。QMT run_time 回测不跑;申赎是柜台能力,研究环境空表很常见。

盘前常见例子是 08:25:00.000。实际开没开、能不能提前报,以券商申赎时段为准。

QMT 版本

上交所用 .SH。细定时器可用 10nMilliSecond,提前几秒挂上。passorder 申赎常见 prType=5price=-1quickTrade=2。11 参才能写唯一备注,8 参版本不要盲发,避免对不上柜台。

python
#coding:gbk
ACTION = '申购'                    # 或 '赎回'
ETF_TARGETS = {'159612.SZ': 1}     # 代码: 申赎单位数(篮)
TARGET_TIME = '08:25:00.000'
WINDOW_BEFORE_MS = 120
WINDOW_AFTER_MS = 120
ORDER_INTERVAL_MS = 10
OP_ETF_CREATE, OP_ETF_REDEEM = 60, 61

def init(C):
    C.set_account(account)
    # NTP 采样 -> G.clock_offset_sec
    # 预检 get_etf_info / PCF.reportUnit、申赎权限
    C.run_time('on_fine_timer', '10nMilliSecond',
               datetime.now().strftime('%Y-%m-%d 08:20:00'))

def _exch_now():
    # wall + perf_counter 锚点,再加上 NTP 偏移
    return G.wall_anchor + timedelta(seconds=elapsed) + timedelta(seconds=G.clock_offset_sec)

def on_fine_timer(C):
    if C.do_back_test or G.burst_done:
        return
    now = _exch_now()
    target, start, end = _window_bounds(now)
    if now < start or now >= end:
        return
    # 只对预检就绪的代码轮转
    passorder(60, 1101, acct, code, 5, -1, units,
              STRATEGY_NAME, 2, remark, C)

预检要看当日 PCF:reportUnit、交易日是否切过来、能不能申、赎回有没有申赎可用量。盘前 tradingDay 为空时,库里允许用最近交易日缓存,但要打日志。volume1 表示一篮,不是 1 份。

废单状态常见 57 / 52。只把「资金不足」这类计入熔断,时间未到被拒不要算进去。委托备注带日期和序号,方便和 order_callback 对上。

完整对时、预检、burst 循环见星球附件 ETF定时申购_交易所对时_高频窗口_QMT.py

PTrade 版本

库里没有和 QMT 同款的毫秒对时申赎稿。托管机在券商机房,本机 NTP 偏移用不上;tick_data 大约 3 秒一拍,也铺不满 10ms 一轮。能做的是:盘前用 get_etf_info 对 PCF,到点调用 etf_purchase_redemption

python
def initialize(context):
    g.etf = '510050.SS'     # 基金代码,不是 PCF 申赎代码
    g.units = 1             # 下面会换成「份」
    g.fired = False
    set_universe([g.etf])
    run_daily(context, do_create, time='08:25')

def do_create(context, data):
    if g.fired or not is_trade():
        return
    info = get_etf_info(g.etf) or {}
    report_unit = int(info.get('reportUnit') or info.get('report_unit') or 0)
    if report_unit <= 0:
        log.info('PCF 无 reportUnit,本轮不发')
        return
    shares = g.units * report_unit          # 正数申购,负数赎回
    etf_purchase_redemption(g.etf, shares)
    g.fired = True

和 QMT 最容易混的三点:

QMTPTrade
函数passorder opType=60/61etf_purchase_redemption
数量篮数(1 = 一篮)份数(正申购、负赎回)
代码.SH / .SZ.SS / .SZ
时钟本机 NTP 平移 + 毫秒窗券商机房钟;run_daily 到秒

篮子成分用 etf_basket_order。回测、研究模块这两个接口经常不可用。快照里的 iopv 是二级参考净值,不能代替 PCF 申赎清单。

两边都容易踩的坑

  1. 把二级 order / passorder(23/24) 当成申赎。一级过户和二级成交不是一回事。
  2. QMT 把篮数写成份额,或 PTrade 把份额写成篮数,数量会差两个数量级。
  3. 申赎代码和基金代码不是同一个字段。PCF 里的赎回代码不要填进二级下单。
  4. QMT run_time 回测不跑,见 run_time 回测不跑
  5. 后缀:QMT .SH,PTrade .SS。见 .SS / .SH
  6. 资金不足废单要熔断。窗口里不要无限制重报。
  7. 能不能提前报、最小单位、当日额度,以券商和当日 PCF 为准。

完整源码见知识星球附件,这里只放逻辑和片段。

相关:API 查询 · 新股申购 · 热门问题 · 关注 / 知识星球

完整讨论和附件在 知识星球 Kimi量化研究所