【Python量化实战 #13】想有自己的选股器?从零搭一套 Python 多条件筛选+回测系统

📅 2026/8/3 16:37:47
【Python量化实战 #13】想有自己的选股器?从零搭一套 Python 多条件筛选+回测系统
连载系列「Python量化实战」第 13 篇模块六 · 量化策略实战。上一篇《Python多因子选股模型搭建》用打分排序解决谁更好本篇换一种更直观的思路——多条件筛选解决谁合格并把入选组合放回历史里验证效果。搭建一个 Python 量化选股系统核心就四步批量拉取股票池数据 → 按多个条件逐层筛选 → 对入选组合做历史回测 → 结果导出复盘。多数教程只讲到筛出名单就结束了但名单不回测等于没有闭环——你不知道这套条件历史上到底管不管用。本文用 mairui SDK 把四步完整走通条件写成可插拔的规则列表筛选过程打印漏斗入选组合与全池基准对比净值全部代码可直接运行。本文你将得到什么一套可插拔的选股系统规则列表式多条件筛选漏斗可视化 入选组合 vs 全池基准回测 CSV 导出复盘真实运行结果20 只池筛出 3 只入选组合 21.65% vs 全池 -4.79%及回测意义是证伪的讨论缺失值、前复权、幸存者偏差等 5 个避坑点及改配置不改代码的进阶方向一、在线体验想先看看筛选用的行情快照接口返回什么字段打开 API Playground 在线调用即可https://mairuiapi.com/playground建议先在 Playground 里测试实时交易数据接口能直接看到pe市盈率、sjl市净率、zdf6060日涨跌幅等字段——这些就是下文筛选条件的数据来源。二、环境准备本文代码使用 mairui SDK 获取股票数据安装方法如下pipinstallmairuiSDK 的完整接口文档与使用说明请查阅 GitHub 仓库https://github.com/MaiRuiApi/mairui接口的详细参数说明请查阅官网 API 文档https://mairuiapi.com/hsdata运行环境Python 3.9mairui SDK 1.0.0pandas 2.2 / numpyimportosimportmairui# 证书从环境变量读取注册后可在官网获取apimairui.Client(os.environ[MAIRUI_LICENCE])本文数据截至 2026-07-28行情为实时快照回测区间 2025-07-28 至 2026-07-27读者复现时数值会随行情更新而变化。三、第一步批量拉取股票池数据选股系统的地基是一张股票池指标快照表。本文用 20 只跨行业沪深大盘股做演示池固定池便于复现实际使用时换成指数成分股或全市场列表即可代码结构不变。每只股票取 5 个筛选用指标分别来自两个接口stock_ssjy实时快照提供现价、PE、PB、60日涨跌幅stock_pershareindex历季财务指标提供 ROE。importtimeimportnumpyasnpimportpandasaspd# 示例股票池沪深大盘股样本跨行业实际可换成指数成分股/全市场列表STOCK_POOL{600519:贵州茅台,000858:五粮液,601318:中国平安,600036:招商银行,601166:兴业银行,000333:美的集团,600900:长江电力,601088:中国神华,600030:中信证券,000651:格力电器,600276:恒瑞医药,300750:宁德时代,601899:紫金矿业,600887:伊利股份,601668:中国建筑,002594:比亚迪,601012:隆基绿能,600809:山西汾酒,000002:万科A,601398:工商银行,}defto_num(value):接口缺失值以 - 返回统一转成 NaN。try:returnfloat(value)except(TypeError,ValueError):returnnp.nandeffetch_stock_snapshot(api,code,name):取单只股票的筛选用指标。snapapi.stock_ssjy(code)# 实时交易快照snapsnap[0]ifisinstance(snap,list)elsesnap fin_rowsapi.stock_pershareindex(code)# 历季财务指标[-1] 为最新一期finfin_rows[-1]iffin_rowselse{}return{code:code,name:name,price:to_num(snap.get(p)),pe:to_num(snap.get(pe)),# 市盈率动pb:to_num(snap.get(sjl)),# 市净率momentum_60d:to_num(snap.get(zdf60)),# 60日涨跌幅(%)roe:to_num(fin.get(jzcsyl)),# 净资产收益率(%)}defbuild_universe(api):records[]forcode,nameinSTOCK_POOL.items():records.append(fetch_stock_snapshot(api,code,name))time.sleep(0.2)# 控制请求频率returnpd.DataFrame(records)关键解释财务接口的缺失值以字符串-返回to_num统一转成NaN后续筛选时NaN会被当作不通过处理避免脏数据混进名单。四、第二步多条件筛选组合器选股系统最怕把条件写死在一堆if里——每次调整策略都要改逻辑代码。更好的做法是把条件抽象成(名称, 判断函数)的规则列表增删条件只改配置# 条件写成 (名称, 判断函数) 列表增删条件只需改这里SCREEN_RULES[(PE介于0~30,lambdadf:(df[pe]0)(df[pe]30)),(PB低于5,lambdadf:df[pb]5),(ROE不低于10%,lambdadf:df[roe]10),]defrun_screener(universe_df,rules):逐条应用筛选规则打印每一步剩余数量筛选漏斗。maskpd.Series(True,indexuniverse_df.index)print(f初始股票池:{len(universe_df)}只)forrule_name,rule_fninrules:maskrule_fn(universe_df).fillna(False)print(f通过[{rule_name}]后剩余:{int(mask.sum())}只)returnuniverse_df[mask].reset_index(dropTrue)真实运行的筛选漏斗输出如下2026-07-28 实时数据 筛选漏斗 初始股票池: 20 只 通过[PE介于0~30]后剩余: 16 只 通过[PB低于5]后剩余: 15 只 通过[ROE不低于10%]后剩余: 3 只 入选名单 code name price pe pb roe momentum_60d 300750 宁德时代 398.37 21.29 4.86 12.08 -10.48 601899 紫金矿业 31.47 10.42 4.42 10.40 -6.78 600809 山西汾酒 122.80 6.96 4.05 12.72 -14.21关键解释漏斗式打印能直观看到每个条件杀掉了多少股票——如果某个条件一下子筛掉 90%通常说明阈值过严或数据口径有问题这是调试选股条件最实用的手段。PE 0同时排除了亏损股亏损股 PE 为负不具可比性。五、第三步入选组合历史回测名单筛出来只是一半另一半是回答这套条件历史上表现如何。做法拉取全池近一年前复权日 K 线入选组合等权持有与全池等权基准对比净值。deffetch_close_series(api,code,start,end):拉前复权日K返回以日期为索引的收盘价序列。rowsapi.stock_history(code,d,f,ststart,etend)# d日线 f前复权kline_dfpd.DataFrame(rows)kline_df[t]pd.to_datetime(kline_df[t])returnkline_df.set_index(t)[c].astype(float).sort_index()defbacktest_portfolio(api,picked,benchmark_pool,start,end):等权持有入选组合与全池等权基准对比教学用不含手续费/调仓。closes{}forcodeinbenchmark_pool:closes[code]fetch_close_series(api,code,start,end)time.sleep(0.2)close_dfpd.DataFrame(closes).dropna()ret_dfclose_df.pct_change().fillna(0)# 日收益率picked_codes[cforcinpicked[code]ifcinclose_df.columns]navpd.DataFrame({入选组合:(1ret_df[picked_codes].mean(axis1)).cumprod(),全池基准:(1ret_df.mean(axis1)).cumprod(),})returnnav真实回测输出回测区间 2025-07-28 至 2026-07-27共 242 个交易日 回测结果2025-07-28 ~ 2026-07-27, 共242个交易日 入选组合累计收益: 21.65% | 全池等权基准: -4.79%关键解释stock_history的st/et参数只认YYYYMMDD格式第三个参数f表示前复权回测必须用复权价否则除权缺口会被当成暴跌。这次筛选条件在过去一年跑赢了全池基准约 26 个百分点但务必清醒单次、单区间、小样本池的回测结果不代表条件本身长期有效换一个区间结论完全可能反转——回测的意义是证伪明显糟糕的条件而不是证明未来能赚钱。六、完整实战示例把四步串成完整可运行脚本含结果导出importosimportmairui apimairui.Client(os.environ[MAIRUI_LICENCE])# 1) 批量取数universe_dfbuild_universe(api)# 2) 多条件筛选picked_dfrun_screener(universe_df,SCREEN_RULES)print(picked_df[[code,name,price,pe,pb,roe]].round(2))# 3) 历史回测入选组合 vs 全池等权nav_dfbacktest_portfolio(api,picked_df,STOCK_POOL,20250728,20260727)total(nav_df.iloc[-1]-1)*100print(f入选组合:{total[入选组合]:.2f}% | 全池基准:{total[全池基准]:.2f}%)# 4) 结果导出便于 Excel 复盘universe_df.to_csv(universe_snapshot.csv,indexFalse,encodingutf-8-sig)picked_df.to_csv(picked_stocks.csv,indexFalse,encodingutf-8-sig)nav_df.to_csv(backtest_nav.csv,encodingutf-8-sig)导出三张表universe_snapshot.csv全池指标快照、picked_stocks.csv入选名单、backtest_nav.csv净值曲线可直接在 Excel 里画图。CSV 用utf-8-sig编码Excel 打开不乱码。七、避坑与进阶缺失值必须显式处理筛选条件里NaN参与比较结果是False看似自动排除但mask rule_fn(df)时若不fillna(False)NaN会在取反、组合等场景产生意外结果务必显式处理。回测必须用前复权价stock_history第三个参数传f。用不复权价回测分红除权日会被算成大跌结论完全失真。警惕幸存者偏差与过拟合固定池演示没有包含已退市股票条件调到历史收益最好看就是过拟合。实用做法是把条件的经济含义讲清楚低估值高质量而非无限调参。频率控制批量拉取时加time.sleep(0.2)20 只以内很快全市场级别的批量拉取与并发方案本系列工程化篇#16会专门展开。进阶方向把SCREEN_RULES改成从 JSON/YAML 配置文件加载即可做成改配置不改代码的选股服务再配合定时任务每日收盘后自动跑一遍并推送名单。八、总结与延伸本文从零搭建了一个可扩展的 Python 量化选股系统stock_ssjystock_pershareindex批量取数、规则列表式多条件筛选漏斗可视化、入选组合 vs 全池基准的历史回测、CSV 导出复盘。系统的价值不在筛出了哪 3 只而在这套可插拔、可回测、可复盘的结构——条件随你换验证框架不变。下一篇预告【Python量化实战 #14】将接入专业回测框架 backtrader跑通第一个带手续费、夏普比率与最大回撤分析的策略回测。延伸阅读在线体验更多接口https://mairuiapi.com/playground查看完整 API 文档https://mairuiapi.com/hsdataSDK 文档与源码https://github.com/MaiRuiApi/mairui关注公众号获取更多技术分享本文为技术演示文中股票仅为代码示例样本不构成投资建议。市场有风险投资需谨慎。