简介:面向量化投资初学者与Python开发者的实战型资源,聚焦如何利用TuShare开源财经数据接口构建A股自动选股系统。项目完整覆盖数据获取、清洗处理、指标计算、策略回测等核心环节,策略示例涉及移动平均线交叉、RSI、MACD等常用技术指标,也包含基于财务报表的基本面分析思路,并提供了平台突破、均线回归、ATR波动、乌龟交易、持续上涨等多种买卖信号模块,适合想将编程技能应用到金融场景的读者。资源压缩包共25个文件,其中17个Python脚本为主要代码,涵盖数据抓取、策略编写、工作流调度与回测工具;另有Markdown说明、文本配置以及统计图表图片,便于理解工程结构和策略表现,整体仅1.95MB,轻量易部署。已有5963人学习下载,代码模块划分清晰、可直接运行调试,能帮助读者快速搭起自己的量化选股框架。 打开TuShare的文档页面之前,我一直觉得“自动选股”是很玄的事情,直到我用Python把它拆成了“拉数据—算指标—做过滤—出结果”四步,事情一下就变得极其朴素。这篇文章就是想把我这套基于TuShare的A股自动选股程序完整摊开讲一遍:整体怎么设计、数据怎么接、规则怎么落成代码、实际跑起来会踩哪些坑。适合已经会一点Python、想把自己那套选股逻辑“代码化”的朋友,也适合刚开始接触量化、但不想一上来就碰重型框架的人。
1. 整体设计思路与方案选型
1.1 先想清楚:选股不等于交易
我见过不少人一提到量化就想着全自动买卖,这是最大的误区。我这套程序只解决“选股池怎么来”这件事,也就是每天收盘后自动跑一遍全市场数据,筛出符合规则的股票名单。最后的买点、仓位、下单,全部人工确认。
原因很简单:A股有涨跌停、T+1、停复牌、ST摘帽等各种规则,实盘自动交易还涉及券商接口、风控、滑点处理,复杂度是指数级上升。做自动选股则安全得多——它是“人做决策,机器做数据整理”,就算代码有bug,最坏的结果就是名单不准,不会造成资金损失。
1.2 为什么是TuShare,而不是别的数据源
市面上的A股数据源我基本都试过,简单说下我的对比感受:
| 数据源 | 优点 | 缺点 | 我的评价 |
|---|---|---|---|
| TuShare Pro | 字段全,行情/财务/每日指标都有,社区成熟 | 部分接口有积分门槛 | 选股场景最均衡,首选 |
| AkShare | 免费、接口非常多 | 文档跳跃,部分接口依赖爬虫,稳定性一般 | 适合做研究,不适合做定时任务 |
| Baostock | 免费、行情稳定 | 财务指标少,更新偏基础 | 只做行情回测可以 |
| 第三方Excel插件/软件 | 省事、可视化 | 黑盒、不可定制、关键逻辑看不到 | 不满足“固化成代码”的需求 |
TuShare Pro的核心接口正好覆盖选股所需:stock_basic拿股票列表,daily和pro_bar拿行情,daily_basic拿每日估值数据,fina_indicator拿财务指标。这些都是程序化筛选的“原材”。虽然有积分限制,但如果你只是做基础选股,注册后默认的积分额度基本够用,后面我会专门讲积分不够怎么办。
1.3 选股逻辑怎么设计
一套选股程序最关键的其实不是代码,是规则。我把规则分成三层:
- 基本面定范围:估值不能太贵(PE、PB),盈利能力要过得去(ROE),市值要适中(太小有退市风险,太大涨不动)。
- 技术面找时机:均线多头排列、不能下跌趋势、成交量不能太低。
- 风险面做排除:剔除ST股、剔除上市不满一年的次新股、剔除当天一字涨停买不进去的票。
设计逻辑的时候要记住一个原则:规则宁少勿多。规则越多,筛完往往一只不剩,反而没法用。我一开始加了十几条过滤条件,回测三天全是空仓,后来砍到六条核心规则,效果才正常。
1.4 程序模块怎么划分
我习惯按数据流分层,每个模块只干一件事:
- 数据获取层:从TuShare拉取原始数据,做本地缓存。
- 数据清洗层:处理缺失值、复权、日期对齐。
- 指标计算层:计算均线、PE、ROE等技术面和基本面因子。
- 策略筛选层:把选股规则固化成多个过滤函数。
- 输出层:把符合条件的股票写入CSV或Excel,方便第二天直接看。
分层的好处是,以后想改逻辑,只动筛选层,不用碰数据层。这个程序我后来迭代了四五个版本,每次都是这种结构省下来的时间。
2. 环境准备与TuShare接入
2.1 Python环境与依赖
我用的是Python 3.10,理论上3.8以上都能跑。建议用虚拟环境,别把库装到系统Python里,不然哪天系统Python被别的项目搞坏,你的选股程序也跟着遭殃。
python -m venv stockenv source stockenv/bin/activate # Windows下执行 stockenv\Scripts\activate pip install pandas numpy tushareTushare要装最新版,老版本很多Pro接口不兼容:
pip install --upgrade tushare2.2 注册TuShare Pro与Token配置
打开TuShare官网,注册账号,然后在“个人主页”里找到“接口TOKEN”,复制那一串token字符串。程序里通过ts.pro_api(token)完成身份认证。
有一个新手特别容易犯的错:把token直接硬编码在代码里。一旦你把代码传到GitHub或者发给别人,token就泄露了。哪怕只是自己本地跑,我也建议放在单独的配置文件里:
# config.py TUSHARE_TOKEN = "你的token放这里,这个文件不要提交到Git"import config import tushare as ts pro = ts.pro_api(config.TUSHARE_TOKEN)初始化之后,后续所有数据接口都通过pro这个对象调用。
2.3 数据本地缓存设计
TuShare的行情接口有频率限制,每天调用次数也有限。如果每次跑选股都现拉几年历史行情,接口分分钟被限流。我的办法是:拉下来的日线数据全部存本地CSV,下次启动时先读本地文件,只在本地数据缺失时才去请求。
import os import pandas as pd def get_daily_from_cache(ts_code, start_date, end_date): cache_dir = "data/daily" os.makedirs(cache_dir, exist_ok=True) cache_file = os.path.join(cache_dir, f"{ts_code}.csv") if os.path.exists(cache_file): df = pd.read_csv(cache_file, dtype={"ts_code": str}) df["trade_date"] = df["trade_date"].astype(str) # 缺哪段补哪段,增量的概念 need_start = df["trade_date"].max() if len(df) else start_date if need_start >= end_date: return df[(df["trade_date"] >= start_date) & (df["trade_date"] <= end_date)] new_df = ts.pro_bar(ts_code=ts_code, adj="qfq", start_date=need_start, end_date=end_date) if new_df is not None and not new_df.empty: df = pd.concat([df, new_df.drop_duplicates(subset=["trade_date"])], ignore_index=True) df = df.drop_duplicates(subset=["trade_date"]).sort_values("trade_date") df.to_csv(cache_file, index=False) return df[(df["trade_date"] >= start_date) & (df["trade_date"] <= end_date)] else: df = ts.pro_bar(ts_code=ts_code, adj="qfq", start_date=start_date, end_date=end_date) if df is not None and not df.empty: df.to_csv(cache_file, index=False) return df这个函数后来成了整个程序的地基,没有它,选股跑一遍要十分钟,有了它基本十几秒出结果。
3. 核心代码实现
3.1 股票池构建:拿到全市场A股列表
第一步是获取所有正常上市的A股,用stock_basic接口:
def get_stock_pool(): df = pro.stock_basic(exchange="", list_status="L", fields="ts_code,name,industry,market,list_date") # 剔除ST、退市整理、上市不满一年 df = df[~df["name"].str.contains("ST", na=False)] df = df[df["market"].isin(["主板", "创业板"])] # 按自己需求保留板块 df["list_date"] = pd.to_datetime(df["list_date"]) df = df[df["list_date"] < (pd.Timestamp.now() - pd.Timedelta(days=365))] return df我在这里剔除ST股用的是名称过滤,简单粗暴但有效。板块我保留了主板和创业板,科创板当时没纳入是因为我自己的交易习惯,你可以改成["主板", "创业板", "科创板"]。
3.2 行情获取:复权是必须处理的坑
A股动不动分红送股,不复权的价格K线会有跳空。做量化筛选时,算均线、涨幅一定要用前复权数据,不然均线形态完全失真。
def get_kline(ts_code, start_date, end_date): # 前复权 df = ts.pro_bar(ts_code=ts_code, adj="qfq", start_date=start_date, end_date=end_date) if df is None or df.empty: return pd.DataFrame() df = df.sort_values("trade_date").reset_index(drop=True) return df这里有个小技巧:pro_bar是TuShare提供的复合接口,比直接用pro.daily加pro.adj_factor方便很多,一次就能拿到复权后的行情。
3.3 基本面指标:用每日估值接口还是财务报告接口
基本面过滤我用了两个接口:
daily_basic:每天更新的估值数据,包含pe_ttm(滚动市盈率)、pb(市净率)、total_mv(总市值,单位万元)。fina_indicator:财务指标,我主要取roe。
关键是这个:财务指标用ann_date(公告日期)对齐,不要用end_date(报告期)对齐。因为报告期是“这个数据描述的是哪个时间段”,但数据真正公布出来要晚一两个月。如果用报告期对齐做选股,等于用了未来数据——这就是量化里常说的“未来函数”。
def get_fundamental(ts_code, trade_date): # 取trade_date之前最近一期公告的财务指标 fin = pro.fina_indicator(ts_code=ts_code, start_date="20200101", end_date=trade_date, fields="ts_code,ann_date,roe") if fin is None or fin.empty: return None fin = fin[fin["ann_date"] <= trade_date].sort_values("ann_date") return fin.iloc[-1] if not fin.empty else None3.4 技术面过滤:均线多头排列
我的核心技术条件很简单:收盘价在20日均线之上,且5日、10日、20日均线呈现多头排列。这个条件表达的是“中期趋势向上,短期动能还在”。
def calculate_ma(df): df = df.copy() df["ma5"] = df["close"].rolling(5).mean() df["ma10"] = df["close"].rolling(10).mean() df["ma20"] = df["close"].rolling(20).mean() return df def check_trend(df): if len(df) < 25: return False last = df.iloc[-1] return last["close"] > last["ma20"] and last["ma5"] > last["ma10"] > last["ma20"]均线算完之后,注意把最新一天的数据取出来判断,不要用整段数据做bool判断,否则容易踩到pandas对齐的坑。
3.5 主流程整合:跑一遍全市场
把上面的函数串起来,核心选股流程如下:
def run_select(trade_date): pool = get_stock_pool() results = [] for idx, row in pool.iterrows(): ts_code = row["ts_code"] try: df = get_daily_from_cache(ts_code, "20240101", trade_date) if df.empty or len(df) < 60: continue # 技术面过滤 df_ma = calculate_ma(df) if not check_trend(df_ma): continue # 基本面过滤 daily_basic = pro.daily_basic(ts_code=ts_code, trade_date=trade_date, fields="ts_code,pe_ttm,pb,total_mv,turnover_rate") if daily_basic is None or daily_basic.empty: continue pe = daily_basic.iloc[0]["pe_ttm"] pb = daily_basic.iloc[0]["pb"] mv = daily_basic.iloc[0]["total_mv"] # 单位:万元 turnover = daily_basic.iloc[0]["turnover_rate"] if not (0 < pe < 40 and mv > 500000 and mv < 5000000 and turnover > 1): continue # 财务指标过滤 fin = get_fundamental(ts_code, trade_date) if fin is None or fin["roe"] < 10: continue results.append({ "ts_code": ts_code, "name": row["name"], "pe_ttm": pe, "pb": pb, "roe": fin["roe"], "total_mv": mv / 10000, # 转成亿 "turnover_rate": turnover }) except Exception as e: # 单只股票出错不中断整个程序 print(f"{ts_code} error: {e}") continue res_df = pd.DataFrame(results).sort_values("roe", ascending=False) res_df.to_csv("selected_stocks.csv", index=False, encoding="utf-8-sig") return res_df这个主流程有几个值得说的地方:
- 单只股票出错用
try...except包住,继续下一只。全市场五千多只股票,偶尔一只数据异常很正常,不能让一只票搞挂整个任务。 - 估值和市值条件写在同一行,利用Python短路求值减少多余调用。
- 最后按ROE降序排序,因为ROE代表了盈利能力,选股池内部的排序也要有意义。
3.6 定期运行的实现思路
我不想每天手动跑一遍,所以加了个定时触发。Windows下用“任务计划程序”,Linux/Mac下用cron,本质都是每天收盘后(比如15:30)执行一次python main.py。
# crontab示例:每个交易日15:30运行 30 15 * * 1-5 cd /path/to/stock-selector && python main.py不过要注意,交易日和自然日不是一回事,节假日A股不开盘。简单处理是让程序自己判断当天是不是交易日,TuShare有trade_cal接口:
def is_trade_date(date_str): cal = pro.trade_cal(exchange="SSE", start_date=date_str, end_date=date_str, fields="cal_date,is_open") return cal.iloc[0]["is_open"] == 14. 常见问题与排查技巧实录
4.1 积分不足:最常见的拦路虎
TuShare Pro的接口有积分门槛,注册后你默认有一定积分,但一些接口需要更高积分才能调。实际跑的时候最常见的报错是:抱歉,您没有访问该接口的权限。
我的应对思路分三步:
- 优先用低积分接口:
stock_basic、daily、trade_cal这些基础接口积分要求低,优先保障。 - 积分不够时换替代接口:比如拿不到
fina_indicator的ROE,可以从daily_basic先用pe_ttm、pb做简易版基本面过滤。 - 攒积分:TuShare的积分可以通过完善资料、每日签到等方式累积,等积分够了再逐步解锁更多字段。
说白了,积分体系本质上是限制高频调用,选股是低频任务,只要合理设计缓存和请求次数,低积分档也能跑得很舒服。
4.2 拉回来的数据是空的:先查这三个地方
我调试阶段遇到最多的问题就是接口返回空数据,排查顺序永远是:
- 股票代码的交易所后缀是否正确:平安银行是
000001.SZ,浦发银行是600000.SH,后缀写错基本拉不到数据。 - 交易日和参数范围是否正确:比如
trade_date传了非交易日,接口自然返回空。 - 是否停牌:停牌期间没有行情数据,合理。
4.3 未来函数和数据对齐
这个坑我在3.3已经重点提过,再补充一个场景:你在跑历史回测或者历史某一天的选股时,一定要用ann_date对齐财务数据,而不是end_date。我之前做回测时,直接取了最新一期财报的ROE去筛三个月前的股票,结果回测曲线漂亮得不行,后来才发现这是典型的未来函数,真实盘根本不可能赚到这个钱。
4.4 循环慢到怀疑人生:批量请求优于单只请求
最开始的版本我是在循环里一只一只调日线接口,5000只股票跑下来,加上网络延迟,四十分钟都跑不完。后来优化成两个方向:
- 按交易日批量拉全市场行情,一次请求拿到几千只股票的数据。
- 本地缓存做增量更新,只在首次运行或者数据缺失时发请求。
优化之后,选股时间从四十分钟缩短到二十秒以内,这个提升非常明显。
4.5 选出来的票到底能不能买
实盘验证下来,我想强调一点:程序选出来的是一篮子“符合规则的候选股”,不是“马上就能买入的信号”。原因有几点:
- 技术面过滤使用的是收盘后数据,第二天开盘很可能直接高开,买点完全不同。
- 基本面数据天然滞后,财报是季度快照,不能反映最新变化。
- 单日选股结果受大盘情绪影响很大,极端行情下选出的票可能会集体失真。
所以我的习惯是:程序选出候选池后,再人工叠加一个简单的“等回调再进”的判断。自动化的价值在于帮你把几千只股票压缩成十几只,把精力花在真正值得看的目标上。
5. 实操经验与后续扩展方向
5.1 代码组织的三个小建议
第一,所有接口请求封装成独立函数,加缓存逻辑时只要改一处。第二,选股规则写成配置,比如PE区间、市值区间、ROE阈值都放进config.py,改参数不需要动逻辑代码。第三,输出结果带更新时间戳,不然第二天看到昨天的名单容易误判。
5.2 后续可以扩展的方向
这套程序的架构留了很好的扩展空间。比如我在考虑加一个简单的回测模块:用历史数据模拟“如果按这个选股规则每天换仓,一年收益如何”。逻辑上就是对历史每一天都跑一次选股、计算组合收益,难点在于历史财务数据的对齐和交易成本的估算。
另外,很多人会把选股结果推送到微信或者钉钉,做法也不复杂:在主流程结束的地方加一个requests.post,把结果文本发到webhook地址。我目前是用CSV文件落地,因为看表格更直观,推送到手机适合出差时应急看一眼。
5.3 一个个人心得
我这套程序跑了大半年,最大的体会不是代码本身,而是:选股程序让我的决策从“凭感觉”变成了“有依据”。以前复盘一支股票为什么买,只能说自己“当时觉得会涨”。现在不一样了,我可以明确说出当时满足哪几条规则、数据支撑是什么。即便最终结果不理想,复盘也有迹可循。
另外,参数千万不要过度优化。我一开始把PE区间调到20到25之间,回测结果特别好看,但实盘一跑,这样极端的区间往往筛不出几只票,样本量太小,统计意义不大。后来我把区间放宽到0到40,搭配ROE和技术面条件,选股池的稳定性和实用性反而更好。自动选股的目标不是找到“最完美的参数”,而是把纪律执行下去,让交易体系保持稳定。
本文还有配套的精品资源,点击获取