ETF资金流监测系统实战:用Python构建场内场外套利信号识别
ETF是A股一个特殊的存在——它既是一篮子股票,又能在二级市场像股票一样交易。我做量化这几年,专门搭建了一套ETF资金流监测系统,用Python把场内场外的资金流数据打通,识别套利信号。这篇文章把核心实现分享出来。
ETF数据主要分四块。第一块是ETF基金列表,路径base/jjlbList(基金列表),返回所有ETF基金的代码、名称、类型、跟踪指数。第二块是ETF基金行情,路径time/jjHQ/{基金代码},返回ETF的实时行情。第三块是ETF基金规模变动,路径time/jjgm/{基金代码},返回ETF的份额变动历史。第四块是行业资金流向,路径all/zjlx/zjhhy(行业资金流向),返回各行业的资金流向数据。我把这四块数据全部沉到本地。
先说ETF基金列表怎么读。
import json
import os
import pandas as pd
data_dir = "D:/stock_data"
def read_etf_list():
file_path = os.path.join(data_dir, "base", "jjlbList")
if not os.path.exists(file_path):
return None
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
df = pd.DataFrame(data)
df.columns = ["dm", "mc", "jjlb", "gmbm", "gmrb", "gmsj", "gmjz", "gmje"]
return df
def read_etf_realtime(dm):
file_path = os.path.join(data_dir, "time", "jjHQ", dm)
if not os.path.exists(file_path):
return None
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
df = pd.DataFrame([data])
df.columns = ["dm", "mc", "now", "zf", "jk", "zs", "zsz", "cjsl", "cjje", "zt", "dt", "wb", "wp"]
return df
字段简写,dm基金代码mc基金名称jjlb基金类型gmbm规模编码gmrb规模日期gmje规模金额。实时行情字段now当前价zf涨跌幅jk今开zs昨收zsz总市值cjsl成交量cjje成交金额zt涨停dt跌停wb委买wp委卖。
第一步是分析ETF场内场外溢价。ETF同时在场内(二级市场)和场外(一级市场)交易,两个价格之间的差额就是套利空间。
def analyze_etf_premium(dm, lookback_days=30):
realtime = read_etf_realtime(dm)
if realtime is None:
return None
etf_list = read_etf_list()
if etf_list is None:
return None
target_etf = etf_list[etf_list["dm"] == dm]
if len(target_etf) == 0:
return None
etf_iopv = target_etf.iloc[0]["gmjz"]
etf_price = realtime["now"].iloc[0]
premium = (etf_price - etf_iopv) / etf_iopv * 100
print("ETF{}场内场外溢价分析:".format(dm))
print("场内价格:{:.4f}".format(etf_price))
print("场外净值:{:.4f}".format(etf_iopv))
print("溢价率:{:.2f}%".format(premium))
signal = None
if premium > 2.0:
signal = "场内高溢价,可考虑卖出套利"
elif premium < -1.0:
signal = "场内折价,可考虑买入套利"
else:
signal = "溢价正常,无套利空间"
return {"premium": premium, "signal": signal}
第二步是分析ETF份额变动。ETF份额变动反映了资金对这只ETF的态度,是大资金进场还是离场最直观的指标。
def read_etf_share_change(dm):
file_path = os.path.join(data_dir, "time", "jjgm", dm)
if not os.path.exists(file_path):
return None
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
df = pd.DataFrame(data)
df.columns = ["rq", "fbdw", "fqjz", "fcye", "zfdw", "zqjz", "zcye", "jzzzl"]
return df
def analyze_etf_share_change(dm, days=30):
df = read_etf_share_change(dm)
if df is None or len(df) == 0:
return None
df["fbdw"] = pd.to_numeric(df["fbdw"], errors="coerce")
df["zfdw"] = pd.to_numeric(df["zfdw"], errors="coerce")
df["rq"] = pd.to_datetime(df["rq"], errors="coerce")
recent = df.head(days)
total_inflow = recent["fbdw"].sum() / 1e8
total_outflow = recent["zfdw"].sum() / 1e8
net = total_inflow - total_outflow
print("ETF{}近{}日份额变动:".format(dm, days))
print("总申购:{:.2f}亿份".format(total_inflow))
print("总赎回:{:.2f}亿份".format(total_outflow))
print("净申购:{:.2f}亿份".format(net))
signal = None
if net > 1.0:
signal = "大额净申购,资金流入"
elif net < -1.0:
signal = "大额净赎回,资金流出"
else:
signal = "份额变动正常"
return {"net": net, "signal": signal}
第三步是行业ETF联动分析。行业ETF和行业指数高度联动,可以用行业资金流向数据来预判行业ETF的走势。
def read_industry_flow():
file_path = os.path.join(data_dir, "all", "zjlx", "zjhhy")
if not os.path.exists(file_path):
return None
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
df = pd.DataFrame(data)
df.columns = ["rq", "hy", "zjl", "zlJlr", "zlJlb", "shJlb", "ddy", "ddx", "ddz", "ddf", "f5MinZlJe"]
return df
def find_industry_etf_signals(date_str):
df = read_industry_flow()
if df is None:
return None
df["rq"] = pd.to_datetime(df["rq"], errors="coerce")
target = df[df["rq"].dt.strftime("%Y-%m-%d") == date_str]
if len(target) == 0:
return None
target["zlJlr"] = pd.to_numeric(target["zlJlr"], errors="coerce")
strong = target[target["zlJlr"] > 5]
weak = target[target["zlJlr"] < -5]
print("日期:{} 行业资金流向".format(date_str))
print("强势行业:")
for idx, row in strong.iterrows():
print(" {}:净流入{:.2f}亿".format(row["hy"], row["zlJlr"]))
print("弱势行业:")
for idx, row in weak.iterrows():
print(" {}:净流出{:.2f}亿".format(row["hy"], abs(row["zlJlr"])))
return {"strong": strong, "weak": weak}
第四步是构建完整的ETF套利信号系统。我把上面几个模块组合起来。
def etf_arbitrage_scan(date_str):
etf_list = read_etf_list()
if etf_list is None:
return None
etf_list = etf_list[etf_list["jjlb"].str.contains("ETF", na=False)]
signals = []
for dm in etf_list["dm"].tolist():
premium_result = analyze_etf_premium(dm)
share_result = analyze_etf_share_change(dm, days=10)
if premium_result is None or share_result is None:
continue
premium = premium_result["premium"]
net_share = share_result["net"]
if premium > 2.0 and net_share > 0.5:
signals.append({
"dm": dm,
"signal": "高溢价+净申购:可能有大资金申购套利,关注卖出机会",
"premium": premium,
"net_share": net_share
})
elif premium < -1.0 and net_share < -0.5:
signals.append({
"dm": dm,
"signal": "折价+净赎回:可能有大资金赎回套利,关注买入机会",
"premium": premium,
"net_share": net_share
})
return pd.DataFrame(signals)
这套ETF系统跑了一年,最关键的收获是:ETF的资金流向比价格更值得关注。当某只ETF出现大额净申购,但价格没怎么涨的时候,往往是聪明钱在悄悄建仓。反过来,当某只ETF大额净赎回但价格还坚挺的时候,往往是陷阱。
接口说明:
- ETF基金列表:base/jjlbList
- ETF实时行情:time/jjHQ/{dm}
- ETF份额变动:time/jjgm/{dm}
- 行业资金流向:all/zjlx/zjhhy
资料参考:ig50

1188

被折叠的 条评论
为什么被折叠?



