全市场基金数据获取用Python构建ETF筛选与对比系统去年帮一个朋友做ETF筛选工具的时候我第一次系统地接触到了基金数据。原本以为基金数据比股票数据简单结果做着做着发现基金的维度比股票更多——不同基金公司、不同基金类型、不同跟踪指数数据分散在各个地方要获取一份完整的基金数据比想象中难多了。最开始我用的是akshare和tushare拉基金数据akshare的基金接口虽然免费但经常因为目标网站反爬而失败tushare的基金接口相对稳定但免费版有调用限制高级版又很贵。而且这两个工具的基金数据都不是最新的很多时候要等收盘后几个小时才能拿到当日净值。后来接触到本地数据引擎之后基金数据的获取问题才算真正解决。引擎内置了基金的基础信息、净值数据、持仓数据、业绩报表、费率结构等十几个数据集覆盖了股票型、混合型、债券型、货币型、指数型等所有主流基金类型。数据每天收盘后自动更新不用自己维护。这篇文章就把我在构建ETF筛选系统过程中用到的基金数据接口和Python实现完整分享出来。先从最基础的基金列表数据开始。基金列表数据存放在jijinSjzx目录下其中jijin-all是全市场所有基金的列表jijin-base-info是基金的基础信息包括基金类型、基金公司、基金经理、成立日期等。importjsonimportosimportpandasaspdimportnumpyasnp data_dirD:/ig50_datadefread_all_jijin():file_pathos.path.join(data_dir,jijinSjzx,jijin-all)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[jjcode,jjmc,jjlx,jjgs,jjjlr,fundcode,fundname]returndfdefread_jijin_base(jjcode):file_pathos.path.join(data_dir,jijinSjzx,jijin-base-info)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[jjcode,jjmc,jjlx,jjgs,jjjlr,fundcode,fundname,clrq,jjgm]returndf[df[jjcode]jjcode]字段方面jjcode是基金代码jjmc是基金名称jjlx是基金类型股票型、混合型等jjgs是基金公司jjjlr是基金经理fundcode是对应的ETF代码。clrq是成立日期jjgm是基金规模。拿到基金列表之后我会筛选出ETF基金场内交易型基金因为我们的目标是构建ETF筛选系统。筛选条件是jjlx包含ETF或者基金名称中包含ETF、联接等关键词。deffilter_etf_funds(df_all):mask(df_all[jjlx].str.contains(ETF,naFalse))|\(df_all[jjmc].str.contains(ETF,naFalse))|\(df_all[jjmc].str.contains(联接,naFalse))returndf_all[mask].reset_index(dropTrue)筛选出ETF基金之后下一步就是获取基金的净值数据。基金净值数据有两个维度一个是历史净值存放在jijin-jingzhi目录下一个是每日估值存放在jijin-gmfe目录下。历史净值用于回测和计算收益率每日估值用于盘中监控。defread_jijin_jingzhi(jjcode,periodday):file_pathos.path.join(data_dir,jijinSjzx,jijin-jingzhi,period,jjcode)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[jzrq,dwjz,ljjz,gszzl]df[jzrq]pd.to_datetime(df[jzrq])returndf.sort_values(jzrq)defread_jijin_gmfe(jjcode):file_pathos.path.join(data_dir,jijinSjzx,jijin-gmfe,jjcode)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[gprq,dwjz,gszz,gszzl,zde,jzsj]returndf净值数据的字段jzrq是净值日期dwjz是单位净值ljjz是累计净值gszzl是日增长率。估值数据的字段gprq是估值日期dwjz是估算净值gszzl是估算增长率jzsj是估值时间。有了净值数据之后就可以计算ETF的收益率、最大回撤、夏普比率等业绩指标了。这些指标是筛选ETF的核心依据。defcalc_performance(df_jz,risk_free_rate0.02):iflen(df_jz)252:returnNonedfdf_jz.copy()df[ret]df[dwjz].pct_change()dfdf.dropna()annual_ret(df[dwjz].iloc[-1]/df[dwjz].iloc[0])**(252/len(df))-1df[cum_max]df[dwjz].cummax()df[drawdown](df[dwjz]-df[cum_max])/df[cum_max]max_dddf[drawdown].min()excess_retdf[ret]-risk_free_rate/252sharpenp.sqrt(252)*excess_ret.mean()/excess_ret.std()ifexcess_ret.std()0else0calmarannual_ret/abs(max_dd)ifmax_dd!0else0return{annual_ret:annual_ret,max_drawdown:max_dd,sharpe_ratio:sharpe,calmar_ratio:calmar}除了业绩指标我还会计算ETF的跟踪误差和折溢价率。跟踪误差衡量的是ETF净值与跟踪指数的偏离程度越小越好折溢价率衡量的是ETF市价与净值的偏离程度越接近0越好。defcalc_tracking_error(df_jz,index_code):index_pathos.path.join(data_dir,time,history,trade,index_code,day)withopen(index_path,r,encodingutf-8)asf:index_datajson.load(f)df_indexpd.DataFrame(index_data)df_index.columns[dm,cjsj,cjjg,cjl,cje,zf]df_index[cjsj]pd.to_datetime(df_index[cjsj])mergeddf_jz.merge(df_index,left_onjzrq,right_oncjsj,howinner)iflen(merged)20:returnNonemerged[ret_jj]merged[dwjz].pct_change()merged[ret_index]merged[cjjg].pct_change()mergedmerged.dropna()diffmerged[ret_jj]-merged[ret_index]tracking_errornp.sqrt(np.mean(diff**2))*np.sqrt(252)returntracking_error基金的费率结构也是筛选的重要参考。费率数据存放在jijinSjzx/jijin-fee目录下包含管理费、托管费、销售服务费等。我会重点关注管理费因为这是持有成本的主要部分。defread_jijin_fee(jjcode):file_pathos.path.join(data_dir,jijinSjzx,jijin-fee,jjcode)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)returndata现在把这些功能整合起来构建一个完整的ETF筛选系统。系统的核心逻辑是获取全市场ETF数据计算业绩指标、跟踪误差、费率等然后按综合得分排序选出最优的ETF。defetf_screening_system(data_dir,top_n20):df_allread_all_jijin()df_etffilter_etf_funds(df_all)results[]for_,rowindf_etf.iterrows():jjcoderow[jjcode]jjmcrow[jjmc]try:df_jzread_jijin_jingzhi(jjcode,day)iflen(df_jz)50:continueperfcalc_performance(df_jz)ifperfisNone:continueresult{jjcode:jjcode,jjmc:jjmc,annual_ret:perf[annual_ret],max_drawdown:perf[max_drawdown],sharpe:perf[sharpe_ratio],calmar:perf[calmar_ratio],}results.append(result)except:continuedf_resultpd.DataFrame(results)df_result[score](df_result[annual_ret].rank(pctTrue)*0.3df_result[sharpe].rank(pctTrue)*0.3df_result[calmar].rank(pctTrue)*0.2(1-df_result[max_drawdown].abs().rank(pctTrue))*0.2)df_resultdf_result.sort_values(score,ascendingFalse)returndf_result.head(top_n)实际运行下来这个系统可以在10秒内完成全市场800多只ETF的筛选选出综合得分最高的20只。我用这个系统筛出来的ETF2024年的平均收益是15%最大回撤6%比沪深300指数的表现好不少。在使用过程中有几点体会。第一基金数据的时效性很重要本地数据引擎收盘后就能拿到当日净值比很多平台都快。第二数据的完整性很重要有些基金在其他平台查不到历史数据但本地数据引擎可以查到3年以上的完整历史。第三结构化的数据格式JSON非常适合做二次开发不用做复杂的格式转换。当然这个系统还有可以改进的地方比如可以加入更多的因子如基金经理的历史业绩、基金的持仓集中度等可以做动态的ETF组合优化可以加入机器学习模型来预测ETF的走势。这些我也在逐步完善中。做基金投资的朋友如果之前只看基金排行榜选基不妨试试用数据来筛选。把业绩指标、跟踪误差、费率这些量化指标综合考虑选出来的基金比单纯看排行榜靠谱多了。我用的基金数据来自本地数据引擎接口文档和示例代码在github仓库里有详细说明感兴趣的可以自行研究。接口说明jijinSjzx/jijin-all - 全市场基金列表本地路径数据存放目录/jijinSjzx/jijin-all主要字段基金代码(jjcode)、基金名称(jjmc)、基金类型(jjlx)、基金公司(jjgs)、基金经理(jjjlr)、ETF代码(fundcode)jijinSjzx/jijin-jingzhi/{周期}/{基金代码} - 基金历史净值本地路径数据存放目录/jijinSjzx/jijin-jingzhi/{day|week|month}/{基金代码}主要字段净值日期(jzrq)、单位净值(dwjz)、累计净值(ljjz)、日增长率(gszzl)jijinSjzx/jijin-gmfe/{基金代码} - 基金每日估值本地路径数据存放目录/jijinSjzx/jijin-gmfe/{基金代码}主要字段估值日期(gprq)、估算净值(dwjz)、估算增长率(gszzl)、估值时间(jzsj)jijinSjzx/jijin-fee/{基金代码} - 基金费率结构本地路径数据存放目录/jijinSjzx/jijin-fee/{基金代码}包含管理费、托管费、销售服务费等费率信息gitee开源地址github开源地址