
简介面向希望用Python搭建基本面量化选股流程的投资者、金融专业学生和数据分析开发者这份资源围绕财务指标选股的核心需求展示了如何结合tushare库完成股票数据的获取、加工与筛选。资源包共2个文件包含一个“选股.py”Python脚本和一份“程序说明.txt”压缩包仅约1KB体量轻巧核心逻辑集中。脚本演示了调用get_stock_basics获取全部股票列表、get_report_data拉取财务报表进而计算每股收益、市盈率、市净率、净资产收益率等关键指标并以市盈率低于30、净资产收益率高于10%等条件进行布尔筛选形成一整套可复用的选股框架。说明文档则对实现步骤和指标含义做了补充梳理方便读者对照代码理解原理也可以直接修改阈值或换用其他财务字段扩展为个性化策略。目前已有1219人浏览学习适合刚接触tushare或希望快速搭建选股工具的读者。1. 从财报到代码财务指标选股不再是手工活用财务指标挑股票说起来直接打开行情软件翻财务报表算EPS、PE、PB、ROE再横向对比。但全市场五千多只股票手工筛一遍就够呛季度财报一更新所有计算还得重来一遍。把“财务指标选股”固化成Python代码用tushare拉数据、算指标、设阈值几分钟就能把全市场过一遍而且每次换参数都能重新跑结果有据可查。这份源码包里的“选股.py”就是这个思路的落地实现获取股票列表、拉取财务报表、计算指标、按条件筛选、输出结果。它不保证预测涨跌而是把“按财务门槛筛出值得进一步研究的股票池”这件事自动化。适合有Python基础、想把自己的选股规则写成可复现代码的投资者也适合做量化入门练习时作为第一个选股框架来用。2. tushare数据获取与财报字段的前置处理2.1 先分清两代tushare接口get_stock_basics与pro_api项目里用的tushare有两代接口代码风格和字段命名差别很大。老版接口直接import tushare as ts几个常用函数返回的是pandas DataFrame字段名接近驼峰或直接就是下划线风格比如total_share、total_assets新版接口需要到tushare官网注册token调用ts.pro_api()字段统一为下划线风格更规范但需要积分门槛。项目标题里的“tushar”是大家比较常见的口语化记法库名以官网为准导入时统一写tushare。ts.get_stock_basics()返回全市场A股的基础快照index是6位股票代码列包含股票名称、行业、地区、总股本、流通股本、每股收益、每股净资产、PE、PB等。它是调用当天数据源维护的静态快照不是实时行情。ts.get_report_data(code, period)则按股票代码和报告期返回财务报表数据包含净利润、总资产、总负债、营业收入等核心字段。理解这两者差异才能判断后面哪些字段能直接用哪些要单独拉取。2.2 全市场取数的标准姿势循环取数加限流get_report_data没有一次取全市场的重载方法常见做法是先拿股票列表再逐只取财报最后拼接。参考实现import tushare as ts import pandas as pd import time # 获取全市场股票基础信息index 是股票代码 basics ts.get_stock_basics() basics basics.reset_index().rename(columns{index: code}) # 指定报告期例如年报 report_dt 2023-12-31 chunks [] for code in basics[code].tolist(): try: rpt ts.get_report_data(codecode, periodreport_dt) if rpt is not None and not rpt.empty: chunks.append(rpt) except Exception as e: # 单只失败不中断整体记录后继续便于事后补取 print(ffetch fail: {code}, {e}) # 限流缓冲跑全市场时防止被数据源拉黑 time.sleep(0.1) report_df pd.concat(chunks, ignore_indexTrue)这段代码是先reset_index再rename成code列因为老接口返回的DataFrame索引本身就是股票代码不reset出来后面和行情数据merge就没有公共字段。time.sleep(0.1)是给数据源留出限流缓冲5000多只股票全量跑一遍要近十分钟可以把sleep改成按失败次数指数退避比如连续失败三次sleep加到0.5秒。catch到的单只失败不会中断整体流程打印code方便单独补取。get_report_data有两个常用参数code传6位股票代码period传报告期格式为YYYY-MM-DD。它还有一个可选参数type常见用法是typep取年报数据这样可以避开一季度、半年报、三季报的重复计算选股时统一用同一报告期横向对比。2.3 财报字段里最常用的几个老版get_report_data返回的字段很多选股时最常用的是这几个字段含义在选股中的用途net_profit净利润计算EPS、ROE的分子total_assets总资产计算净资产判断公司规模total_liabilities总负债净资产 总资产 - 总负债revenue营业收入可加营收增速或营收规模条件eps每股收益与自行计算的EPS核对口径有一点需要特别注意老接口返回的eps是报告期的每股收益它只代表该报告期的累计值。一季报的EPS、半年报的EPS、年报的EPS不在同一个时间口径上横向对比时必须保证所有股票用的是同一个报告期。代码里如果直接用df[eps]去筛选容易混入不同报告期的数据导致结果失真。3. EPS、PE、PB、ROE的计算口径修正与数据合并3.1 源码里的四行公式哪里能直接用哪里有坑源码中的指标计算部分大概长这样df[EPS] df[net_profit] / df[total_shares] df[P/E] df[close] / df[EPS] df[P/B] df[close] / df[total_assets] / df[total_shares] df[ROE] df[net_profit] / (df[total_assets] - df[total_liabilities]) * 4这四行里EPS的计算没问题净利润除以总股本就是每股收益的摊薄口径。PE也没问题收盘价除以每股收益得到静态市盈率注意这是用报告期净利润算的静态PE行情软件默认显示的通常是滚动PE或动态PE所以程序算出的值和软件对不上是正常现象不是代码错了。P/B那一行建议修正。源码写的是close / total_assets / total_shares这算出来是收盘价除以总资产再除以总股本不是市净率。市净率的定义是股价除以每股净资产每股净资产 净资产 / 总股本净资产 总资产 - 总负债。正确写法是close / ((total_assets - total_liabilities) / total_shares)。ROE那行乘4意图大概率是把单季净利润年化。如果net_profit是单季度数据乘4粗略年化还能接受如果net_profit本身已经是累计值比如第三季报的净利润就是前三季度累计再乘4就会夸大ROE四倍。用之前必须确认数据源给的是单季值还是累计值不确定时宁可不乘用同比增长率做辅助判断。另外列名里带/的写法不建议保留。pandas里访问df[P/E]没问题但保存成CSV、或后续用query语法时容易踩坑统一改成pe、pb这种无特殊字符的列名。修正后的版本# 净资产 总资产 - 总负债 df[net_assets] df[total_assets] - df[total_liabilities] # EPS 摊薄口径 df[eps] df[net_profit] / df[total_shares] # 静态市盈率亏损股为负值筛选时单独处理 df[pe] df[close] / df[eps] # 市净率 股价 / 每股净资产 df[pb] df[close] / (df[net_assets] / df[total_shares]) # ROE单位统一为百分比若为单季利润再做年化 df[roe] df[net_profit] / df[net_assets] * 100这段代码先把净资产单独算出来避免后续公式里重复写total_assets - total_liabilities。eps和pe保留负值不要提前丢弃因为亏损股的负PE本身也是信息可以在筛选阶段单独判断。roe统一乘100转成百分比方便和行情软件展示的ROE对比。3.2 行情、财务、股本三张表怎么对齐选股至少要三份数据股票基础信息、财务报表、最近收盘价。这三份数据的对齐靠code这个公共字段做merge。收盘价如果直接用不复权价格遇到除权除息会出现价格断层所以一般取复权价具体获取方式在第5章展开这里先约定close_map是包含code和close两列的数据。# basics: 股票基础信息包含 total_share 总股本 # report_df: 财务报表按报告期逐只拉取后拼接 # close_map: 最近交易日复权收盘价code close merged pd.merge( basics[[code, name, total_share]], report_df[[code, net_profit, total_assets, total_liabilities]], oncode, howleft ) merged pd.merge(merged, close_map, oncode, howleft)merge时统一用howleft以股票基础信息为主表。这样财务数据或行情数据缺失的股票会保留为NaN不会因为某只股票财报缺失导致整行记录消失筛选时再用dropna处理。3.3 指标体系表哪些指标必须自算哪些可以直接信任老接口get_stock_basics里本身有pe、pb、eps这些字段但不建议直接用。原因是这些字段来自数据源快照报告期口径不透明可能是按上一份财报算的也可能是按TTM算的混在一起做横向对比容易出错。自己用统一的报告期和公式重算一遍虽然代码多几行但保证全市场口径一致。指标建议来源说明EPS自算net_profit / total_share摊薄口径PE自算close / EPS静态市盈率PB自算close / (net_assets / total_share)ROE自算net_profit / net_assets注意单季与累计行业、名称get_stock_basics用于剔除ST、按行业分组4. A股财务指标阈值筛选与组合打分排序4.1 条件组合把“好公司”写进布尔索引筛选逻辑是全市场跑一遍把不合条件的股票过滤掉。常见的入门条件组合是市盈率大于0且低于30ROE大于10%剔除ST股。写成代码# 剔除财务数据缺失的股票 merged merged.dropna(subset[pe, roe, close]) # PE 大于 0 是排除亏损股PE 小于等于 30 是估值门槛 cond_pe (merged[pe] 0) (merged[pe] 30) # ROE 大于 10单位是百分比 cond_roe merged[roe] 10 # ST 股有退市风险直接剔除使用 naFalse 防止名称缺失时报错 cond_st ~merged[name].str.contains(ST, naFalse) selected merged[cond_pe cond_roe cond_st].copy()这里单独把cond_pe拆成两个条件是因为PE为负的亏损股没有估值含义直接设PE 0比后续再用dropna处理负值更干净。ST股用名称字段做正则匹配naFalse确保name字段为空时不抛异常。如果想把次新股也剔除get_stock_basics里有上市日期字段转成datetime后按上市满一年过滤即可。4.2 打分排序避免硬性阈值的边界问题单靠阈值筛选有两个问题一是刚好卡在边界上的股票比如ROE为9.9%和10.1%的两只基本面差别不大但一个入选一个落选二是筛选出的股票没有优先级几十只股票不知道先研究哪只。常见做法是在阈值筛选之后加一步打分排序。# ROE 越高越好rank(pctTrue) 转成 0~1 分位排名 selected[roe_score] selected[roe].rank(pctTrue) # PE 越低越好用 1 - 分位排名让低 PE 得高分 selected[pe_score] 1 - selected[pe].rank(pctTrue) # 综合得分可调权重例如更看重盈利质量就加大 roe_score 权重 selected[total_score] selected[roe_score] * 1.0 selected[pe_score] * 1.0 selected selected.sort_values(total_score, ascendingFalse) selected.head(50).to_csv( fselected_{report_dt}_top50.csv, indexFalse, encodingutf-8-sig )为什么用分位排名而不是Z-score财务数据极值很多一只PE为200倍的股票会把均值拉得很高Z-score会整体扭曲rank(pctTrue)只关心相对位置对极值不敏感打出来的分数稳定。输出CSV时用utf-8-sig编码Excel打开才不会乱码这是Windows环境下做工最保险的姿势。4.3 参数调节先宽口径跑通再收紧条件筛选条件不是越严越好建议分两步走。第一步用宽口径跑通全流程比如PE小于50、ROE大于5把程序链路验证通过第二步再收紧到PE小于30、ROE大于10对比两批结果观察新增了哪些股票再判断是条件问题还是数据口径问题。参数默认值调高效果注意点PE上限30候选池变大纳入更多低估标的容易混入周期股结合行业看ROE下限10候选池变小盈利质量更严高ROE可能来自高杠杆可加负债率约束打分权重1:1权重偏向某因子可用总负债/总资产 0.6 做杠杆过滤一个值得养成的习惯跑结果之前先检查数据区间。收盘价用的日期是哪天、报告期是哪个季度这两个条件在最终结果里看不到但决定了结果是否可用。把报告期和数据日期写进文件名就是最简单有效的追溯手段。5. 复权因子、报告期对齐与选股结果的边界修正5.1 除权除息下的价格断层选股也要用复权价股票分红送转后股价会除权除息不复权价格会留下一个向下的跳空缺口。比如一只100元的股票10派5元除息后开盘价直接跳成95元左右但公司基本面没有实质变化。如果用不复权价算PE和PB会得出“突然变便宜了”的错误结论。选股和回测一样必须用复权价。tushare获取复权价有两种常见方式。老版接口ts.get_k_data(code, autypeqfq)直接返回前复权K线取最后一行的close就是复权后收盘价。新版pro接口用复权因子自己折算pro ts.pro_api() # 拉最近一段交易日的行情和复权因子 df pro.daily(ts_code000001.SZ, start_date20240101, end_date20240430) adj pro.adj_factor(ts_code000001.SZ, start_date20240101, end_date20240430) # 合并后用最新一期的复权因子折算前复权价 m pd.merge(df, adj, on[ts_code, trade_date]) m[close_qfq] m[close] * m[adj_factor] / m[adj_factor].iloc[-1]前复权的思路是以最新价格为基准把历史价格按因子比例折算保证最近收盘价就是真实成交价同时让历史价格同步调整。选股时取close_qfq最后一行的值作为当前收盘价这样计算出的PE和PB不会因为除权除息产生虚假波动。5.2 报告期对齐小心“未来数据”混进选股财报发布有滞后性一季报4月底披露完半年报8月底披露完三季报10月底披露完年报次年4月底披露完。如果在2024年9月选股最新完整披露的是2024年半年报而不是2024年三季报。代码里如果直接选用当前日期的报告期很可能用到还没发布的财务数据这在量化里属于未来函数实盘上会亏损。常见做法是在参数里固定报告期比如每年4月底之后选股用上年年报8月底之后用半年报避免动态取数带来的逻辑混乱。get_report_data的period参数就是用来干这件事的把它放在配置文件的顶部每次选股前先确认。5.3 边界验证手工抽查跑出来的股票自动筛选结果出来后最重要的验证是手工抽查。从selected结果里挑3到5只股票打开行情软件对照两个数字PE是否一致、ROE是否和财报披露的净资产收益率在同一量级。如果行情软件显示的是滚动PE程序输出的是静态PE两者数值不同是正常的数量级应该接近。如果差得离谱先查两个点一是收盘价有没有复权二是报告期是不是选对了。这个动作每次调整完参数都做一遍比写一百行单元测试都管用。最后还有一个细节值得完善输出结果里把report_dt、数据日期、筛选条件版本号列进去比如文件名写成selected_report_20231231_close_20240915_pe30_roe10.csv。这样过两周再回看还能清楚知道这份结果是在什么数据口径下算出来的不会对着历史文件犯迷糊。本文还有配套的精品资源点击获取