全市场财务数据挖掘实战用Python构建基本面因子分析系统做量化投资快六年了从最开始跟着网上的教程写简单的均线策略到后来逐渐接触多因子模型我最深的感触是数据质量和因子构建逻辑直接决定了策略的上限。很多人花大量时间调参优化却忽略了最基础的数据清洗和因子计算环节。这篇文章我想分享一下自己从零搭建全市场基本面因子分析系统的完整过程希望能帮到同样在摸索的朋友。先说说为什么要做这个系统。市面上的量化平台虽然方便但往往有几个问题一是数据更新不及时二是因子计算不透明三是无法灵活地进行自定义因子开发。所以从去年开始我决定自己搭一套本地的基本面因子分析系统。经过一段时间的试错终于形成了一套相对稳定的流程。整个系统的核心数据来源分三块第一块是全市场股票列表用来确定股票池第二块是每只股票的财务指标数据包括ROE、毛利率、净利率这些关键指标第三块是实时行情数据用来获取PE、PB、市值这些估值指标。数据都存在本地用的时候直接读文件速度比调接口快很多。先看数据读取部分。我把数据都按统一的目录结构存放股票列表在base/gplist目录下财务指标在time/f10/fi/{股票代码}实时行情在time/real/{股票代码}。写个读取函数就行这里我习惯用pandas来处理。importjsonimportosimportpandasaspdimportnumpyasnpfrompathlibimportPath data_dirD:/stock_datadefread_gplist():file_pathos.path.join(data_dir,base,gplist)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[dm,mc,jys,isCy,isKc,isSt,isNew]returndfdefread_fi(dm):file_pathos.path.join(data_dir,time,f10,fi,dm)ifnotos.path.exists(file_path):returnNonewithopen(file_path,r,encodingutf-8)asf:datajson.load(f)ifnotdata:returnNonedfpd.DataFrame(data)returndfdefread_real(dm):file_pathos.path.join(data_dir,time,real,dm)ifnotos.path.exists(file_path):returnNonewithopen(file_path,r,encodingutf-8)asf:datajson.load(f)returndata这里字段名我用了简写dm代表股票代码mc代表名称jys是交易所isCy、isKc、isSt、isNew分别表示是否创业板、科创板、ST、新股。这样写代码的时候简洁很多自己能看懂就行。接下来是数据清洗这一步非常关键。全市场有五千多只股票不是每只都适合做基本面分析的。首先要过滤掉ST股和新股ST股基本面有问题新股数据不完整都会影响因子分析的准确性。然后还要考虑数据的完整性有些股票的财务指标缺得厉害也要剔除。deffilter_stock_pool(df_gp):mask(df_gp[isSt]0)(df_gp[isNew]0)df_filtereddf_gp[mask].copy()returndf_filtered过滤完股票池就该计算因子了。我常用的基本面因子大概分几类盈利能力因子、成长能力因子、估值因子、偿债能力因子。每一类下面又有具体的指标。比如盈利能力因子里我会看ROE净资产收益率、销售毛利率、销售净利率这三个核心指标。ROE是巴菲特最看重的指标它反映了公司用股东的钱能赚多少回报。我一般取最近四个季度的加权净资产收益率来计算。销售毛利率和净利率则反映了公司的定价能力和成本控制能力这两个指标结合起来看更有意义。defcalc_profit_factors(dm):df_firead_fi(dm)ifdf_fiisNoneorlen(df_fi)0:returnNonefactors{}roe_col加权净资产收益率(%)ifroe_colindf_fi.columns:factors[roe]df_fi[roe_col].iloc[0]ifpd.notna(df_fi[roe_col].iloc[0])elsenp.nan gross_margin_col销售毛利率(%)ifgross_margin_colindf_fi.columns:factors[gross_margin]df_fi[gross_margin_col].iloc[0]ifpd.notna(df_fi[gross_margin_col].iloc[0])elsenp.nan net_margin_col销售净利率(%)ifnet_margin_colindf_fi.columns:factors[net_margin]df_fi[net_margin_col].iloc[0]ifpd.notna(df_fi[net_margin_col].iloc[0])elsenp.nanreturnfactors成长能力因子方面我主要看主营业务收入增长率和净利润增长率。这两个指标能反映公司的扩张速度。不过这里要注意一个问题增长率太高有时候不一定是好事可能是基数太低导致的。所以我通常会结合绝对值一起看或者做一个截尾处理把极端值去掉。估值因子就简单了直接从实时行情数据里取市盈率动态、市净率、总市值。这三个指标分别代表了市场对公司盈利的定价、对净资产的定价以及公司的规模大小。很多人喜欢用低PE、低PB选股但实际效果往往不如预期因为不同行业的估值水平本来就不一样这就涉及到后面要说的行业中性化处理。defcalc_valuation_factors(dm):real_dataread_real(dm)ifnotreal_data:returnNonefactors{}pe_col市盈率动态ifpe_colinreal_data:factors[pe]real_data[pe_col]ifreal_data[pe_col]andreal_data[pe_col]0elsenp.nan pb_col市净率ifpb_colinreal_data:factors[pb]real_data[pb_col]ifreal_data[pb_col]andreal_data[pb_col]0elsenp.nan mv_col总市值元ifmv_colinreal_data:factors[mv]real_data[mv_col]ifreal_data[mv_col]elsenp.nanreturnfactors现在因子计算出来了但还不能直接用来选股。最大的问题就是行业差异。比如银行股的PE普遍在5-10倍而科技股的PE可能几十上百倍你直接把所有股票放一起按PE排序选出来的肯定都是银行、地产这些低估值行业的股票根本达不到分散配置的效果。这时候就需要做行业中性化处理。所谓行业中性化就是把每个因子在行业内部做标准化处理消除行业之间的系统性差异。具体做法是先给每只股票打上行业标签然后按行业分组在组内计算因子的Z-score也就是减去均值再除以标准差。这样处理后不同行业的因子值就有了可比性。defneutralize_by_industry(df,factor_cols,industry_colhy):df_resultdf.copy()forcolinfactor_cols:df_result[col_neutral]np.nanforhy,groupindf_result.groupby(industry_col):iflen(group)5:continuevalsgroup[col].values mean_valnp.nanmean(vals)std_valnp.nanstd(vals)ifstd_val0ornp.isnan(std_val):continueneutral_vals(vals-mean_val)/std_val df_result.loc[group.index,col_neutral]neutral_valsreturndf_result说到行业标签这里还有个小问题。股票列表数据里没有直接的行业分类我是通过所属板块数据来获取的。每只股票的所属板块信息在time/f10/ssbk/{股票代码}目录下里面有概念板块和行业板块的信息。我一般用申万行业分类作为标准从所属板块数据里提取出申万一级行业。defget_industry(dm):file_pathos.path.join(data_dir,time,f10,ssbk,dm)ifnotos.path.exists(file_path):return其他withopen(file_path,r,encodingutf-8)asf:datajson.load(f)foritemindata:if所属板块initem.get(关键字,):contentitem.get(内容,)forpartincontent.split( ):ifpart.startswith(申万)and行业inpart:returnpartreturn其他行业中性化之后就可以把多个因子合成一个综合打分了。最简单的方法就是等权平均每个因子的中性化值加起来除以因子个数。如果对某些因子更有信心也可以给不同的权重。我个人比较喜欢用等权因为简单透明不容易过拟合。defcalc_composite_score(df,neutral_cols):df_resultdf.copy()df_result[composite_score]df_result[neutral_cols].mean(axis1)returndf_result最后就是排名选股了。把所有股票按综合打分从高到低排序选前N只就行。N的大小取决于你的资金量一般选30-50只比较合适既能分散风险又不会因为股票太多而管理不过来。defselect_stocks(df,n30):df_sorteddf.sort_values(composite_score,ascendingFalse)returndf_sorted.head(n)把这些步骤串起来就是一个完整的基本面因子分析流程。我一般每周跑一次先更新股票列表然后批量读取财务指标和行情数据计算因子、中性化、打分选股。整个过程五千多只股票跑下来大概十几分钟效率还可以。实际使用下来这套系统有几个明显的好处。第一是数据完全可控不用担心平台突然停服或者数据出错第二是因子计算逻辑透明可以随时调整优化第三是回测方便因为所有数据都在本地可以很容易地做历史回测验证因子的有效性。当然也有一些需要注意的地方。财务指标的数据更新频率是每天15:30而且是近四个季度的数据所以做回测的时候要注意避免未来函数。还有就是因子的有效性不是一成不变的有些因子在某些市场环境下表现好换个环境可能就失效了所以需要定期评估和更新因子库。另外我还做了一些扩展功能。比如在因子计算之后会检查每只股票的十大股东和股东变化趋势如果发现机构持仓比例高、股东户数持续减少的会额外加分。还会结合资金流向数据看看最近主力资金的动向作为辅助参考。总的来说搭建这套系统花了不少时间但确实是值得的。它让我对基本面量化有了更深刻的理解也让我的投资决策更加理性和系统。如果你也在做类似的事情希望这篇文章能给你一些启发。最后说一下我用的数据来源。数据来自ig50的本地数据接口数据结构清晰字段定义明确做二次开发很方便。有需要的朋友可以自己去了解一下。接口说明base/gplist - 沪深京A股列表本地路径数据存放目录/base/gplist字段说明股票代码(dm)、股票名称(mc)、交易所(jys)、是否创业板(isCy)、是否科创板(isKc)、是否ST(isSt)、是否新股(isNew)time/f10/fi/{股票代码} - 财务指标本地路径数据存放目录/time/f10/fi/{股票代码}主要字段报告日期、加权净资产收益率(%)、销售毛利率(%)、销售净利率(%)、主营业务利润率(%)、总资产净利润率(%)、营业利润率(%)、主营业务收入增长率(%)、净利润增长率(%)、净资产增长率(%)、总资产增长率(%)、流动比率、速动比率、资产负债率(%)等time/real/{股票代码} - 实时行情数据本地路径数据存放目录/time/real/{股票代码}主要字段代码、名称、当前价格、昨收价、涨跌幅(%)、总市值(元)、流通市值(元)、市盈率(动态)、市盈率TTM、市净率、每股收益、每股净资产、换手率(%)等time/f10/ssbk/{股票代码} - 所属板块本地路径数据存放目录/time/f10/ssbk/{股票代码}主要字段关键字、内容包含行业板块、概念板块等信息gitee开源地址github开源地址