准点对时高频申赎ETF:毫秒窗口里发一级单(QMT / PTrade)
这是一级市场申购 / 赎回,不是二级市场买卖 ETF。QMT 库里成熟的是「交易所对时 + 准点前后各约 120ms 窗口 + 笔间隔约 10ms 轮转」。PTrade 托管机没有同一套毫秒对时稿,接口是 etf_purchase_redemption,数量口径也不一样。时刻、单位数、标的都是示例,不是操作指令。不构成投资建议,不代操盘。
手册:passorder · QMT ETF 申赎 · etf_purchase_redemption · run_time 回测不跑
逻辑简述
- 对的是申赎,不是买卖。QMT
passorder的opType常见60申购、61赎回;volume是申赎单位数(篮),不是 ETF 份额。一篮对应 PCF 里的reportUnit份。 - 准点是交易所钟,不是 Windows 钟。NTP 采样算偏移,只平移触发点,不改系统时间。
- 窗口很窄。校正钟上
TARGET ± 120ms才发。窗前做 PCF / 权限预检,窗内只轮转已就绪的标的。 - 一窗一轮转。按表序或沪深交错发,笔间隔约 10ms。资金不足废单累计到阈值就熔断,本窗停。
- 回测无效。QMT
run_time回测不跑;申赎是柜台能力,研究环境空表很常见。
盘前常见例子是 08:25:00.000。实际开没开、能不能提前报,以券商申赎时段为准。
QMT 版本
上交所用 .SH。细定时器可用 10nMilliSecond,提前几秒挂上。passorder 申赎常见 prType=5、price=-1、quickTrade=2。11 参才能写唯一备注,8 参版本不要盲发,避免对不上柜台。
#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 为空时,库里允许用最近交易日缓存,但要打日志。volume 填 1 表示一篮,不是 1 份。
废单状态常见 57 / 52。只把「资金不足」这类计入熔断,时间未到被拒不要算进去。委托备注带日期和序号,方便和 order_callback 对上。
完整对时、预检、burst 循环见星球附件 ETF定时申购_交易所对时_高频窗口_QMT.py。
PTrade 版本
库里没有和 QMT 同款的毫秒对时申赎稿。托管机在券商机房,本机 NTP 偏移用不上;tick_data 大约 3 秒一拍,也铺不满 10ms 一轮。能做的是:盘前用 get_etf_info 对 PCF,到点调用 etf_purchase_redemption。
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 最容易混的三点:
| QMT | PTrade | |
|---|---|---|
| 函数 | passorder opType=60/61 | etf_purchase_redemption |
| 数量 | 篮数(1 = 一篮) | 份数(正申购、负赎回) |
| 代码 | .SH / .SZ | .SS / .SZ |
| 时钟 | 本机 NTP 平移 + 毫秒窗 | 券商机房钟;run_daily 到秒 |
篮子成分用 etf_basket_order。回测、研究模块这两个接口经常不可用。快照里的 iopv 是二级参考净值,不能代替 PCF 申赎清单。
两边都容易踩的坑
- 把二级
order/passorder(23/24)当成申赎。一级过户和二级成交不是一回事。 - QMT 把篮数写成份额,或 PTrade 把份额写成篮数,数量会差两个数量级。
- 申赎代码和基金代码不是同一个字段。PCF 里的赎回代码不要填进二级下单。
- QMT
run_time回测不跑,见 run_time 回测不跑。 - 后缀:QMT
.SH,PTrade.SS。见.SS/.SH。 - 资金不足废单要熔断。窗口里不要无限制重报。
- 能不能提前报、最小单位、当日额度,以券商和当日 PCF 为准。
完整源码见知识星球附件,这里只放逻辑和片段。
相关:API 查询 · 新股申购 · 热门问题 · 关注 / 知识星球
完整讨论和附件在 知识星球 Kimi量化研究所。
