基于TuShare的A股自动选股:从数据拉取到因子打分
简介Python-基于TuShare的A股自动选股程序是一份面向量化投资入门者的实战源码围绕数据驱动选股这一场景演示了如何利用TuShare接口获取A股历史行情与财务数据进而完成数据清洗、指标计算、策略筛选与回测分析。项目代码组织清晰既可作为课程设计参考也能帮助开发者快速搭建自己的选股框架。资源以zip压缩包形式提供共包含25个文件其中17个Python脚本为主体覆盖数据抓取、策略实现、工作流调度等环节另附图片、配置说明与Markdown文档便于理解项目结构与运行方式。压缩包整体仅1.95MB轻量易部署。目前已有6021人学习下载受到不少量化初学者的关注。通过阅读源码可以掌握TuShare常见接口如历史行情、实时报价的用法学习移动平均线、RSI、MACD等技术指标在自动选股中的应用并借鉴其回测与策略评估思路为后续独立开发量化策略打下基础。1. 把“拍脑袋选股”变成“下班跑一遍脚本”TuShare 自动选股是什么做 A 股量化或者半量化的人早晚会遇到同一个尴尬复盘时想按 PE、ROE、换手率筛一遍全市场打开行情软件一个个翻翻到凌晨也翻不完。某开发者最早写这套基于 TuShare 的 A 股自动选股程序就是因为某天想筛“市值 100 亿到 300 亿、ROE 连续三年大于 12%、最近 20 天涨幅不超过 15%”的票手工操作到夜里一点半才筛完还漏了两只。后来他把整套流程拆成了三个模块数据拉取、因子计算、结果落盘用 TuShare Pro 接口把全市场日线、财务指标、每日指标拉下来再按照自己设定的规则批量打分排序半小时跑完一次全市场扫描。这套程序适合两类人一类是有 Python 基础、想把自己选股规则固化成代码的散户另一类是做策略研究的初级量化从业者拿它当数据管道和因子计算的脚手架。它解决的问题是“规则明确但重复繁琐的筛选工作”不解决“策略一定会赚钱”——这一点在拆解之前先摆出来免得后面白忙一场。2. TuShare 环境搭建与数据准备初始化 Token 到拉取全市场行情2.1 为什么选 TuShare Pro 而不是免费行情接口市面上能拿 A 股数据的 Python 库有好几个但 TuShare Pro 在“免费额度 数据字段完整度”上比较均衡。免费行情接口常见的问题是行情数据只有不复权或前复权财务数据更新慢每日指标字段缺失最麻烦的是访问频率限制不透明——某开发者之前用一个免费接口单日请求上限 500 次跑一次全市场日线就用掉了一半额度第二天直接卡死。TuShare Pro 注册后送积分基础积分就能调 daily、daily_basic、fina_indicator 这类核心接口虽然每分钟调用次数有限制但按“单次拉全市场本地缓存”的思路设计程序不会撞上限。另外TuShare 的数据字段命名规范字段名和国内券商研报里的习惯叫法基本一致比如 pe_ttm、pb、total_mv、turnover_rate写策略的时候不太需要来回翻译。选择 TuShare 还有一个隐性理由它的数据是按交易日更新的对“自动选股”这种定期跑批的需求来说数据新鲜度够用。盘中实时行情它不完全擅长但选股程序本来就是收盘后跑——用收盘数据筛规则第二天开盘再看结果这个节奏正好匹配。如果你做的是盘中高频交易那 TuShare 不合适那是另一套实时数据源和硬件方案的范畴别拿选股程序去干这事。2.2 注册、安装与 Token 初始化搞定第一道门槛TuShare 的使用流程分三步注册账号、拿到 Token、在代码里初始化。注册在 TuShare 官网完成注册后进入个人主页找到“接口TOKEN”一栏复制那一串 Token。安装库直接 pip 装pip install tushare装完之后写一个初始化模块供整个项目复用。某开发者的习惯是单独建一个ts_init.py把 token 配置、pro 对象创建、数据拉取封装都放进去避免每个脚本里重复写 token。import tushare as ts import pandas as pd # 这里填你自己注册得到的 token 字符串 TOKEN 你的token字符串 ts.set_token(TOKEN) pro ts.pro_api() def get_pro(): 返回已初始化的 pro 接口对象 return pro # 测试连接拉取最近一个交易日的全市场每日指标 def test_connection(): df pro.daily_basic(trade_date20240115, fieldsts_code,pe_ttm,pb,total_mv) print(df.head()) return df if __name__ __main__: test_connection()这段代码做了三件事设置全局 Token、创建 pro 接口对象、用一个读接口验证连通性。需要注意一点ts.set_token()是设置本地缓存 tokents.pro_api()是创建接口对象两者缺一不可——只 set_token 不 pro_api调用数据接口时会因为实例未初始化而抛异常。第一次跑测试连接时如果返回空 DataFrame 或者报错“抱歉您没有访问该接口的权限”多半是积分不够需要去 TuShare 官网看积分说明或者把积分提升到对应接口要求档位。日常使用中daily_basic这个接口的基础积分就能用但高频调用会被限流下面会细说。2.3 拉取全市场股票列表与交易日历先把“范围”定下来自动选股的第一步不是选股是确定“候选池”。A 股市场有沪深主板、创业板、科创板、北交所不同板块的上市条件、涨跌幅限制都不一样。选股程序默认应该先拉一份全量股票列表然后按自己的规则过滤而不是把范围写死在某几个代码里。TuShare 的stock_basic接口干这个事def fetch_stock_basic(): pro get_pro() # 获取全部上市公司基础信息 df pro.stock_basic( exchange, # 空字符串表示所有交易所 list_statusL, # L 上市状态D 退市P 暂停 fieldsts_code,name,industry,market,list_date ) return df # 示例只看创业板和科创板且上市时间超过一年 df fetch_stock_basic() df df[df[market].isin([创业板, 科创板])] df[list_date] pd.to_datetime(df[list_date]) df df[df[list_date] 2023-01-01] print(df.shape)这里有两个关键参数list_statusL只保留当前上市的公司避免把退市股拉进来干扰数据market字段用来区分板块。某开发者的经验是新股前半年波动大、数据不稳定而且很多因子在次新股上失真比如上市首日无涨跌幅限制导致的异常换手率所以回测和实盘筛选都应该加一个上市时间过滤条件。还有一个容易忽略的点——stock_basic返回的ts_code形如000001.SZ、600000.SH这个格式是后续所有接口的主键不要自己拼接替代否则容易在日期匹配时出现字符串格式不一致的问题。交易日历用trade_cal接口拉它的作用是在定时任务里判断“今天是不是交易日”def get_latest_trade_date(): pro get_pro() # 从 2024-01-01 到今天的交易日历 cal pro.trade_cal( start_date20240101, end_date20241231, is_open1 # 只取开市日期 ) cal[cal_date] pd.to_datetime(cal[cal_date]) latest cal[cal_date].max().strftime(%Y%m%d) return latest # 输出形如 20241220 的字符串直接用于数据接口的 trade_date 参数2.4 批量拉取日线行情与每日指标循环请求与本地缓存候选池确定后核心数据有两份日线行情OHLCV和每日指标PE、PB、换手率、总市值等。TuShare 的daily接口按股票代码获取某段时间的日线daily_basic接口按日期获取全市场每日指标。这里的选型有一个关键权衡按股票一只只拉还是按日期一天天拉某开发者踩过这个坑daily接口支持单只股票的区间查询也支持按 trade_date 查全市场。如果代码写成“遍历所有股票每只拉一年日线”3000 只票就要请求 3000 次但 TuShare 每分钟请求次数限制通常在几百到一千以内很快会撞限流。反过来按交易日一天天拉全市场一年 250 个交易日只要 250 次请求效率高一个数量级。所以正确做法是先确定日期范围再对每个交易日拉一次全市场行情然后本地拼接成逐股的时间序列。def fetch_daily_by_date(trade_date): 按交易日拉取全市场日线行情 pro get_pro() df pro.daily(trade_datetrade_date) return df # 拉取最近 5 个交易日数据按日期循环 trade_dates [20241216, 20241217, 20241218, 20241219, 20241220] all_daily pd.DataFrame() for d in trade_dates: try: day_df fetch_daily_by_date(d) all_daily pd.concat([all_daily, day_df], ignore_indexTrue) print(f{d}: {day_df.shape[0]} 只股票) except Exception as e: print(f{d} 拉取失败: {e}) # 失败先记录不要中断整个循环 continue # 保存到本地 CSV避免重复请求 all_daily.to_csv(./data/daily_data.csv, indexFalse)这段循环里的 try-except 是必须的。实际运行中某一天的请求可能因为网络波动或限流失败如果直接中断循环前面拉的数据全白费。保存 CSV 是本地的第一层缓存——同一个交易日数据在当日不会变化重复跑选股程序时直接读 CSV不用再请求接口。参数说明daily接口如果只传 trade_date返回该日全部上市公司的 OHLCV 数据如果要拿单只股票的历史区间需要传ts_code和start_date、end_date。两种调用方式对应不同场景选股程序用第一种就够。daily_basic的拉取逻辑完全一样也是按 trade_date 拿全市场但它带的是估值和交易类指标def fetch_daily_basic_by_date(trade_date): 按交易日获取全市场每日指标PE/PB/市值/换手率 pro get_pro() df pro.daily_basic( trade_datetrade_date, fieldsts_code,close,turnover_rate,pe_ttm,pb,total_mv ) return df # 合并到主数据表 basic_df fetch_daily_basic_by_date(20241220) daily_df pd.read_csv(./data/daily_data.csv) # 按股票代码合并让每行同时拥有行情和指标字段 merged pd.merge( daily_df[daily_df[trade_date] 20241220], basic_df, onts_code, howleft )合并时用howleft以日线行情为准每日指标缺席的行保留 NaN——有些停牌股或者刚上市的公司可能没有完整指标NaN 后续在因子计算时要专门处理不能直接丢弃。total_mv的单位是万元如果要换算成亿元记得除以 10000这个单位坑很多新手栽过。3. 设计选股规则与因子打分从“条件过滤”到“排序打分”3.1 把选股思路翻译成因子表达式条件过滤的写法与边界拿到数据之后最直接的做法是“条件过滤”——把选股规则写成布尔表达式筛出同时满足所有条件的股票。这是选股程序的第一层形态也是容易实现、容易验证的一版。举个例子某开发者的过滤规则是“总市值在 50 亿到 300 亿之间PE-TTM 在 0 到 30 之间最近 5 日涨幅不超过 10%”。def apply_filters(df): 输入合并后的行情 指标 DataFrame 输出通过全部过滤条件的股票列表 df df.copy() # 单位换算total_mv 从万元转亿元 df[total_mv_yi] df[total_mv] / 10000 # 条件 1市值区间 cond_mv (df[total_mv_yi] 50) (df[total_mv_yi] 300) # 条件 2PE-TTM 为正且不太高 cond_pe (df[pe_ttm] 0) (df[pe_ttm] 30) # 条件 3近 5 日涨幅过滤交易日历取最近 5 个交易日 # 这里简化为用收盘价相对 5 日前收盘价的涨幅 df df.sort_values([ts_code, trade_date]).groupby(ts_code).apply( lambda x: x.assign( chg_5d(x[close] / x[close].shift(5) - 1) * 100 ) ).reset_index(dropTrue) cond_chg (df[chg_5d] 10) # 合并条件 result df[cond_mv cond_pe cond_chg] return result[[ts_code, name, close, total_mv_yi, pe_ttm, chg_5d]]这段代码有三个细节值得注意。第一groupby(ts_code).apply里面计算chg_5d时shfit(5) 用的 5 是“5 个交易日”不是自然日——这在 A 股很重要因为中间可能有周末和节假日。第二市值条件写在数据准备阶段之外单独抽象成函数方便以后调整边界值——50 亿到 300 亿只是一个示例阈值你不一定认同改参数就能换成自己的规则。第三条件里pe_ttm 0这一条看着多余其实是为了过滤“亏损企业 PE 为负”的情况——很多新手直接把 PE 小于 30 写成pe_ttm 30结果把 PE 为负的公司也筛进来了真的是血泪经验。3.2 打分模型把多个因子加权求和输出可排序的分数条件过滤的问题是“一刀切”市值为 51 亿的股票和 299 亿的股票被同等对待PE 为 5 和 PE 为 29 也没有区别。当规则超过 5 条时过滤后剩下的股票经常只剩个位数或者一个条件筛选不当直接筛空。更实用的做法是打分模型——每个因子给一个分项得分然后按权重加权求和输出一个总分用于排序。这样即使没有股票满足全部条件你也能知道“最接近”的是哪些。def score_stocks(df): 打分模型市值、PE、换手率三个因子加权 每个因子先做分位数归一化再按权重求和 df df.copy() df[total_mv_yi] df[total_mv] / 10000 # 分位数归一化把原始值映射到 0~100 分 df[score_mv] 100 - (df[total_mv_yi].rank(pctTrue) * 100) # 市值越小分越高逆序 df[score_pe] 100 - (df[pe_ttm].rank(pctTrue) * 100) # PE 越低分越高 df[score_turnover] df[turnover_rate].rank(pctTrue) * 100 # 换手率越高分越高这个因子因策略而异 # 权重分配市值 0.4PE 0.4换手率 0.2 df[total_score] ( df[score_mv] * 0.4 df[score_pe] * 0.4 df[score_turnover] * 0.2 ) # 过滤掉 PE 为负的股票避免负值在 rank 中产生扭曲 df df[df[pe_ttm] 0] # 按分数降序排列 result df.sort_values(total_score, ascendingFalse) return result[[ts_code, name, close, total_mv_yi, pe_ttm, turnover_rate, total_score]].head(50)打分模型的核心是“分位数归一化”这个思路。市值、PE、换手率三个因子的量纲完全不同——市值是亿元级别PE 是几倍到几十倍换手率是百分比——把它们直接相加没有意义。rank(pctTrue)把每个因子映射到 0 到 1 的相对位置乘 100 变成百分制这样三个因子就“可加”了。权重的设置则是主观的部分你更看重估值就加大 PE 权重更看重流动性就加大换手率权重。某开发者常用的做法是先用等权重跑一遍看结果里股票排名的分布再根据实际输出调整权重——比如发现 PE 因子打出来全是银行股就把 PE 权重调低发现市值因子让小盘股扎堆也调低权重。这套调参流程其实就是半量化最常见的“因子权重寻优”的简化版。参数说明这里的head(50)是输出前 50 名这个数不是固定的如果只想要 10 只候选就改成 10。rank(pctTrue)遇到重复值会取平均排名不会报错。还有一点打分前必须先过滤 PE 为负、市值为 0 的异常行因为异常值会把分位数分布整体拉偏——一只 PE 为 500 倍的股票看起来是极端值其实它只影响 0~100 的映射但如果 PE 为负的不先剔除rank会把负 PE 排在前面打分结果直接翻车。3.3 因子的时效性用最新交易日还是一个区间选股程序最常被质疑的一点是“数据滞后”。如果只拿最新一个交易日的daily_basic那 PE、PB 都是当天的快照反映的是“此刻的状态”但选股有时候要看“趋势”比如 ROE 连续三期改善、营收增速连续两季回升这类因子必须拉多期财务数据才能算。TuShare 的fina_indicator接口就是干这个的def fetch_fina_indicator(ts_code, period2024): 获取单只股票的财务指标 period 参数传报告期如 2024 表示 2024 年三季报 pro get_pro() df pro.fina_indicator( ts_codets_code, start_date20240101, end_date20241231, fieldsts_code,end_date,roe,netprofit_yoy,or_yoy ) return df # 示例拉取某只股票的财务指标 # 注意这里是按股票循环控制频率 df_fin fetch_fina_indicator(000001.SZ) # 检查 ROE 是否连续两个报告期持续为正 df_fin df_fin.sort_values(end_date) roe_positive_count (df_fin[roe] 0).sum()财务指标的接口和行情接口最大的不同行情按日期全市场拉一次就行财务指标必须按股票循环。因为财务数据是“每家公司一个报告期一条记录”不存在“某一天全市场财报统一更新”的情况。这就带来一个实际问题全市场 3000 只股票每只拉一次fina_indicator就是 3000 次请求流量控制是个大问题。某开发者的折中方案是先用行情和每日指标打分选出前 50 名再对这 50 只逐一拉财务指标做二次精选——把“全市场全因子扫描”变成“粗筛 精排”两段式请求量从 3000 降到 50完全够用。netprofit_yoy净利润同比增长率和or_yoy营业收入同比增长率这两个字段在fina_indicator里可以直接用不需要自己用利润表计算增长。这省了很多事但也埋了一个坑这些字段是 TuShare 已经算好的不同报告期之间的可比性依赖于它的口径如果发现数据明显异常比如某公司营收增速突然 1000%大概率是报告期调整或者重组要回到原始财报确认不要直接采信。4. 避坑指南TuShare 自动选股最容易翻车的五个细节4.1 Token 初始化失败set_token 和 pro_api 的调用顺序现象代码运行到pro.daily()直接抛异常提示Authentication failed或token 不存在。 原因ts.set_token()和ts.pro_api()是两步——set_token只是把 token 存到本地配置文件pro_api()才是创建带 token 的接口对象。如果只set_token不pro_api()或者pro_api()在set_token之前调用接口对象没有拿到认证信息。 解决确保每次运行脚本时先set_token再pro_api()并且初始化代码放在模块顶部不要放在函数内部反复执行。某开发者习惯把这段初始化逻辑放进ts_init.py其他脚本统一import彻底避免忘了初始化。4.2 行情数据拉下来但合并后大量 NaN停牌股与新股干扰现象merge之后发现相当多股票的close为 NaN或者daily_basic里部分行缺失pe_ttm。 原因A 股存在停牌股——停牌期间无日线记录但每日指标可能有缓存值另外次新股上市前五天不设涨跌幅部分指标接口在上市初期数据不完整。 解决合并之后加一步清理逻辑——dropna(subset[close])先去除无行情记录的行再对pe_ttm和total_mv做阈值过滤而不是直接删除比如total_mv为 NaN 说明当天没更新可以保留但不算入打分。不要一来就dropna()全删可能把刚上市但基本面很好的次新股一并删掉。4.3 按股票循环拉数据撞上限流一分钟内请求次数超限现象循环到第 200 只股票时接口开始报错抱歉您每分钟最多访问该接口X次。 原因TuShare Pro 对每个接口有独立的每分钟请求上限不同积分档位不同。在“按股票循环拉财务指标”的场景里很容易无意识地把请求数打到上限。 解决两种方案。一是把循环改成按日期拉全市场减少请求次数行情类接口适用二是循环里加time.sleep(0.2)主动降速让每分钟请求数落在限制之内。某开发者的实际经验是即使积分够高也建议把 sleep 加上——TuShare 的限流是滑动窗口短时间突发的 10 请求也可能触发熔断稳定优先级高于速度。4.4 市值单位搞错total_mv 是万元不是亿元现象过滤市值小于 300 亿的股票结果出来的全是贵州茅台、工商银行这种超大盘权重股。 原因daily_basic接口的total_mv字段单位是万元不是亿。300 亿 3,000,000 万元如果拿 300 和 total_mv 直接比较等于在找市值 0.03 亿元的股票——显然不存在。 解决读接口字段时留意官方文档的单位说明total_mv拿到后统一除以 10000 得到亿元再参与计算。代码里把它改名为total_mv_yi后续所有引用这个新字段避免每次都要心里换算。4.5 财务指标字段的“报告期”陷阱end_date 不是发布日期现象用fina_indicator拉某股票的 ROE发现最新数据停留在半年前。 原因fina_indicator里end_date是报告期截止日不是财报发布日期。A 股一年报披露截止到次年 4 月 30 日半年报截止到 8 月 31 日所以 3 月份看年度报告还没出全最晚只能取到三季报数据这是正常的。 解决在选股程序里不要把“最新报告期”和“当前日期”混为一谈。处理方式是动态获取最近一个已经完整披露的财报期——写死的话季度切换后会失效。一个简单做法是遍历最近 3 个季度的end_date取日期上不超过当前且数据完整度最高的那个。另外财报发布后还可能修订真要精确到“实际可用”状态需要用 TuShare 的公告或披露时间字段再二次校准普通选股场景取最近报告期就够。5. 把选股程序变成每天自动跑的工具定时执行与结果验证5.1 日程化执行用脚本判断交易日并自动拉数选股程序不能“手动点一下跑一次”那和用行情软件手工筛选没本质区别。第一步是让脚本自己能判断“今天该不该跑”并且自动把数据更新到最新。这里用到前面构建的交易日历逻辑import datetime def should_run_today(): 判断今天是否为交易日以及是否已经收盘 today datetime.datetime.now() # 简单判断周末不跑 if today.weekday() 5: return False # 用 TuShare 交易日历做二次确认跳过节假日 cal pro.trade_cal( start_datetoday.strftime(%Y%m%d), end_datetoday.strftime(%Y%m%d), is_open1 ) if cal.empty: return False # 时间判断默认 18:00 后跑确保当日数据已经更新完毕 if today.hour 18: print(今日数据可能未更新完建议 18:00 后运行) return False return True这个should_run_today()里的两个判断是经验值TuShare 的日线数据通常晚上才更新完毕下午 5 点前拉经常拿到不完整的数据早上 9 点前拉前一天的数据可能还没入库。某开发者踩过“早上 8 点跑脚本拿到空数据”的坑之后把执行时间硬性定为交易日 18:30——这个时间点当天所有数据肯定入库了而且不耽误晚上复盘。代码里时间判断部分可以按你自己的作息改但“尽量晚跑”这个原则别丢。5.2 输出结果加时间戳方便对比哪一天的选股结果每次运行选股程序后结果应该写入带日期标识的文件而不是覆盖同一个文件。这个习惯能让后续“回看某天的选股结果到底准不准”成为可能——只有保留历史结果你才能评估自己的选股规则是不是真的有效。import os def save_result(df, output_dir./output): 保存选股结果文件名带日期戳 os.makedirs(output_dir, exist_okTrue) today datetime.datetime.now().strftime(%Y%m%d) file_path os.path.join(output_dir, fstock_picks_{today}.csv) df.to_csv(file_path, indexFalse, encodingutf-8-sig) print(f结果已保存: {file_path}) # 保存时加一列生成时间方便追溯 result_df[generate_time] datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) save_result(result_df)这里用了utf-8-sig编码而不是默认的utf-8是为了让 Excel 直接打开 CSV 不乱码——Windows 下的 Excel 默认用 GBK 解码不加 BOM 头的中文 CSV 打开就是乱码加了utf-8-sig就没这个问题。这个细节对于“把选股结果发给不写代码的搭档看”的场景非常有用属于“自己省事不如让别人省事”的经验。5.3 验证选股结果拿历史数据试算一遍别急着实盘程序跑通之后最忌讳的一件事是第二天直接按选出来的 50 只票买入。某开发者的习惯是先让程序跑两周的“纸面交易”——每天记录选股结果手动记录每只票次日和五日后涨跌幅两周后统计平均收益率。如果连纸面都跑不赢指数说明选股因子本身不行需要调如果纸面有明显超额收益再考虑拿小仓位试水。这个“纸面验证”可以部分自动化用 TuShare 拉选中股票次日的daily数据计算实际涨跌幅把结果追加到验证表里def verify_previous_picks(pick_date, hold_days5): 验证某天选出的股票在 hold_days 交易日后的表现 pick_date: 选股日期字符串 YYYYMMDD # 读取历史选股结果 file_path f./output/stock_picks_{pick_date}.csv if not os.path.exists(file_path): print(f{pick_date} 无选股记录) return picks pd.read_csv(file_path) # 获取 pick_date 之后的行情计算每只股票持有期收益 pro get_pro() results [] for _, row in picks.head(20).iterrows(): # 用 trade_cal 推算出 pick_date 后第 hold_days 个交易日 cal pro.trade_cal( start_datepick_date, end_datedatetime.datetime.now().strftime(%Y%m%d), is_open1 ) if len(cal) hold_days: continue target_date cal.iloc[hold_days][cal_date] # 拉取该股票在目标日的收盘价 daily pro.daily( ts_coderow[ts_code], start_datetarget_date, end_datetarget_date ) if daily.empty: continue close_after daily.iloc[0][close] close_before row[close] ret (close_after / close_before - 1) * 100 results.append({ts_code: row[ts_code], ret_pct: ret}) result_df pd.DataFrame(results) print(f{pick_date} 选股 {len(result_df)} 只平均收益: {result_df[ret_pct].mean():.2f}%) return result_df这个验证函数有个细节用trade_cal而不是直接用pick_date hold_days推目标日是为了自动跳过中间的非交易日——A 股休市日期比较多直接加自然日算出的持仓天数大概率错。hold_days5是“持有 5 个交易日”的意思你可以改成 10 或 20 来观察不同持仓周期下选股策略的表现。5.4 保持结果可解释每次保存因子明细不要只存代码最后一条技巧也是某开发者踩过最大的坑早期版本只保存了选出的股票代码和总分没有保存当时的因子明细。两周后发现某只股票总分排名第一想回头看它当时 PE 是多少、换手率是多少结果完全查不到——因为文件里只有一堆 6 位代码。从那以后他每次保存结果都会把当时的市值、PE、换手率、5 日涨幅这些因子原样带上宁多勿少。这样做的价值在于当策略表现不好想调整权重时你有一份完整的历史因子快照可以用来复盘而不是只能对着股票代码拍脑袋猜。选股程序的核心价值不是“今天选出什么”而是“为什么选出它、这个原因还成不成立”——把因子明细留给未来的自己就是给决策留后悔药。希望这份拆解帮到你下次跑完脚本拿到结果时记得先问一句这些票入选的原因数据里还看得到吗本文还有配套的精品资源点击获取