ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python财务指标选股实战:从数据清洗到多因子回测

Python财务指标选股实战:从数据清洗到多因子回测 简介基于Python与tushare的财务指标选股方案面向量化分析初学者及有选股需求的投资者解决如何利用公开财务数据批量筛选潜力股票的问题。资源包共2个文件压缩包大小1KB包含一个.py脚本与一个.txt说明文档脚本覆盖股票列表获取、财务报告数据提取、市盈率/净利润增长率等指标计算以及按自定义条件遍历筛选的核心流程说明文档则对各步骤逻辑和代码用途进行了逐段讲解方便对照学习。已有1567人浏览学习。借助这份示例读者可以快速理解tushare接口在财务因子选股场景中的典型用法并在此基础上扩展自身指标与策略减少从零搭建框架的重复工作提升选股判断的客观性和效率。尤其适合具备一定Python基础、希望将上市公司财报数据转化为可执行投资策略的开发者。1. 财务指标选股方法Python 到底在解决什么问题一个常见的场景你从行情软件里翻出十几家公司的 ROE 和毛利率手工排一排挑出净资产收益率最高、负债率最低的几只买入。这一步对十家公司可行但对三百家公司、每季度更新一次的全市场数据手工操作会在数据清洗这一步耗尽时间。Python 财务指标选股方法核心是把财报数据从报表格式转换成可计算的因子再把选股逻辑写成能重复执行的代码让同一套标准在每个报告期稳定跑完全市场。它不负责预测股价只负责把“按财务指标排列的股票池”这件事做得比人工更快、口径更一致、结果可复现。你要做的是把指标口径、数据源和阈值这三个决定成败的环节先锁住。2. 财务指标选股方法的第一步数据口径、提取与对齐2.1 指标口径先定死选股才谈得上复现财务指标选股方法里最容易被忽略的不是算法而是口径。ROE 有加权与摊薄两种计算方式毛利率有没有扣除税金及附加结果差异不小。实际操作中我会先固定一张指标清单它们来自三大报表盈利能力ROE、毛利率、净利率、偿债能力资产负债率、流动比率、营运能力存货周转率、应收账款周转率以及现金流质量经营现金流净额/净利润、销售收现比。指标报表来源口径注意点ROE利润表 资产负债表优先用加权 ROE别混用摊薄口径毛利率利润表不同行业差异极大需行业内比较资产负债率资产负债表银行、地产要剔除口径完全不同经营现金流净额/净利润现金流量表 利润表比值长期小于 0.8 时需警惕盈利质量财务指标选股方法的第一步不是去写取数代码而是把这份清单连同阈值一起定下来。阈值一旦改动后面的结果就必须重新回测否则你只是在“调参数碰运气”。比如 ROE 15%、资产负债率 60%这些数值没有普适性用全市场分位数比如 ROE 前 20%来定义阈值通常比硬编码更稳。2.2 数据源选择与 Python 抓取的最小代码数据源是影响整个选股效率的核心。常见做法是用 akshare 这类免费开源接口做原型验证它把东方财富、同花顺、新浪等来源封装成统一函数省去自己写爬虫解析网页的麻烦。先装好依赖确认 Python 环境用的是同一个解释器——尤其用 VS Code 时pip install装进系统环境、但编辑器用的是虚拟环境是出现“没有 pandas”最常见的坑。安装命令和最小拉取代码如下# 安装pip install akshare pandas import akshare as ak import pandas as pd # 1. 先确认当前库里有没有这个接口 if hasattr(ak, stock_financial_abstract_ths): # 2. 拉取平安银行的按报告期财务摘要 df ak.stock_financial_abstract_ths(symbol000001, indicator按报告期) print(df.columns.tolist())这段代码里symbol传 6 位股票代码indicator控制返回口径可填“按报告期/按年度/按单季度”。实际使用时先打印columns确认当前 akshare 版本的列名因为财务数据接口的字段经常随数据源页面调整。确认没问题后再对全市场循环拉取# 生产环境先拿全量代码列表 codes ak.stock_info_a_code_name()[code].tolist() frames [] for code in codes[:50]: # 先跑前 50 只验证速度 try: tmp ak.stock_financial_abstract_ths(symbolcode, indicator按报告期) tmp[code] code frames.append(tmp) except Exception as exc: print(f{code} 拉取失败: {exc}) continue raw pd.concat(frames, ignore_indexTrue)用try/except把单只股票的抓取包起来某只失败只打印一行不会中断整个循环。全市场五千多只单只耗时约 0.3 到 0.5 秒全量跑完要二三十分钟所以先codes[:50]做速度验证。正式跑全量时建议把结果存成 parquet 或 SQLite 作为本地缓存避免每次回测都重复请求远程接口。2.3 财务报表期与回测日期的对齐避免前视偏差财务指标选股方法里最大的坑是用“报告期”当作“可用日期”。三季报的截止披露日是 10 月 31 日但报告期是 9 月 30 日如果回测代码在 10 月 1 日就使用三季报那一个月的数据泄漏足以让年化收益虚高几个点。正确做法是用公告日期ann_date而不是报告期末end_date。如果数据源没有公告日字段就按披露截止日做保守平移def get_latest_financial(trade_date, financial_df): 在交易日获取当日真正可用的财务数据 available financial_df[financial_df[ann_date] trade_date] # 同一股票取最新一条按代码聚合 latest available.sort_values(end_date).groupby(code).tail(1) return latest这段逻辑的核心有两点先过滤“公告日期不晚于当前交易日”再按股票取最新报告期两个条件缺一不可。如果拿不到公告日期就用报告期加一个月作为保守延迟。一季报、中报、三季报的披露截止日分别是 4 月 30 日、8 月 31 日、10 月 31 日财报越晚披露这个时间差越大。提示财务数据接口的字段名随 akshare 版本更新而调整跑之前先用print(df.columns.tolist())检查实际列名不要盲写列名。3. 用 Python 实现财务指标选股的核心筛选与打分3.1 硬性条件筛股ROE、毛利率、负债率的阈值怎么给指标数据整理成宽表后第一步是硬性条件筛选。比如要求 ROE 大于 15%、毛利率大于 30%、资产负债率小于 60%同时排除 ST 股和金融行业。代码本身不复杂但数据类型的预处理要放在前面# merged: 多股票多报告期的指标宽表 merged[roe] pd.to_numeric(merged[roe], errorscoerce) merged[gross_margin] pd.to_numeric(merged[gross_margin], errorscoerce) merged[debt_ratio] pd.to_numeric(merged[debt_ratio], errorscoerce) pool merged.dropna(subset[roe, gross_margin, debt_ratio]) cond ( (pool[roe] 15) (pool[gross_margin] 30) (pool[debt_ratio] 60) (~pool[name].str.contains(ST, naFalse)) (~pool[industry].isin([银行, 非银金融, 多元金融])) ) pool pool[cond]pd.to_numeric(errorscoerce)这一步几乎每次必做财报源经常把“15.32%”这种带百分号的字符串直接给你不转成 float 后面所有比较都会报错。转不了的自动变 NaN先dropna兜底更细致的缺失值处理放到本章后面。阈值只给参考区间参数保守取值激进取值适用说明ROE 下限12%20%周期股要看三年均值单期容易失真毛利率下限20%40%传统制造偏低软件医药天然偏高资产负债率上限70%50%排除金融行业后使用地产单独处理硬筛选的问题是阈值收紧后股票池可能缩到个位数这时候就要把思路从“过滤”改成“打分”。这就是财务指标选股方法从初筛进入量化排序的分水岭。3.2 多因子打分排序百分位与 z-score 两种口径硬条件筛完剩下的股票如果还有一两百只就进入多因子打分。常见做法有两种截面百分位和 z-score 标准分。百分位最大的好处是天然免疫极值一只毛利率畸高的股票最多打到 1.0不会把其他样本压到看不见# 截面指同一报告期内比较避免不同报告期混在一起排名 pool[rank_roe] pool.groupby(end_date)[roe].rank(pctTrue) pool[rank_gross] pool.groupby(end_date)[gross_margin].rank(pctTrue) # 负债率越低越好用 1 - 排名 pool[rank_debt] 1 - pool.groupby(end_date)[debt_ratio].rank(pctTrue) # 综合分默认等权实际使用时按 IC 值动态调权 pool[total_score] ( pool[rank_roe] * 0.5 pool[rank_gross] * 0.3 pool[rank_debt] * 0.2 ) # 每个报告期取分数最高的前 30 只 top_pool pool.sort_values( [end_date, total_score], ascending[True, False] ).groupby(end_date).head(30)权重设置是整个财务指标选股方法里最需要谨慎对待的地方。等权只适合做基线真正应用时建议先用第 5 章的 IC 值决定权重ROE 的 IC 如果长期最高权重就放大到 0.5 甚至 0.6其他因子相应缩小。z-score 则适合指标分布接近正态的情况mean_roe pool.groupby(end_date)[roe].transform(mean) std_roe pool.groupby(end_date)[roe].transform(std) pool[roe_zscore] (pool[roe] - mean_roe) / std_roez-score 的坑在于财务指标普遍是厚尾分布某家公司一次巨额亏损会把 z-score 压到负几十其他样本全部挤在零附近。实战里我更常用rank(pctTrue)它只关心排序关系对单调变换完全免疫这正是选股方法里较少被提及但很实用的一点。3.3 极值缩尾、缺失值填充与行业分类处理某个公司的毛利率因为一次性资产处置收益变成 200%直接冲进前 1%会把整个排名带偏。量化解法是对极端值做缩尾winsorizefrom scipy.stats.mstats import winsorize # 对毛利率做 1%~99% 缩尾超边界的值被拉回边界 pool[gross_clean] winsorize( pool[gross_margin].fillna(pool[gross_margin].median()), limits[0.01, 0.01] )先填充中位数再缩尾顺序不能反。先缩尾的话缺失值会原样参与计算等把数据拉回边界之后发现 NaN 还在等于白处理。缺失值填充也要分情况毛利率缺失可以用行业中位数填资产负债率缺失就不填直接剔除因为缺失率高的样本本身报表质量就存疑。行业分类字段务必随报表一起保留财务指标选股方法里的“行业中性化”是在打分前先剥离行业差异这一块放到最后一章展开因为不能用全市场分位数替代行业内分位数。4. 财务指标选股策略的回测与排名验证4.1 报告期截面排名与分组用 groupby 控制年度和行业干扰回测的第一步是截面排名。注意groupby的键顺序先按报告期分组再按行业内分组。财务指标选股方法里同一报告期的数据才有可比性不同报告期混在一起排名遇到业绩整体向好的年份会失真# 行业内百分位industry 建议用申万一级行业分类 pool[industry_rank] pool.groupby( [end_date, industry] )[roe].rank(pctTrue) # 综合分里把行业排名加进去 pool[score_final] (pool[score_final] pool[industry_rank]) / 2为什么要加一个行业内排名因为全市场排名永远让白酒和钢铁放在同一尺度下比毛利率结果没有意义——前者毛利率普遍 70% 以上后者可能只有 15%。行业内排名把比较范围缩到同行才是真正能稳定运行的姿势。分组排序完成后一般把股票按分数切成五层Q1 到 Q5Q5 就是多头和回测的对象# 切成五层 pool[layer] pool.groupby(end_date)[score_final].transform( lambda x: pd.qcut(x.rank(methodfirst), 5, labelsFalse) 1 ) # 取分数最高的第五层 q5 pool[pool[layer] 5]这里用x.rank(methodfirst)给相同分数分先后顺序避免qcut在边界值上直接报错。没有这一行排名时一旦出现大量相同总分qcut的区间分位数就可能失效。4.2 最小可运行的回测循环怎么写回测框架不用求大而全几百行纯 Pandas 就能把逻辑讲清楚。核心循环只有三层调仓判断、取最近可用报告期、组合日收益等权计算def run_backtest(pick_df, price_df, rebalance_days21): pick_df: 每个 end_date 选出的 topN 组合 price_df: 交易日期 x 股票代码 的日收益率宽表 返回策略累计净值 trade_dates sorted(price_df.index) nav 1.0 current_pool [] next_rebalance trade_dates[0] for date in trade_dates: # 到达调仓日重新取最近可用的选出股票 if date next_rebalance: prev_report pick_df[pick_df[end_date] date][end_date].max() current_pool pick_df[pick_df[end_date] prev_report][code].tolist() next_rebalance date pd.Timedelta(daysrebalance_days) # 组合等权日收益 if current_pool: daily_ret price_df.loc[date, current_pool].fillna(0).mean() nav * (1 daily_ret) return navrebalance_days21是月度调仓的近似值一个自然月约 21 个交易日。如果调仓周期改成季度就改成 63。注意end_date date在这里是近似判断生产环境应该替换成第 2 章的ann_date date否则回测结果会受前视偏差污染。组合日收益用fillna(0)是假设停牌当天不产生收益这是一种偏乐观的处理正式回测还要考虑涨跌停无法成交和手续费率骨架先跑通。4.3 幸存者偏差和前视偏差财务因子回测最容易高估收益的地方回测出来的年化收益比实际操作高最常见的两个原因就是幸存者偏差和前视偏差。用今天的ak.stock_info_a_code_name()得到的股票列表回溯 2018 年会把当时上市、后来退市的公司全部忽略掉这就是幸存者偏差。规避方法是使用带历史成分的数据源拿到“当时上市且未退市”的股票如果数据源不支持就保留 ST 股票和退市风险高的公司不要人工剔除。前视偏差前面反复强调过凡是用财报的一律以公告日期为准。公告日期往往比报告期末晚 15 到 90 天。第二个容易被忽略的地方是财务数据修正年报披露时可能对前三季度数据做重述抓数时如果用当前最新快照做历史回测拿到的不是当时的真实财报而是修订后的版本。正确做法是历史时点回测只使用ann_date早于该时点的数据这是财务指标选股方法里回测环节最容易踩、也最影响结论的东西。5. 财务指标选股模型的进阶验证IC 检验与行业中性化5.1 行业中性化的两种实现路径前面反复强调过“不要跨行业比毛利率”业界做法分两种。第一种是行业分组百分位即在排名维度里直接加入industry第 4 章代码已经展示第二种是回归残差法把原始因子对行业虚拟变量做线性回归残差作为中性化后的新因子import statsmodels.formula.api as smf # 先做 z-score pool[roe_z] pool.groupby(end_date)[roe].transform( lambda x: (x - x.mean()) / x.std() ) # 对行业虚拟变量回归残差就是剔除行业影响后的因子 model smf.ols(roe_z ~ C(industry), datapool).fit() pool[roe_neutral] model.resid回归残差法的纯度更高行业属性被完全剥离代价是行业里样本太少时回归参数不稳定。分组法稳健但损失粒度回归法精细却依赖样本量。生产环境里我一般把两种结果对照着看差异过大说明行业间的盈利模式差距已经大到单一指标无法校正需要重新审视这个因子本身是否适合跨行业选股。5.2 用 IC 值判断财务指标是否真的有效ICInformation Coefficient衡量因子截面排名与下期收益截面排名的相关性。经验法则是 IC 绝对值大于 0.03 算勉强可用大于 0.05 算较强符号一致说明方向稳定。计算时用 Spearman 秩相关避免线性相关对极端值敏感from scipy.stats import spearmanr def calc_ic(factor_df, return_df, report_dates): factor_df: 每个报告期的因子值宽表 return_df: 下期收益宽表index 是报告期 ic_list [] for dt in report_dates: factor factor_df.loc[dt].dropna() # 收益对齐到因子索引 ret return_df.loc[dt].reindex(factor.index).dropna() common factor.index.intersection(ret.index) if len(common) 20: ic, _ spearmanr(factor[common], ret[common]) ic_list.append((dt, ic)) return pd.DataFrame(ic_list, columns[end_date, ic])计算 IC 的收益窗口必须与调仓周期对齐月度调仓就用下 20 个交易日收益季度调仓就用下 60 个交易日收益窗口错配会把 IC 值打散到没有参考意义。最后提一个会被大多数人忽略的细节财务指标的有效性会随时间衰减把 IC 序列按半衰期指数加权之后再决定打分权重优于固定一个历史权重。实操时从 60 个交易日的半衰期开始调IC 的滚动均值能稳定越过 0.03再把这个因子纳入评分体系。本文还有配套的精品资源点击获取
返回列表