资讯动态

TuShare + pandas 构建 A 股自动选股程序:从数据拉取到策略回测

发布时间:2026/10/17 17:49:58 来源:尧图企业网站定制
简介这是一份面向量化投资初学者与Python开发者的实战型项目源码围绕TuShare财经数据接口构建A股自动选股系统解决从行情获取到策略回测的完整链路问题。资源包共25个文件以17个py脚本为核心涵盖数据获取、策略制定、回测与退出等模块另含2张jpg与2张png策略示意图、1份md说明文档及1个txt依赖清单压缩包约1.95MB结构清晰便于按模块研读。目前已有6025人学习下载热度较高。读者可从中掌握TuShare历史与实时行情接口的调用方式理解数据清洗、缺失值处理与格式转换流程并参考移动平均线交叉、RSI、MACD及突破平台、海龟交易、低ATR等策略实现配合回测模块评估不同市场环境下的表现适合作为量化选股与金融数据分析的入门实践参考。1. 用 TuShare 搭一套 A 股自动选股程序到底能省下多少复盘时间每天收盘后手动翻几百只票的均线、量比、换手率翻完已经晚上十点第二天开盘还是凭感觉下单——这是很多做量化入门的朋友真实的状态。这套基于 TuShare 的 A 股自动选股程序干的就是把这段重复劳动交给脚本拉取日线行情、计算技术指标、按预设条件过滤、输出候选股票池一条命令跑完。它适合两类人一是想用 Python 做量化但不知道从哪下手的开发者二是已经有选股逻辑、只差一个稳定数据管线和筛选框架的散户。整套代码围绕 TuShare 的数据接口组织不依赖付费行情源本地跑通后每天定时执行即可属于典型的 Python 开发-其它杂项类小工具但麻雀虽小数据清洗、指标计算、条件组合这几块该有的都有。2. 项目结构与技术选型为什么是 TuShare pandas 这套组合2.1 整体目录长什么样拿到源码后先别急着跑花两分钟把目录结构过一遍后面改代码会顺很多。这类选股程序的结构通常比较扁平核心逻辑集中在几个文件里常见做法是这样组织的astock_selector/ ├── config.py # 全局配置token、股票池范围、筛选阈值 ├── data_fetcher.py # TuShare 数据拉取与本地缓存 ├── indicators.py # 技术指标计算MA、MACD、RSI、量比等 ├── strategy.py # 选股条件组合与过滤逻辑 ├── main.py # 主入口串起拉数、算指标、选股、输出 ├── output/ # 选股结果落盘目录csv/excel └── requirements.txt # 依赖清单这个划分的好处是职责清晰数据源换了只动data_fetcher.py选股逻辑改了只动strategy.py指标库要扩充就加在indicators.py。我一般建议新手先读main.py它是整条流水线的主干看懂主干再往两边看细节比从config.py一行行啃效率高得多。2.2 技术栈为什么这么选核心依赖就三个tushare负责行情数据pandas负责数据处理numpy负责数值计算。选 TuShare 而不是别的数据源主要原因是它对 A 股日线、复权因子、每日指标换手率、量比、市盈率的覆盖比较全而且接口风格统一一个pro_api()对象就能调大部分接口。pandas 在这里不是可选项而是刚需——拉回来的数据是 DataFrame指标计算、条件过滤、分组排序全靠它用原生 list 处理几千只股票会非常痛苦。requirements.txt里通常还会带上openpyxl导出 Excel和tqdm显示拉取进度。这两个不是核心但加上之后体验好很多尤其是拉全市场数据时没有进度条你会怀疑程序是不是卡死了。2.3 数据流是怎么走的整条链路可以概括成四步初始化接口 → 拉取股票列表和日线 → 计算指标 → 按条件筛选输出。理解这条链路后面看代码就不会迷路。数据从 TuShare 进来是「长表」形态每行一个交易日一只股票指标计算后还是长表筛选时按股票代码分组取最新一天的值做判断最后输出的是「当前满足条件的股票清单」。这个「分组取最新」是整条链路里最容易写错的地方第 4 章会专门讲。3. 核心模块拆解从拉数据到出选股结果3.1 初始化 TuShare 接口与股票池第一步是把接口对象建起来并确定要扫描的股票范围。全市场四千多只票全拉一遍很慢通常先按板块或市值筛一个池子出来。import tushare as ts import pandas as pd # 初始化 pro 接口token 从 config 读取不要硬编码在业务代码里 pro ts.pro_api(tokenTOKEN) def get_stock_pool(): 获取基础股票池剔除退市、ST、上市不足 60 天的票 # stock_basic 返回所有股票的基础信息 basic pro.stock_basic( exchange, list_statusL, # L 上市 D 退市 P 暂停 fieldsts_code,name,industry,list_date,market ) # 剔除 ST 和退市整理 basic basic[~basic[name].str.contains(ST)] # 剔除上市不足 60 天的次新股避免指标失真 basic[list_date] pd.to_datetime(basic[list_date]) cutoff pd.Timestamp.today() - pd.Timedelta(days60) basic basic[basic[list_date] cutoff] return basic.reset_index(dropTrue)这段代码的关键在list_statusL和两个过滤条件。list_status参数控制返回哪些状态的股票写L只拿正常上市的退市和暂停的自动排除。剔除 ST 是因为这类票波动逻辑和正常票不一样混在一起选会污染结果。剔除次新股是因为上市不足 60 天算不出 60 日均线指标会变成 NaN后面筛选时容易出玄学问题。fields参数建议显式指定不写的话返回字段很多白白增加传输和内存开销。3.2 拉取日线行情并做本地缓存TuShare 的日线接口有频率限制反复拉同一批数据既慢又浪费额度所以本地缓存几乎是必备的。import os from datetime import datetime, timedelta CACHE_DIR ./cache def get_daily(ts_code, start_date, end_date): 拉取单只股票日线优先读本地缓存 cache_file os.path.join(CACHE_DIR, f{ts_code}.csv) # 缓存存在且是今天更新的直接读 if os.path.exists(cache_file): mtime datetime.fromtimestamp(os.path.getmtime(cache_file)) if mtime.date() datetime.today().date(): return pd.read_csv(cache_file) # 否则走接口拉取 df pro.daily(ts_codets_code, start_datestart_date, end_dateend_date) if df.empty: return df df.to_csv(cache_file, indexFalse) return df缓存策略这里用的是「按天失效」只要文件是今天生成的就直接读否则重新拉。这个粒度对日线数据刚好够用因为日线一天只更新一次。start_date和end_date格式是YYYYMMDD字符串TuShare 不接受带横杠的日期这点新手经常踩。另外注意pro.daily返回的数据是未复权的如果策略里要用到长期均线得换成pro.pro_bar并传adjqfq拿前复权数据否则除权那天会出现价格跳空均线直接失真。3.3 技术指标计算MA、量比、换手率指标计算是选股的核心这里以最常用的均线和量比为例。def calc_indicators(df): 在日线 DataFrame 上追加技术指标列 df df.sort_values(trade_date).reset_index(dropTrue) # 收盘价均线窗口按需调整 df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() df[ma60] df[close].rolling(60).mean() # 量比当日成交量 / 过去5日平均成交量 df[vol_ma5] df[vol].rolling(5).mean() df[vol_ratio] df[vol] / df[vol_ma5] # 涨跌幅TuShare 的 pct_chg 已是百分比直接用 df[pct_chg] df[pct_chg] return dfrolling(n).mean()是 pandas 算移动平均的标准写法注意它默认按行顺序算所以前面必须先sort_values(trade_date)把日期排好否则算出来的均线是乱的——这是最隐蔽的坑之一数据看着有值逻辑全错。vol_ratio用当日量除以 5 日均量值大于 1 表示放量小于 1 表示缩量是判断资金异动的常用指标。换手率如果要用得从pro.daily_basic接口单独拉日线接口里没有这个字段别在daily的返回里找。3.4 选股条件组合与结果输出有了指标最后一步是把条件拼起来做筛选。这里演示一个「均线多头 放量」的经典组合。def select_stocks(pool, trade_date): 遍历股票池返回满足条件的股票 result [] for _, row in pool.iterrows(): ts_code row[ts_code] df get_daily(ts_code, start_date, trade_date) if len(df) 60: # 数据不足60天直接跳过 continue df calc_indicators(df) latest df.iloc[-1] # 取最新一个交易日 # 条件5日线上穿20日线 且 放量 且 当日收阳 cond ( latest[ma5] latest[ma20] and latest[vol_ratio] 1.5 and latest[close] latest[open] ) if cond: result.append({ ts_code: ts_code, name: row[name], close: latest[close], vol_ratio: round(latest[vol_ratio], 2), pct_chg: latest[pct_chg] }) return pd.DataFrame(result)这段逻辑里df.iloc[-1]取最新一行前提是前面已经按日期升序排好否则取到的是最老的一天。条件用and串联每个条件对应一个指标阈值改策略本质上就是改这几个阈值和组合方式。vol_ratio 1.5是放量阈值想宽松点改成 1.2想严格点改成 2.0。返回结果用 DataFrame 装方便后面直接to_csv或to_excel落盘。整个循环对四千只票跑一遍配合缓存大概几分钟能出结果第一次拉数据会慢一些。4. 避坑与常见问题跑不起来多半是这几个原因4.1 报错「抱歉您没有访问该接口的权限」现象是调用pro.daily或pro.daily_basic时直接抛异常提示积分不足或权限不够。原因是 TuShare 的部分接口对账号积分有门槛日线基础接口门槛低但daily_basic、pro_bar复权这类接口要求更高积分。解决办法是先确认自己账号的积分等级把非必需的高权限接口替换成基础接口或者先用ts.pro_bar的免费额度测试。别一上来就调一堆高权限接口跑不通还以为是代码问题。4.2 指标全是 NaN筛选结果为空现象是ma60整列是 NaN或者筛选出来一只票都没有。原因通常是两个一是数据行数不够 60 天rolling(60)前 59 行必然是 NaN二是日期没排序rolling 算出来错位。解决办法是在计算指标前先判断len(df) 60不够就跳过同时强制sort_values(trade_date)。我一般会在calc_indicators开头加一句断言数据不满足条件直接抛错比默默算出 NaN 强。4.3 拉全市场数据跑到一半卡死现象是循环拉几百只票后程序不动了也没有报错。原因是 TuShare 接口有每分钟调用次数限制超了会被限流表现就是请求挂起。解决办法是在循环里加time.sleep(0.3)之类的间隔或者用缓存避免重复拉取。更稳妥的做法是把拉取和计算拆成两个脚本拉取脚本慢慢跑把数据落盘计算脚本读本地文件互不干扰。4.4 复权没处理均线出现断崖现象是某只票的均线在除权日附近突然跳变选出来的信号完全不合理。原因是用了未复权的pro.daily数据算长期均线。解决办法是改用ts.pro_bar(ts_code..., adjqfq)拿前复权数据或者在策略里对除权日做特殊处理。前复权是量化里最常用的处理方式能保证历史价格和当前价格在同一尺度上比较。4.5 结果每天不一样怀疑程序有 bug现象是同样的代码今天跑和昨天跑选出的票差异很大。这通常不是 bug而是数据本身在变——每天收盘后行情更新指标自然变化。要验证程序稳定性可以固定一个历史交易日把trade_date写死跑两次看结果是否一致。如果一致说明逻辑没问题差异来自数据更新如果不一致那才是代码里有随机性或状态残留。5. 二次开发把选股逻辑改成你自己的5.1 换策略就是换条件组合这套程序最大的价值不是它自带的均线策略而是那套「拉数 → 算指标 → 过滤」的框架。你想换成 MACD 金叉、RSI 超卖、或者基本面选股只需要在indicators.py里加指标、在strategy.py里改条件主干完全不用动。比如加一个 MACDdef add_macd(df, fast12, slow26, signal9): 追加 MACD 指标列 ema_fast df[close].ewm(spanfast, adjustFalse).mean() ema_slow df[close].ewm(spanslow, adjustFalse).mean() df[dif] ema_fast - ema_slow df[dea] df[dif].ewm(spansignal, adjustFalse).mean() df[macd] (df[dif] - df[dea]) * 2 return dfewm是指数移动平均adjustFalse表示不做递归调整和大多数行情软件的口径一致。加完之后在筛选条件里判断latest[dif] latest[dea]就是金叉信号。参数fast/slow/signal保持默认的 12/26/9 就行改这几个值等于换了一套 MACD 定义除非你明确知道自己在干什么否则别乱动。5.2 用配置文件管理阈值别写死在代码里策略调参是高频操作把阈值写死在strategy.py里每次改都要动代码很容易改出问题。更好的做法是全部塞进config.py# config.py STRATEGY { ma_short: 5, ma_long: 20, vol_ratio_min: 1.5, pct_chg_min: 0, max_results: 50, }然后在策略里读STRATEGY[vol_ratio_min]。这样调参只改一个文件还能顺手做参数扫描——写个循环遍历几组阈值看哪组历史表现好。我一般会把不同策略存成不同的 dict跑的时候指定用哪套方便对比。5.3 加一层回测验证别直接上实盘选股程序最容易犯的错是「看着逻辑对就直接用」结果实盘一跑就亏。加一层简单回测能过滤掉大部分想当然的策略。思路是对每个历史交易日用当天及之前的数据选出股票统计这些股票在之后 N 天的收益最后看整体胜率和盈亏比。def backtest(pool, start_date, end_date, hold_days5): 简化回测统计选出股票持有N天的平均收益 trade_dates pro.trade_cal( exchangeSSE, start_datestart_date, end_dateend_date, is_open1 )[cal_date].tolist() records [] for d in trade_dates[:-hold_days]: picks select_stocks(pool, d) for _, p in picks.iterrows(): # 取持有期结束那天的收盘价算收益 future get_daily(p[ts_code], d, trade_dates[-1]) if len(future) hold_days: ret future.iloc[hold_days][close] / p[close] - 1 records.append(ret) return pd.Series(records).describe()这段回测很粗糙没考虑手续费、滑点、涨跌停无法成交但足够用来判断一个策略是不是「明显不靠谱」。如果平均收益是负的或者胜率低得离谱就别浪费时间优化参数了直接换思路。trade_cal接口拿交易日历避免把周末和节假日算进去这个细节不做的话回测结果会偏。5.4 一个我踩过的坑早期我图省事把选股结果直接接到自动下单脚本上结果某天数据接口返回异常选出一堆空值下单脚本照着空值去下单差点出事。从那以后我每次改完策略都强制走一遍「固定历史日期 → 跑选股 → 人工核对结果」的流程确认输出正常才敢接下游。选股程序本身不复杂但它连着真金白银多一道校验不亏。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑