简介:基于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(symbol="000001", 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(symbol=code, indicator="按报告期") tmp["code"] = code frames.append(tmp) except Exception as exc: print(f"{code} 拉取失败: {exc}") continue raw = pd.concat(frames, ignore_index=True)用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"], errors="coerce") merged["gross_margin"] = pd.to_numeric(merged["gross_margin"], errors="coerce") merged["debt_ratio"] = pd.to_numeric(merged["debt_ratio"], errors="coerce") 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", na=False)) & (~pool["industry"].isin(["银行", "非银金融", "多元金融"])) ) pool = pool[cond]pd.to_numeric(errors="coerce")这一步几乎每次必做,财报源经常把“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(pct=True) pool["rank_gross"] = pool.groupby("end_date")["gross_margin"].rank(pct=True) # 负债率越低越好,用 1 - 排名 pool["rank_debt"] = 1 - pool.groupby("end_date")["debt_ratio"].rank(pct=True) # 综合分:默认等权,实际使用时按 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(pct=True),它只关心排序关系,对单调变换完全免疫,这正是选股方法里较少被提及但很实用的一点。
3.3 极值缩尾、缺失值填充与行业分类处理
某个公司的毛利率因为一次性资产处置收益变成 200%,直接冲进前 1%,会把整个排名带偏。量化解法是对极端值做缩尾(winsorize):
from 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(pct=True) # 综合分里把行业排名加进去 pool["score_final"] = (pool["score_final"] + pool["industry_rank"]) / 2为什么要加一个行业内排名?因为全市场排名永远让白酒和钢铁放在同一尺度下比毛利率,结果没有意义——前者毛利率普遍 70% 以上,后者可能只有 15%。行业内排名把比较范围缩到同行,才是真正能稳定运行的姿势。分组排序完成后,一般把股票按分数切成五层(Q1 到 Q5),Q5 就是多头和回测的对象:
# 切成五层 pool["layer"] = pool.groupby("end_date")["score_final"].transform( lambda x: pd.qcut(x.rank(method="first"), 5, labels=False) + 1 ) # 取分数最高的第五层 q5 = pool[pool["layer"] == 5]这里用x.rank(method="first")给相同分数分先后顺序,避免qcut在边界值上直接报错。没有这一行,排名时一旦出现大量相同总分,qcut的区间分位数就可能失效。
4.2 最小可运行的回测循环怎么写
回测框架不用求大而全,几百行纯 Pandas 就能把逻辑讲清楚。核心循环只有三层:调仓判断、取最近可用报告期、组合日收益等权计算:
def run_backtest(pick_df, price_df, rebalance_days=21): """ 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(days=rebalance_days) # 组合等权日收益 if current_pool: daily_ret = price_df.loc[date, current_pool].fillna(0).mean() nav *= (1 + daily_ret) return navrebalance_days=21是月度调仓的近似值,一个自然月约 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)", data=pool).fit() pool["roe_neutral"] = model.resid回归残差法的纯度更高,行业属性被完全剥离;代价是行业里样本太少时回归参数不稳定。分组法稳健但损失粒度,回归法精细却依赖样本量。生产环境里我一般把两种结果对照着看,差异过大说明行业间的盈利模式差距已经大到单一指标无法校正,需要重新审视这个因子本身是否适合跨行业选股。
5.2 用 IC 值判断财务指标是否真的有效
IC(Information 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,再把这个因子纳入评分体系。
本文还有配套的精品资源,点击获取