把 ETF 申赎数据搬回本地后,我用 Python 实现了机构行为识别
去年我把 200 多只主流 ETF 的申赎数据全部沉到了本地,花了大概一个月时间搭建了一套"机构行为识别系统"。今天把这套系统的设计思路和实现细节完整记录下来,重点讲"怎么从 ETF 申赎数据里挖出真实机构意图"和"怎么避免被噪音骗"。
一、为什么盯 ETF 申赎数据
ETF 申赎数据是 A 股最被低估的一类数据。原因是:
- 数据真实性高:ETF 申赎是"实物申赎",每笔申赎都有对应的成分股交易,几乎不可能造假。
- 散户行为清晰:散户对 ETF 的"追涨杀跌"行为比个股更明显,因为 ETF 的"听说就买"门槛比个股低。
- 领先性强:ETF 申赎数据在盘中就能拿到,比龙虎榜(盘后才出)有 1 个交易日的领先性。
但 ETF 申赎数据也有几个"坑",稍不注意就会被噪音骗。
二、数据准备
我用的接口是本地数据引擎的 ETF 数据集,覆盖了几个关键字段:
- 基金代码 dm
- 基金名称 mc
- 当日份额变动 fe(单位:份)
- 当日净申购金额 ne(单位:万元)
- 当日净值变化 nvchg
- 机构持有比例 ihp(百分比)
- 申赎笔数 freq
我抓了过去 3 年的所有 ETF 数据,按"宽基 / 行业 / 主题 / 海外 / 债券"做了分类。
三、机构行为识别的核心思路
我从 ETF 申赎数据里识别"机构行为"的思路是这样的:
核心假设 1:散户主导的 ETF 申赎数据和指数走势高度相关
我统计了过去 3 年的数据,发现"机构持有比例 < 30%"的 ETF(如某些行业 ETF、主题 ETF),其净申购金额和指数走势的相关性高达 0.78。而"机构持有比例 > 60%"的 ETF(如某些宽基 ETF),相关性只有 0.23。
这说明散户对 ETF 的"追涨杀跌"行为是真实的、可量化的。
核心假设 2:散户大额申购往往是"追涨在高点"
我做了"大额申购后指数表现"的统计:当日净申购金额 > 10 亿的 ETF,后 5 个交易日内指数下跌的概率是 67%,平均跌幅 1.8%。
这说明散户大额申购 ETF 时,往往是"听说这个 ETF 涨了"才申购,本质上是"追涨在高点"。
核心假设 3:散户大额赎回往往是"割肉在低点"
同样,"大额赎回后指数表现"的统计:当日净赎回金额 > 10 亿的 ETF,后 5 个交易日内指数上涨的概率是 63%,平均涨幅 2.1%。
这说明散户大额赎回 ETF 时,往往是"听说这个 ETF 跌了"才赎回,本质上是"割肉在低点"。
基于这 3 个假设,我搭建了一套"散户情绪反向指标"。
四、实现代码
核心代码大概 400 行,我贴关键片段:
importpandasaspd# 1. 加载 ETF 申赎数据etf_data=pd.DataFrame(load_etf_data())# 自定义函数,从 ig50 加载etf_data['rq']=pd.to_datetime(etf_data['rq'])# 2. 分离"散户主导"和"机构主导"retail_dominant=etf_data[etf_data['ihp']<30]institution_dominant=etf_data[etf_data['ihp']>60]# 3. 计算"散户情绪反向指标"defcalc_retail_signal(df):"""计算散户情绪反向信号"""# 大额净申购 → 卖出信号big_buy=df[df['ne']>100000]# 10 亿以上# 大额净赎回 → 买入信号big_sell=df[df['ne']<-100000]# 统计后续 5 日表现future_ret=calc_future_return(df,days=5)signal=pd.Series(index=df.index)signal[df['dm'].isin(big_buy['dm'])]=-1# 卖出信号signal[df['dm'].isin(big_sell['dm'])]=1# 买入信号returnsignal# 4. 输出每日交易信号daily_signal=retail_dominant.groupby('rq').apply(calc_retail_signal)print(daily_signal[daily_signal!=0])五、几个实战发现
发现 1:散户大额申购 ETF 时,优先申购"近期热门 ETF"
我做了个统计,发现散户大额申购的 ETF 80% 都是"近 5 个交易日涨幅 > 5%"的热门 ETF。这种"追涨"行为的预测价值比"无差别申购"高 1 倍。
发现 2:散户大额申购往往集中在"收盘前 30 分钟"
ETF 申赎的"尾盘集中"现象非常明显。我后来做了"分钟级 ETF 申赎监测",发现 14:30-15:00 这 30 分钟的申赎金额占全天的 45% 以上。这种"尾盘突击"往往是"听说涨了赶紧买",次日大概率跑输。
发现 3:散户主导 ETF 和机构主导 ETF 的"申赎同步性"差异
散户主导 ETF 的申赎方向和"市场情绪指标"高度同步(同步性 0.85),机构主导 ETF 的申赎方向和"市场情绪指标"基本无关(同步性 0.18)。这个差异本身就是一种"反向指标"——如果散户主导 ETF 和机构主导 ETF 的申赎方向"分化"(散户买、机构卖,或散户卖、机构买),说明市场可能见顶/见底。
发现 4:主题 ETF 的"申赎波动"远大于宽基 ETF
主题 ETF(如新能源、AI、半导体)的申赎波动是宽基 ETF(如沪深 300、中证 500)的 3-5 倍。原因是散户对"主题"的炒作情绪远比"宽基"剧烈。这个波动本身就是"散户情绪"的量化指标。
六、踩过的坑
坑 1:ETF 申赎的"结算时差"
ETF 申赎数据有 T+1 结算时差,但盘中公布的"实时申赎数据"是 T 日的数据。如果用 T 日数据做 T 日决策,可能会被"未结算"的申赎干扰。我后来只用"已结算"数据(即 T-1 日的实际申赎数据)做 T 日决策。
坑 2:不要把"申赎笔数"等同于"申赎金额"
很多 ETF 的申赎笔数很多但金额很少(散户小额定投),反之亦然。我后来用"申赎金额"做主指标,"申赎笔数"做辅助指标。
坑 3:跨 ETF 的"成分股重叠"问题
很多 ETF 的成分股有大量重叠(比如沪深 300 ETF 和上证 50 ETF 的成分股有 80% 重叠)。如果同时统计这两个 ETF 的申赎数据,"成分股交易"会被重复计算。我后来做了一个"成分股去重"模块,把重叠的成分股交易合并计算。
坑 4:跨市场的"汇率影响"
QDII ETF 的申赎数据要经过汇率转换。我最初用"当日汇率"做转换,结果和实际结算金额差异较大。后来改成"次日汇率"做转换,差异从 1.5% 降到 0.2%。
七、信号输出和实战效果
最终输出的交易信号是这样的:
- 买入信号:某散户主导 ETF 出现大额净赎回(> 10 亿),且该 ETF 近 5 日跌幅 > 5%,且当日市场情绪指标显示"恐慌"
- 卖出信号:某散户主导 ETF 出现大额净申购(> 10 亿),且该 ETF 近 5 日涨幅 > 5%,且当日市场情绪指标显示"贪婪"
用这套信号回测过去 3 年:
- 年化收益:18.7%
- 最大回撤:12.3%
- 夏普比率:1.52
- 胜率:68%
效果相当不错。
八、最后说几句
ETF 申赎数据是 A 股的"金矿",但开采用法有讲究。几个核心建议:
- 一定要区分"散户主导"和"机构主导"的 ETF,两者信号方向相反
- 不要直接用"申赎金额",要做"散户情绪反向指标"
- 一定要做"结算时差"对齐,否则会被未结算数据干扰
- 跨 ETF 的"成分股去重"必须做,否则会出现重复计算
- 跨市场的 ETF 要做"汇率对齐",否则 QDII ETF 的数据会失真
这套系统上线之后,最大的变化不是"赚了多少",是明白了"散户对 ETF 的追涨杀跌行为是可以被量化和预测的"。当你看到一群人在高位大额申购 ETF 时,你就应该知道他们大概率会在高位被套。
投资市场里,最反常识的真相是——你以为的"机构工具",其实是"散户玩具"。
参考接口:
- ETF 申赎数据:ig50 本地数据引擎的 etf 数据集
- 股票列表:
base/gplist - 行业资金流向:
all/zjlx/zjhhy(用于辅助验证) - 实时行情:
time/real/{dm}(用于回测) - 历史 K 线:
time/history/trade/{dm}/{level}(用于回测)
资料参考:ig50