
简介一个基于Python和Tushare的财务指标选股实战包专为金融从业者、量化投资爱好者以及有Python基础的数据分析人员设计用于解决从A股市场批量提取财务数据、计算关键财务指标并筛选潜力股票的问题。资源覆盖股票列表获取、财务报告读取、市盈率与净利润增长率计算、自定义条件选股的完整流程毋需逐个翻阅上市公司财报即可快速缩小跟踪范围。压缩包内共2个文件包括1个Python选股脚本和1个TXT程序说明文档脚本内部实现了上述选股逻辑说明文档则对每一步代码和参数含义做了详细注释整个包仅1KB轻量且便于直接阅读和二次开发。已有1567人参与学习或下载代码结构清晰替换Tushare token后即可运行适合用作业财选股策略入门及量化框架搭建的参考。读者还可举一反三在现有指标基础上扩展ROE、PEG、营收增速等因子灵活调整筛选条件从而建立个性化的财务指标选股模型。1. 为什么用 Python 做财务指标选股而不是 Excel 或同花顺条件选股如果你只是想在周末筛一批市盈率低于 20 的股票同花顺问财或者东方财富的选股器 30 秒就能搞定没必要写代码。但当你需要把「ROE 连续三年大于 15%、营收增速大于 20%、且剔除商誉占净资产比过高的公司」这种复合条件做成一个每周自动跑一遍的流程时图形化工具的劣势就出来了条件写死、逻辑不透明、没法回溯历史调参。这时候用 Python 加 tushare 拉财务数据自己写筛选脚本等于把选股逻辑变成了可版本管理、可回测、可复现的工程。本文要拆的这份「财务指标选股 Python 源码」正好覆盖了这条链路从 tushare 拿股票列表和财务报告计算 PE、净利润增速、ROE 等指标再按条件过滤出候选股票。适合两类人一是刚开始接触量化选股、想搞明白财务数据怎么和行情数据对齐的初学者二是已经在用现成选股器、但觉得阈值不可控、想自己掌控筛选逻辑的从业者。下面的内容会直接落到选股.py里你会遇到的真实问题——接口怎么调、字段怎么对齐、指标怎么算才不会踩到「用的财报还没披露」这种坑。2. tushare 数据接口选型与股票池构建2.1 接口不是平替stock_basic、daily_basic、fina_indicator 的分工tushare 的 pro 版本接口很多但做财务选股真正高频用到的就三个stock_basic拿股票列表daily_basic拿每日估值指标PE、PB、总市值fina_indicator拿财务指标ROE、净利润增长率、毛利率。很多人第一次写的时候容易混淆income是原始利润表字段是n_income_attr_p这种会计科目而fina_indicator是已经帮你算好的指标比如roe、or_yoy、netprofit_yoy省去了自己手工算环比和同比的麻烦。import tushare as ts ts.set_token(你的token) # 在 tushare.pro 个人主页复制 pro ts.pro_api() # 获取当前正常上市的 A 股列表排除退市和暂停上市 stock_list pro.stock_basic(exchange, list_statusL, fieldsts_code,name,industry,market,list_date) print(stock_list.shape) print(stock_list.head(3))代码里list_statusL表示只取上市状态正常的股票fields显式指定字段可以省流量跑全市场时这一项影响速度。ts_code是 tushare 的股票唯一编码后面所有接口都用它关联数据注意 A 股的格式是000001.SZ这样的后缀和你在行情软件里看到的裸代码不一样很多新手在这里踩了第一个坑。2.2 估值指标与财务指标怎么对齐到同一天选股策略里经常需要「某天的 PE」和「该季度财报的 ROE」同时出现。daily_basic是日频数据每天都有fina_indicator是季频数据只有财报发布日才有。对齐的要点是只能用财报的ann_date公告日期去匹配不能用end_date报告期截止日。因为 3 季报可能在 10 月底才披露如果你用end_date去对齐 9 月 30 日的行情等于用了未来数据。# 以贵州茅台为例拿最近一天的估值数据 df_basic pro.daily_basic(ts_code600519.SH, start_date20240101, end_date20240630, fieldsts_code,trade_date,pe,pb,total_mv,turnover_rate) # 拿 2023 年报和 2024 一季报的财务指标 df_fina pro.fina_indicator(ts_code600519.SH, start_date20230101, end_date20240630, fieldsts_code,ann_date,end_date,roe,netprofit_yoy,or_yoy)trade_date和ann_date都是YYYYMMDD格式的字符串对齐时直接比较大小即可。日常写策略我会建议以daily_basic的交易日为主表把满足ann_date trade_date的最新一条fina_indicator横向拼接过去。这种方法在选股.py里通常会被封装成一个merge_finance_with_daily()函数核心逻辑就是排序后去重取最近一期。2.3 拉全市场数据的节奏控制全市场 5000 多只股票如果逐只调用fina_indicator会触发 tushare 的每分钟接口调用频率限制。积分配额低的账号每分钟只能调 50 次左右逐只拉 5000 次要 100 分钟完全不可行。所以正确的顺序是先用fina_indicator按报告期整体拉一次全市场数据再在本地做过滤而不是先循环股票再去拉财务。# 推荐做法按报告期批量拉全市场财务指标 df_all_fina pro.fina_indicator(period20240331, fieldsts_code,ann_date,end_date,roe,netprofit_yoy,or_yoy,grossprofit_margin) df_all_basic pro.daily_basic(trade_date20240630, fieldsts_code,trade_date,pe,pb,total_mv)按period和trade_date各调一次接口得到两张宽表后续所有筛选都在内存里做一分钟内就能跑完。这个思路是财务选股脚本性能优化里最值得记住的一点先按维度批量拉再在本地连接而不是在循环里反复请求接口。3. 财务指标的计算逻辑与选股.py 核心实现3.1 哪些指标直接取数哪些必须自己算tushare 的fina_indicator已经覆盖了大多数常用指标但实际写策略时会发现两类情况一类是接口直接给比如roe净资产收益率、netprofit_yoy归母净利润同比增长率、or_yoy营业收入同比增长率另一类是接口不直接给、需要自己组合的比如 PEG 需要 PE 除以净利润增速或者「连续三年 ROE 大于 15%」需要把历史多期的roe字段做时间序列判断。# 计算 PEGpe 来自 daily_basicnetprofit_yoy 来自 fina_indicator def calc_peg(row): if row[netprofit_yoy] is None or row[netprofit_yoy] 0: return None return round(row[pe] / row[netprofit_yoy], 2) df_merged[peg] df_merged.apply(calc_peg, axis1)calc_peg里先判断netprofit_yoy是否大于 0因为负增长时 PEG 没有意义返回None的股票会在后续筛选里被排除。这个处理的细节值得留意很多新手直接相除结果跑出一堆负 PEG 被当成低估值买点实际上是净利润大幅下滑的困境股。apply按行调用函数全市场 5000 行数据耗时不到 1 秒性能上没有压力。3.2 用财务数据过滤次新股和垃圾标的拿到原始数据后第一步不是算指标而是清洗。次新股上市时间短历史财务数据不完整容易被策略误选ST 股数据波动大存在退市风险一般策略直接排除。这些过滤条件应该写在指标计算之前避免后面白白计算。# 过滤剔除 ST、上市不足 3 年、市值小于 50 亿的标的 import datetime def build_pool(df): today datetime.date.today() df[list_days] (today - pd.to_datetime(df[list_date])).dt.days df df[~df[name].str.contains(ST|退, naFalse)] # 剔除 ST 和退市整理 df df[df[list_days] 365 * 3] # 上市满 3 年 df df[df[total_mv] 500000] # 总市值大于 50 亿单位万元 return dftotal_mv的单位是万元所以 50 亿市值的判断条件是 500000。很多资料上写total_mv直接和数字比较不提单位换算导致筛出来的结果全是巨无霸或者全被过滤掉这是实战里最容易忽略的单位问题。dt.days返回的是整数天数和 365 比较时注意list_date要先转成datetime类型。3.3 多财务指标打分从过滤到排名的演化简单过滤只能告诉你「哪些股票符合条件」但如果符合条件的股票有 300 只你还需要一个排序机制。常见做法是用多个财务指标做打分比如 ROE 排名前 20% 得 5 分、净利润增速排名前 20% 得 5 分最后按总分排序取前 50 只。# 对 ROE 和净利润增速做分位数打分 def add_scores(df): df[roe_score] pd.qcut(df[roe].rank(methodfirst), 5, labels[1, 2, 3, 4, 5]) df[growth_score] pd.qcut(df[netprofit_yoy].rank(methodfirst), 5, labels[1, 2, 3, 4, 5]) df[total_score] df[roe_score].astype(int) df[growth_score].astype(int) return df.sort_values(total_score, ascendingFalse)pd.qcut做的是等频分箱也就是按排名分成 5 组每组 20% 的股票。使用rank(methodfirst)是为了处理同值情况避免qcut因为边界重复而报错。astype(int)是因为qcut默认返回类别类型不能直接相加。这个方法适合对同行业内股票做相对比较——财务指标在不同行业之间差异极大银行股 PE 天然比科技股低跨行业直接比 PE 阈值是不合理的而分位数打分天然避免了这个问题。4. 从单一阈值到组合策略选股条件的实际参数怎么定4.1 筛选条件拆解净利润质量比增速更重要很多初版策略习惯用「净利润增长率 30%」做核心筛选但这类股票往往是因为上一年基数太低或者靠非经常性损益撑起了报表。真正财务选股做得久的普遍会用「扣非净利润增速」而不是「净利润增速」同时要求「经营现金流净额大于净利润」这两条可以从数据源上过滤掉相当一部分财务质量存疑的公司。指标来源接口推荐筛选条件说明扣非净利润同比增长率fina_indicator.netprofit_yoy 15%剔除一次性收益干扰ROE加权fina_indicator.roe 12%连续 3 期均满足可加分经营现金流净额/净利润cashflow自行计算 0.8利润要有现金支撑毛利率fina_indicator.grossprofit_margin 25%行业不同需调整资产负债率balancesheet自行计算 65%排除高杠杆企业4.2 完整筛选代码参数化让你一周调一次仓不费劲实际运行时选股条件会被封装成一个配置字典而不是散落在代码里的魔法数字。这样调参只需要改文件头部的参数表不需要动主逻辑。这个思路在选股.py里体现为CONFIG和run_select()的分离。# 选股配置参数表 CONFIG { start_date: 20240101, # 回看区间起点 end_date: 20240630, # 回看区间终点 min_roe: 12.0, # ROE 最低值% min_growth: 15.0, # 扣非净利润增速最低值% max_pe: 40.0, # PE 上限超过则排除 min_mv: 500000.0, # 最小总市值万元 min_gross_margin: 25.0, # 毛利率最低值% max_debt_ratio: 65.0, # 资产负债率最高值% stk_num: 50 # 最终输出的股票数量 } def run_select(): # 1. 构建股票池 stock_list pro.stock_basic(list_statusL, fieldsts_code,name,list_date) df_basic pro.daily_basic(trade_dateCONFIG[end_date], fieldsts_code,pe,pb,total_mv,turnover_rate) df_fina pro.fina_indicator(periodCONFIG[end_date], fieldsts_code,roe,netprofit_yoy,grossprofit_margin) # 2. 合并数据 df stock_list.merge(df_basic, onts_code, howinner) df df.merge(df_fina, onts_code, howinner) # 3. 应用筛选条件 df df[(df[roe] CONFIG[min_roe]) (df[netprofit_yoy] CONFIG[min_growth]) (df[pe] CONFIG[max_pe]) (df[total_mv] CONFIG[min_mv]) (df[grossprofit_margin] CONFIG[min_gross_margin])] # 4. 按 ROE 降序、PE 升序综合排序 df df.sort_values([roe, pe], ascending[False, True]) return df.head(CONFIG[stk_num])主流程分三步取数、合并、过滤排序。合并用的howinner意思是三张表中都存在的股票才会保留任何一张表缺数据都会被剔除。排序用ascending[False, True]表示 ROE 从高到低、PE 从低到高这样处理之后前 50 只大致是「高 ROE 低估值」的集合。实际运行时建议在合并后打印一下df.shape如果行数低于 1000大概率是trade_date或period选到了一个没有数据的日期。4.3 参数调整的行业差异陷阱固定阈值最大的问题在于它不区分行业。同样是 ROE白酒行业的龙头常年高于 25%而公用事业类的公司达到 10% 就算优秀同样看 PE银行股在 5 倍左右波动半导体公司 50 倍也常见。如果你把「ROE 15%」和「PE 30」同时作为硬性条件筛选结果会高度集中于消费和医药板块而把周期股和金融股全部排除。一个可行的调整方式是引入行业中性化先按industry分组然后在行业内做分位数筛选。比如要求 ROE 在所处行业中排名前 30%代替绝对阈值min_roe。这样选的逻辑不再依赖你对每个行业的主观判断而是假设「好公司是同行比出来的而不是靠统一分数线比出来的」。注意stock_basic里的industry字段用的是申万行业分类粒度较粗。如果你做精细化策略可以用 tushare 的index_classify拿更细的行业分类再和stock_basic关联。5. 跑批验证与数据质量检查一个值得照抄的排错方案5.1 先检查数据行数再谈选股结果选股脚本跑完如果结果和你预期差别很大不要急着调阈值先检查三个地方原始数据行数是不是合理、merge之后是不是丢了很多股票、以及财务指标字段是不是大量为空。tushare 的免费积分等级下fina_indicator部分历史字段可能没有权限返回的全是NaN这时候筛选结果为空不是因为策略差而是数据源本身缺字段。# 数据质量自检函数 def check_data_quality(df, df_name): total len(df) non_null_rate df.notna().mean().round(3) print(f{df_name}: 总行数 {total}关键字段非空率) print(non_null_rate) # 使用示例 check_data_quality(df_fina, fina_indicator)逻辑说明notna()返回布尔矩阵mean()计算每列非空值的占比round(3)保留三位小数。如果roe的非空率低于 0.9说明这期报告期拉下来的数据本身覆盖不全应该考虑换period或者检查 token 权限。5.2 用公告日期回验防止未来函数财务选股最容易犯的错误是「用了未来数据」。比如现在是 2024 年 8 月但 2024 年半年报还没披露完如果你直接按period20240630拉数据tushare 可能返回空或者只返回部分已披露公司的数据。这时候应该改用ann_date区间拉取保证你拿到的都是已经真实公告的数据。# 以公告日拉取更稳妥end_date 表示公告日期 df_fina_safe pro.fina_indicator(ann_date20240830, fieldsts_code,end_date,ann_date,roe,netprofit_yoy)这种写法只拉在 2024 年 8 月 30 日当天公告过财报的公司用于回测时能精确对齐历史调仓时间点。对比另一段代码里用period的方式ann_date方式能彻底避免数据穿越你在 2024 年 9 月 1 日回测时永远不可能用到 9 月 2 日才披露的财报。5.3 跑批上手即用的校验模板把检查写成函数实际项目里数据质量检查这步值得做成固定函数每周跑批时自动执行。下面这个模板检查了空值率、重复值、时间戳合理性三个维度任何一项异常都会打印警告不会闷头跑出一份漂亮但实际上不可用的选股结果。def validate_dataset(df, unique_col, date_col): # 1. 重复值检查 if df[unique_col].duplicated().any(): dup df[unique_col].duplicated().sum() print(f[警告] 存在 {dup} 条重复记录) # 2. 日期范围检查 if date_col in df.columns: print(f[信息] 日期范围: {df[date_col].min()} ~ {df[date_col].max()}) # 3. 关键字段空值率检查 key_fields [roe, pe, total_mv] missing_df df[key_fields].isnull().mean() if (missing_df 0.2).any(): print(f[警告] 以下字段空值率超过 20%:\n{missing_df[missing_df 0.2]})参数说明unique_col传ts_code用来检查是否有重复股票date_col传trade_date用来确认数据区间没有偏差。空值率阈值 0.2 是一个经验值行情类数据一般不会有超过 20% 的缺失如果出现就要去检查 tushare 的 token 权限是否覆盖了目标接口。这套模板复制到任何选股.py里都能直接用跑批之前先validate_dataset比对着选股结果猜原因高效得多。本文还有配套的精品资源点击获取