ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

QMT获取ETF申赎清单全攻略:从client is null到折溢价套利

QMT获取ETF申赎清单全攻略:从client is null到折溢价套利 做ETF相关策略的时候很多新手会被一个坎卡住行情数据好拿但这只ETF今天到底能申购哪些股票、不能用哪些股票替换、最小申赎单位是多少、现金替代溢价比例是几个点这类数据翻遍各大免费数据源都不好找。我在这个系列的往期笔记里讲过QMT的行情、回测和基础下单这一期专门把ETF申赎清单这件事啃透——它属于QMT编程里比较冷门但做套利、做T、做组合增强都绕不开的数据。先说结论QMT的xtquant模块里直接提供了申赎清单的获取接口不用去交易所官网抓PDF也不用去基金公司页面爬Excel。但接口名字、返回结构、登录条件这些细节里有不少坑尤其是client is null这个报错至少拦住了三分之一的新手。这篇笔记我会把从环境准备到代码实现、再到字段含义和策略用法一条线讲清楚适合已经会一点Python、准备认真做ETF量化但还没碰过PCF数据的人。1. 为什么量化盯上ETF申赎清单它不只是成分股列表很多新手第一次听说ETF申赎清单以为它就是一份今天ETF里有哪些股票的静态名单。这么理解不算错但会严重低估它的价值也会在后续做策略的时候不知道该怎么用。1.1 申赎清单里到底有什么ETF申赎清单的英文缩写是PCFPortfolio Composition File由基金管理人在每个交易日盘前发布。A股ETF的PCF清单一般会在早上8点半前后更新在你用QMT拉数据之前它已经躺在交易所的指定位置了。清单本身是一个结构化文本里面至少包含这几类信息基金代码和名称、适用日期、最小申购赎回单位比如50万份还是100万份、预估现金差额、现金替代溢价比例以及最重要的成分券明细——每一只成分券代码、名称、数量还有它的现金替代标志。现金替代标志是新手最容易忽略、但对套利结果影响最大的字段之一。它分三种允许现金替代、禁止现金替代、必须现金替代。允许现金替代的意思是如果你手里没有这只股票的券申购的时候可以直接用现金顶上但要多付一笔溢价禁止现金替代的意思是必须买齐这只股票才能申购必须现金替代则是无论你有没有券都只收现金不接收股票。这三个标志直接决定了套利操作的可行性和摩擦成本。1.2 拿不到清单的旧办法有多痛在没有接触QMT之前我自己也走过弯路。最早是每天开盘前手动去交易所官网找文件然后复制进Excel再手工对比昨天的差异。后来写爬虫去基金公司官网抓PCF文件问题是各家基金公司的文件格式不统一有的是PDF有的是XLS有的直接在网页表格里解析脚本每周都要修一次。再后来发现第三方数据商也有但要额外付费而且字段经常缺胳膊少腿。这个痛点本质上是申赎清单是一份日更的交易规则文件它不是单纯的行情而是申购赎回操作的前提参数。做ETF折溢价套利的人如果拿不到这份清单就像做期货的人不知道保证金比例一样连成本都算不准。而QMT作为券商端的量化终端把这份数据直接封装成了Python接口这才是它真正的价值所在——数据源统一、字段完整、交易日更新及时。2. 数据通路与环境准备先把QMT跑起来2.1 你需要的是这套环境组合QMT获取ETF申赎清单走的是xtquant里的xtdata数据模块不是交易模块。所以你的环境里不需要复杂的前端策略代码只需要满足三个条件第一你的电脑上装了券商提供的QMT终端并且已经登录。这里的登录不只是打开软件看一眼而是要用有权限的账号完成真实的柜台登录。第二Python环境里安装了与终端版本匹配的xtquant包。第三网络能连通QMT的行情服务器因为申赎清单属于行情侧数据走的是数据通道不是交易通道。一个常见的误解是只用xtdata拉数据不交易是不是可以不开QMT终端实测下来不行。xtdata的底层要连接本机的QMT进程QMT不开数据接口就找不到服务端。哪怕你只想要盘后数据也必须让终端在后台跑着。至于安装xtquant常规操作就是pip install xtquant但更稳妥的方式是直接用QMT安装目录自带的xtquant包路径一般在bin或lib子目录下和终端的版本天然匹配。这样能避免pip源里的包版本太新、和终端不兼容的问题。2.2 终端client is null的完整排错链路接下来讲这个系列的读者问到最多的一个报错client is null。这个报错一般出现在你尝试连接交易API或者初始化数据接口的时候表现形式是日志里一行红字或者直接抛一个空对象错误让你摸不着头脑。我第一次遇到这个报错是在写完XtQuantTrader初始化代码之后调用connect()的时候。当时第一反应是代码写错了反复检查参数、换session id折腾了快两个小时最后才发现根本不是代码问题。这里把完整排查链路写出来我按这个顺序踩过一遍之后基本五分钟内能定位问题第一步确认QMT终端是否处于登录状态。这是最高频的原因。xtquant的数据和交易接口都要和本机QMT进程通信终端没启动或者启动后没有登录账号接口拿到的就是一个空客户端对象于是报client is null。解决办法很简单打开QMT终端完成登录再重新运行Python脚本。注意一定要等终端完全进入主界面、行情和账户都加载出来之后再跑脚本否则连接还是可能失败。第二步检查你传给接口的路径是不是指向userdata_mini这一层。新手经常把安装根目录传进去比如C:\QMT\但正确做法是找到你实际的用户数据目录。以我本机为例路径是类似D:\QMT\userdata_mini的结构里面有config、log等子目录。你传的路径应该是包含userdata_mini的那一层而不是它的父目录。判断方法很简单进入你填的路径看看里面有没有userdata_mini这个文件夹没有就是填浅了。第三步核对xtquant包的版本和终端版本是否匹配。有时候终端会自动升级但Python环境里的xtquant还是旧版本两边握手协议对不上也会出现接口偶发返回空对象的情况。这个问题的排查方法是去终端的安装目录里找到配套的xtquant文件夹对比一下版本号不一致就直接用终端自带的那个替换pip版本。第四步检查账号权限。很多券商的QMT账号默认只开通了行情权限没有开通极简交易模式或者没有单独开通ETF申赎权限。这时候你会发现行情数据正常、K线正常但一拉申赎清单就返回空或者报错。处理方式就是找客户经理开通对应权限这个在手机开户的默认设置里往往是没有的。我把这个报错的排查要点整理成了一张表方便对照排查步骤检查内容常见结果解决动作1QMT终端是否登录未登录或登录超时打开终端完成登录等待行情加载2路径层级填到了安装根目录改为包含userdata_mini的路径3版本匹配pip包与终端不一致使用终端自带的xtquant4账号权限API/申赎权限未开联系券商开通对应权限这四步走完绝大多数client is null都能解决。如果你试完还是报错还有一个偏方把QMT终端退出重新以管理员身份运行再登录一次。部分Windows环境下的端口占用问题用这个办法能很神奇地解决。3. 核心代码把申赎清单拉下来并解析成DataFrame3.1 接口调用与返回结构环境通了之后实际拉数就很简单了。xtdata模块里有一个函数是专门取ETF申赎清单的不同版本里函数名可能稍有差异有的叫get_etf_pcf有的版本接口被整合进了get_instrument_detail。最靠谱的做法是打开Python先执行一次dir(xtdata)把所有带etf或者pcf关键字的函数列出来按名字就能猜到哪个是干这个的。我本机的接口调用方式是这样from xtquant import xtdata # 指定ETF代码和日期 sec_code 510300.SH date_str 20240112 # 获取申赎清单 pcf_data xtdata.get_etf_pcf(sec_code, date_str) # 返回结果一般是dict结构 # 先看一下顶层有哪些key if pcf_data: print(pcf_data.keys())返回的数据结构不同版本会有差异但大体上会包含基金级别的信息和一个成分券列表。基金级别的信息里有基金代码、基金名称、清单日期、最小申赎单位、预估现金差额、现金替代溢价比例这些成分券列表里则是每一只股票的代码、名称、数量、现金替代标志。拿到最原始的结构之后我做的第一件事永远是把它转成pandas的DataFrame因为后续的所有计算、筛选、对比操作用DataFrame都要顺手得多import pandas as pd if pcf_data: # 假设成分券列表在stocks字段里 stocks pcf_data.get(stocks) or pcf_data.get(components) or [] df_stocks pd.DataFrame(stocks) # 基金层面的信息单独拆出来 fund_info {k: v for k, v in pcf_data.items() if k not in (stocks, components)} print(df_stocks.head()) print(fund_info)这里有一点要提醒你返回的字段名每次拿到手之后先整体打印一遍不要凭着网上的旧博客记忆硬编码列名。QMT的接口升级比较频繁字段名可能从stock_code变成ticker或者amount变成volume。我会在拿到数据的第一时间执行df_stocks.columns.tolist()把列名保存下来再写映射逻辑。这个习惯能帮你省下很多和版本较劲的时间。3.2 字段映射与常用计算拿到成分券明细之后有几个字段是你无论做什么策略都要先确认的。我把它们列出来你照着核对就行成分券代码注意是带交易所后缀的完整代码比如600519.SH做撮合的时候一定要用完整代码不然和行情数据join的时候容易出问题。成分券数量这个数量通常不是按100股为单位而是按最小申赎单位折算后的数量。也就是说这份清单里的每一行对应的是申购一个最小单位的ETF需要准备多少股这只股票。现金替代标志通常用枚举值表示比如allowed、forbidden、mandatory有的版本是单个字符编码。拿到手先做分布统计看看今天有多少股票是禁止现金替代的这决定了你申购前必须买入哪些股票。字段确认完之后可以顺手算一个很有用的中间量——一篮子股票的总市值。我通常的做法是拿申赎清单里的数量和当日实时行情里的最新价做乘法汇总出买入这一篮子股票大概要花多少钱。这个数除以最小申赎单位再除以100沪市ETF每份面值1元但交易价格按每百份报价这里按你自己习惯统一就好就能估算出ETF的实时参考净值。这个逻辑用代码写就是# 假设df_stocks里有成分券代码和数量 # 再拿一份实时行情快照 quote_df pd.DataFrame(xtdata.get_full_tick(df_stocks[stock_code].tolist())).T quote_df[lastPrice] quote_df[lastPrice].astype(float) # 合并后计算一篮子总市值 merged df_stocks.merge(quote_df[[lastPrice]], left_onstock_code, right_indexTrue, howleft) basket_value (merged[amount] * merged[lastPrice]).sum() print(f一篮子总市值: {basket_value:.2f})这个basket_value算出来之后你再结合ETF市价和最小申赎单位就能算出一个非常重要的指标实时折溢价率。具体的计算公式和判断阈值下一节详细展开。4. 数据背后的交易逻辑折溢价套利怎么用这堆字段4.1 一篮子价值、IOPV与折溢价ETF有一个独特的双轨交易机制场内份额可以在二级市场像股票一样买卖同时一级市场可以用一篮子股票申购份额或者反过来把份额赎回成一篮子股票。因为两个市场的参与者不同、速度不同ETF市价和它的实际净值之间经常会出现短暂偏离这就是折溢价。交易所为了让大家能看到这个偏离会按15秒一次的频率发布IOPV也就是参考净值。IOPV怎么算的本质上就是用申赎清单里的成分股数量和实时价格算出来的。所以你自己有了申赎清单完全可以在盘中自己算IOPV而且可以按你需要的频率刷新不用等交易所的15秒快照。折溢价率的公式很简单# 每份ETF的参考净值 一篮子总市值 / 最小申赎单位 nav_per_share basket_value / min_redemption_unit # 溢价率 (市价 - 参考净值) / 参考净值 premium_rate (etf_market_price - nav_per_share) / nav_per_share算出来溢价率为正说明ETF在市面上的交易价格贵了理论上可以申购份额然后卖掉赚差价溢价率为负说明ETF便宜了理论上可以买入份额然后赎回成股票卖掉赚差价。但这里必须泼一盆冷水理论归理论实盘套利没有这么简单。一级市场申购赎回有最低门槛比如50万份或者100万份起对应的资金量往往是几十万到上百万而且申购赎回不是实时的申赎指令提交后要等确认这个时间差里市价可能已经回来了再加上手续费、冲击成本、资金占用成本实际能下手的折溢价阈值通常要留到0.3%到0.5%以上。我自己在实盘里会把计算出来的折溢价率和阈值表做对比达到阈值才触发提醒成本项典型数值说明申购费0.05%左右基金公司收各家不同卖出滑点0.05%-0.1%取决于ETF流动性冲击成本0.1%-0.2%大资金尤其明显资金占用按持仓天数算申赎有确认周期现金替代溢价0%-1%不等看当天清单字段4.2 现金替代标志最容易忽略的成本变量折溢价套利的另一个隐藏成本藏在现金替代标志和现金替代溢价比例里。假设你今天想申购一只沪深300ETF但成分股里有几只股票因为你仓位里没有需要用现金替代。那么你实际付出的成本不是这几只股票的市价而是市价×1现金替代溢价比例。这个溢价比例由基金公司每天在申赎清单里公布通常在0%到1%之间极端情况下会更高。这就带来一个很实际的操作逻辑同样是溢价套利优先选择那些现金替代标志是允许且溢价比例低的ETF如果某只ETF今天一堆股票强制现金替代它的套利成本就会明显抬高。这份清单数据就是在帮你做这个成本排雷。另外预估现金差额这个字段也值得关注。它反映了申赎一篮子股票时因为现金替代、停牌、涨跌停等因素产生的现金轧差。如果今天预估现金差额特别大说明当天申赎的成本不确定性也高这时候就算盘中有套利信号我也会主动把触发阈值调高一点。5. 从能拉到到能赚钱工程化与策略落地5.1 每日定时拉取与差异监控拿到申赎清单接口之后我的建议是不要只在需要的时候临时拉而是做成每日定时任务连续落库。原因有两个一是回测的时候历史申赎清单是稀缺数据你很难在别的地方补齐二是成分股变动本身就是一个信号源连续监控能让你第一时间发现ETF管理人调仓。我本地的工程化方案很简单就是一套Python脚本加系统计划任务。每天8点40分触发脚本拉取当天全市场关注的ETF申赎清单保存成按日期分区的parquet文件然后和昨天的清单做一次diff把成分股的增减、数量变化、现金替代标志变化记录下来。这份diff的价值可能比你想象的大。举个例子某只ETF的成分股数量或者权重配比突然发生变化往往说明管理人正在调仓而调仓方向在发布当天通常不会体现在公开的基金持仓公告里但申赎清单已经把新篮子亮出来了。对于做配对交易或者行业轮动的策略来说这是一个可以利用的领先信息。5.2 新手最容易踩的权限与费用坑最后说几个我在实盘中替大家踩过的坑这些不在代码报错里体现但会直接影响你的盈亏。第一个坑是申赎权限没开通却以为开通了。很多新手在QMT里能看到申赎清单数据就默认自己可以做申赎套利了。其实拉取数据只需要行情权限但真正提交申购赎回指令还需要券商单独开通ETF申赎业务权限而且要签风险揭示书。如果没有权限你的套利逻辑即使在代码里完全跑通实盘下单那一步也会被柜台拦下来。第二个坑是最小申赎单位的资金门槛计算错误。沪市很多ETF的最小申赎单位是50万份按1元净值算就是50万元起步再加上现金替代的额外冻结实际占用资金可能要到60万。如果你按20万资金去设计套利策略那这只ETF从一开始就不适合你。我会在策略初始化的时候把申赎清单里的最小申赎单位乘上预估净值算出一个参与资金门槛低于门槛的ETF直接过滤掉。第三个坑是现金替代溢价的退款时滞。当你的申购指令里有现金替代成分券商会按较高的比例多冻结资金多冻结的部分要到日终清算之后才退回。这会导致你的可用资金在盘中莫名变少。我吃过这个亏当时算好的资金余量盘中突然显示不足后来才搞清楚是现金替代保证金占用。所以做套利的时候一定要在资金模型里把这部分占用算进去不要满仓用尽。第四个坑是只盯溢价不管T1。A股ETF的申赎套利申购得到的份额虽然可以在场内卖出但二级市场买入的份额当天可以赎回这里存在不同的交收机制。不同ETF、不同券商在这个环节的处理有细微差别回测里看起来很美的收益实盘可能因为交收规则变成负的。我的建议是每一只ETF都要拿小仓位做一次完整的申赎流程测试把实际的资金流时间表跑通再放大资金。我再分享一个小技巧如果你只是用申赎清单辅助做日内的ETF做T不碰一级市场申赎那你可以不关注现金替代溢价这些字段只需要盯住成分券明细和数量的变化。因为做T的核心是判断ETF价格相对一篮子价值的偏离而一篮子价值的变化直接由成分股价格驱动。我每天定时拉取清单后会把当天清单和T-1的清单做一次校验如果成分券数量没有变化就说明篮子结构稳定做T的时候只需要盯实时行情就行。一旦发现篮子结构变化当天做T的基准就要用新清单重新计算否则容易因为篮子变了但脑子没变而做出错误判断。说起来我当初花了大半个周末折腾client is null后来发现只是终端没登录那种感觉真的既窝火又释然。现在如果你也卡在这直接按第二节的那四步走一遍大概率就通了。数据拉下来之后从算出一篮子价值到对比实时折溢价再到结合申赎清单做成本排雷这条路我走通之后回头看其实每一步都不复杂真正的门槛在于知道有这条路和愿意把细节抠清楚。做完日积月累的清单落库几个月之后你回看历史数据会发现它对策略回测的帮助远超预期。
返回列表