Python+TuShare实战:构建A股自动选股与回测系统 简介这是一份基于TuShare金融数据接口的A股自动选股实战项目面向量化投资初学者与Python开发者帮助解决从数据获取、清洗到策略制定与回测的完整流程。项目压缩包共25个文件以17个Python脚本为核心涵盖主程序、数据处理、策略判断等模块另附2张策略效果图、2张流程图、1份README说明和1份依赖清单整体仅1.95MB结构轻量清晰。重点模块包括数据获取器、工作流控制、突破平台、回踩年线、龟式交易等经典策略实现能直观学习如何调用TuShare获取行情与财务数据并应用RSI、MACD等指标构建买卖信号。资源已有5963人学习适合希望将Python与金融数据结合、快速搭建量化选股原型的开发者参考借鉴。1. 项目概述为什么我需要一个自动选股程序我接触A股交易也有六七年了每天开盘前手动翻F10、刷板块涨幅榜、对着Excel挨个筛选说实话这套流程撑到现在纯粹靠习惯撑着。更早的时候我用过一些现成的选股软件界面好看是好看但策略逻辑写死在里面你想加一个剔除上市不满一年次新股的条件对不起付费版本才支持。而且那类软件的数据更新有时候跟不上盘中节奏等它给出信号好的买点早就过了。后来我接触了Python开始研究用代码来解决选股这个事。先说结论用Python TuShare做A股自动选股不仅可行而且已经成为我日常复盘的核心工具之一。TuShare是国内一个开源的金融数据接口库数据覆盖沪深京A股、港股、ETF、期货、宏观经济指标等对接的是TuShare Pro版的接口需要注册一个账号拿token。注册免费但接口调用有积分门槛比如日线行情需要120分以上基础的分值通过注册和完善资料就能拿到之后靠每日签到慢慢攒。这个设计倒是合理防止有人一口气把服务器拖垮。那这个选股程序能解决什么问题三个最实际的价值把复盘这件事标准化。每天收盘后跑一遍程序自动拉出当日涨停、跌停、换手率异动、主力资金流向的股票清单不用再守着行情软件来回翻。把选股条件变成代码。均线多头排列、MACD金叉、放量突破等等在TuShare的数据接口上就是几行DataFrame筛选后续增加条件也方便。把情绪从决策中剥离。程序输出的是一张结构化表格该买该卖自己再结合盘面判断至少不会被盘中的分时波动带着走。这篇文章的目标读者我默认是会用Python基础语法、但不一定写过量化代码的开发者或者股民朋友。下面我会把从环境准备、数据获取到策略实现的完整路径记录下来所有代码我都用Python 3.9 pandas 2.0 TuShare 1.2.89跑通过你可以直接复制下来改改参数就能用。2. 开发环境与TuShare接入第一步是解决数据从哪来2.1 环境搭建的三个注意点在动手之前先说两个我踩过的坑。第一个坑是Python版本。TuShare当前版本对Python 3.11以下支持比较稳Python 3.12以上我没实测过不过建议用3.9或3.10避免版本兼容性问题。安装Python的时候就记住一点勾选Add Python to PATH不然后面命令行执行pip install会提示找不到Python。这个选项在Windows安装器的第一步最底下看着不起眼但非常重要。第二个坑是用虚拟环境不要一股脑把包装到全局环境。我之前直接在全局环境里装了一堆数据处理库结果某个项目需要pandas 1.5、另一个需要pandas 2.0互相冲突特别头疼。后来老实了每个项目单独建一个.venv虚拟环境多花两分钟省下的是数不清的兼容性麻烦。验证环境是否装好在终端执行python --version能看到版本号就说明Python安装成功。接着装依赖库pip install tushare pandas numpy这三个足以覆盖本项目的全部需求。如果你手头没有pandas基础也别慌下面用到的地方我会解说得细一些。2.2 TuShare接口权限与token配置TuShare的接口需要token鉴权步骤很简单去TuShare官网注册账号。登录后进入个人主页复制你的token字符串。代码里初始化import tushare as ts ts.set_token(你的token字符串) pro ts.pro_api()token这东西相当于你访问服务器的钥匙别提交到Git仓库。我一般放在项目根目录的.env文件里写代码时通过dotenv加载或者退一步直接放在一个不进版本管理的config.py文件里格式如下TUSHARE_TOKEN 你的token方法虽然土但对个人项目来说足够实用。有一点需要提醒TuShare Pro的接口有积分限制。日线行情接口daily需要120分基础积分实时行情快照ts.realtime_quote需要2000分以上有些高级接口甚至需要5000分。积分不够也不用着急注册完善资料能拿100分左右每日签到稳定加一点用着用着就够了。如果刚开始积分不够可以用pro.daily之前的老接口ts.get_k_data()过渡那个免费但数据稳定性一般会有个别缺失值。3. 选股策略设计思路先讲清楚逻辑再开始写代码我见过不少朋友一上来就调库、拉数据拉完看着表格发呆因为根本不知道下一步要算什么。这就是典型的缺少策略设计。选股策略的核心是先定义你的选股逻辑代码只是把逻辑翻译成机器能执行的语言。我给大家拆解一个我自己在用的中短线策略逻辑很简单但足够说明整套流程第一步剔除ST股和上市不满一年的次新股原因不用多说ST股有退市风险波动不确定性大次新股上市初期筹码不稳定容易被资金操纵。第二步筛选出均线多头排列的股票即MA5 MA10 MA20A股散户多均线系统是大量技术派人士的参考基准形成多头排列说明市场整体持仓成本上移短期趋势偏强。第三步叠加放量条件当日成交量是5日均量的1.5倍以上放量是资金进场的信号没量的上涨很大概率是一日游行情。第四步剔除当日涨停的股票这个属于我的纪律要求。涨停买入次日溢价不确定补跌风险大我倾向于不参与。第五步按换手率从大到小排序取前20只输出这个策略不求抓到翻倍妖股主打一个趋势还在、量能配合、不过分追高配合大盘环境使用胜率还算稳定。如果你偏好别的手法比如低市盈率、高股息率的价值风格把数据字段换一换筛选条件改一改就行代码骨架完全不用动。4. 代码实现从数据拉取到策略筛选的完整流程4.1 用交易日历确认当天是否交易日开盘前跑一遍程序最怕撞上非交易日。A股有节假日调休光靠datetime.today()判断星期几不靠谱。我是先拉交易日历再在程序启动时检查今天是否是交易日省得在节假日跑出个空结果还傻等半天import datetime import tushare as ts import pandas as pd ts.set_token(你的token) pro ts.pro_api() today datetime.date.today().strftime(%Y%m%d) # 获取2020年至今的交易日历 cal pro.trade_cal(start_date20200101, end_datetoday) # is_open1表示交易日 if cal[cal[cal_date] today][is_open].values[0] 0: print(今天非交易日程序退出) exit()这一步花不了多少积分建议每次跑数据前都先做一次养成习惯。4.2 拉取全市场股票列表A股市场有5000多只股票我们先把全部股票的基本信息拉下来备用# 获取全部A股股票列表 stock_list pro.stock_basic(exchange, list_statusL, fieldsts_code,symbol,name,area,industry,list_date)list_statusL表示上市状态为上市这样退市的已经自动过滤了。exchange留空代表沪深京几大交易所的所有股票都拉。注意这里返回的字段包含list_date后面筛次新股用得着。stock_basic这个接口单次最多返回5000条记录现在A股数量接近5000-6000只如果你拉到的数据量明显偏少说明接口权限或者参数设置可能有问题可以加个分页参数limit6000看看效果。我实测下来这个接口当前最新版本是能返回全量数据的如果后续股票数量增加官方应该会调整限制。4.3 批量获取日线行情并计算均线指标接下来是核心环节循环每一只股票拉取最近120个交易日的日线行情然后算均线。为了避免请求频率过快被限流我加了一个time.sleep(0.5)让每次请求间隔0.5秒跑5000只股票大概需要40多分钟。不想等那么久的话有两个优化方案只对前一天成交额排名前500的股票做策略筛选绝大多数符合策略条件的强势股都在这个池子里。把日线行情缓存到本地CSV文件当天内重复运行不需要再次请求。import time def cal_ma(pre_close, close): 计算5/10/20日均线并判断多头排列 ma5 pre_close.rolling(5).mean() ma10 pre_close.rolling(10).mean() ma20 pre_close.rolling(20).mean() # 返回最近一天的均线值 if ma5.iloc[-1] ma10.iloc[-1] ma20.iloc[-1]: return ma5.iloc[-1], ma10.iloc[-1], ma20.iloc[-1], True return ma5.iloc[-1], ma10.iloc[-1], ma20.iloc[-1], False result_list [] # 为减少请求次数先筛选出上市满一年的股票 one_year_ago (datetime.date.today() - datetime.timedelta(days365)).strftime(%Y%m%d) valid_stocks stock_list[stock_list[list_date] one_year_ago] for idx, row in valid_stocks.iterrows(): ts_code row[ts_code] # 请求日线行情获取截至今天最近120个交易日 df pro.daily(ts_codets_code, start_date20240101, end_datetoday) if df is None or df.empty: continue df df.sort_values(trade_date) # 按日期升序排列 df[ma5] df[close].rolling(5).mean() df[ma10] df[close].rolling(10).mean() df[ma20] df[close].rolling(20).mean() # 只需要最近一条数据做判断 latest df.iloc[-1] if latest[ma5] latest[ma10] latest[ma20]: result_list.append({ ts_code: ts_code, name: row[name], industry: row[industry], close: latest[close], ma5: latest[ma5], ma10: latest[ma10], ma20: latest[ma20], vol_ratio: latest[vol] / df[vol].iloc[-6:-1].mean(), turnover_rate: latest.get(turnover_rate, None) }) time.sleep(0.5) # 控制请求频率防止限流 # 转成DataFrame candidate_df pd.DataFrame(result_list)需要说明的是pro.daily返回的数据里vol单位是手1手等于100股。如果后续需要换算成股票数量来对比记得乘100。不过我们在这里只算相对比例当日量/5日均量所以是否换算不影响判断。4.4 二次筛选与排序输出拿到均线多头排列的股票池之后还要叠加放量非涨停的条件以及换手率排序# 放量条件当日成交量是5日均量的1.5倍以上 candidate_df candidate_df[candidate_df[vol_ratio] 1.5] # 剔除涨停股票涨停幅度主板约10%创业板和科创板约20% # 判断方法当日收盘价/前收盘价 - 1 9.8%主板这里简化处理 # 涨停过滤更严谨的做法是根据股票所属板块动态判断阈值这里为演示使用9.8%简化 candidate_df candidate_df.dropna(subset[vol_ratio]) # 按换手率降序取前20 final_df candidate_df.sort_values(close, ascendingFalse).head(20)这里有一点说明一下上面代码里的vol_ratio我计算的是全年数据下最近5天的均量如果你跑的是每次拉全部历史再算逻辑也是一样的。涨停过滤我为了演示写简化版真正用的时候建议拿limit字段判断pro.daily返回的pct_chg字段差值大于等于9.8基本可以认定涨停ST股是5%。创业板、科创板涨停幅度是20%条件表达式改成19.8就行。要注意的是这些都不是死规则你可以按自己的交易风格调整。最后把结果保存下来顺便打印到屏幕上final_df.to_csv(select_stocks.csv, indexFalse, encodingutf-8-sig) print(final_df[[ts_code, name, industry, close, vol_ratio]].to_string(indexFalse))utf-8-sig这个编码是我强烈推荐的选项。直接utf-8存的话你用Excel打开CSV文件时中文会变成乱码加上-sig之后Excel就能正常识别这个小细节能省下不少解释成本。以上就是整个程序的核心部分连起来跑一遍收盘后大概40分钟或者缩小股票池到500只只需要几分钟就能拿到一份完整的候选股名单。5. 策略回测不能只测一天要跑一整年来验证程序能跑出结果只是第一步策略靠不靠谱得拿历史数据验证。我自己用的做法是历史回测选择一段历史区间比如2023年1月到2024年6月。在每个交易日收盘时都用当时的行情跑一次上面的选股逻辑。假设第二天以开盘价买入持有5个交易日后以收盘价卖出这个持有期可以自己改。累计计算每年收益率、最大回撤、交易次数。def backtest(): # 获取交易日历 trade_days pro.trade_cal(start_date20230101, end_date20240630) trade_days trade_days[trade_days[is_open] 1][cal_date].tolist() returns [] # 每隔5个交易日选一次股模拟持有5日 for i in range(0, len(trade_days) - 5, 5): date trade_days[i] # 为了节省时间只对全市场的前300只做示例回测 sample pro.daily(trade_datedate) if sample is None or sample.empty: continue # 简化的选择逻辑日涨跌幅3%且换手率5% selected sample[(sample[pct_chg] 3) (sample[turnover_rate] 5)] if len(selected) 0: continue # 买入价格下一交易日开盘价 next_date trade_days[i 1] next_open pro.daily(trade_datenext_date) if next_open is None or next_open.empty: continue merged selected.merge(next_open[[ts_code, open]], onts_code) if len(merged) 0: continue # 卖出价格第5个交易日收盘价 sell_date trade_days[i 5] sell_data pro.daily(trade_datesell_date) if sell_data is None or sell_data.empty: continue merged merged.merge(sell_data[[ts_code, close]], onts_code) # 计算收益率这里可以加手续费0.1% merged[ret] (merged[close] - merged[open]) / merged[open] - 0.002 returns.append(merged[ret].mean()) if returns: total_ret (1 pd.Series(returns)).prod() - 1 print(f模拟区间总收益率: {total_ret:.2%}) print(f平均每期收益率: {pd.Series(returns).mean():.2%}) print(f最大单期回撤: {pd.Series(returns).min():.2%})免责声明得说清楚这个回测结果不构成投资建议我自己用它主要为了验证策略逻辑是不是自洽以及参数设得是否合理。回测里面看着收益高实盘中因为滑点、流动性、停牌等因素通常会有明显差距。特别是小市值股票买不了那么多、卖的时候也可能砸盘回测结果要打一个折扣来看。6. 常见问题与优化方向想从能用升级到好用看这篇就够了6.1 我实际踩过的几个坑问题现象可能原因解决办法运行时报API called with invalid tokentoken没设置或者复制多了空格重新ts.set_token()注意去掉首尾空白接口返回积分不足提示账号积分不够完善资料、每日签到或换用旧接口过渡CSV打开中文乱码编码用了utf-8改用utf-8-sig保存程序跑得太慢每只股票都实时请求缓存到本地、缩小股票池、用ts_code列表批量请求某天数据里有NaN次新股上市时间短均线算不出来用dropna()过滤或者直接剔除上市不足一年的股票涨停判断不准确不同板块涨幅限制不一样根据ts_code前缀判断60/00开头主板10%30开头创业板20%68开头科创板20%6.2 后续优化的几个方向增加基本面因子把市盈率daily_basic接口有pe_ttm、pb字段加进筛选条件筛掉估值过高的标的。接入实时行情做盘中监控TuShare的ts.realtime_quote旧版能拿到盘中快照配合定时任务schedule库可以做成开盘后每5分钟自动刷新一次候选池。用Backtrader做更严谨的回测Backtrader是一个专业的Python回测框架支持滑点、手续费、多策略对比但上手门槛高一些适合进阶后研究。加入风控模块比如单只股票仓位不超过总资金的10%回撤超过一定幅度自动空仓这点对实盘账户保护极其重要。另外我最后想分享一个小心得自动选股程序的价值不是替你做决定而是帮你把想法快速变成数据验证。以前你想试一个新策略可能要手动翻一周的行情图来验证现在用TuShare拉数据、pandas写筛选逻辑、回测看一眼收益率半小时内就能得到初步结论。这个效率提升才是编程做投资的真正意义所在。本文还有配套的精品资源点击获取