news 2026/10/20 12:37:43

TuShare实战:Python获取A股行情数据与量化回测全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TuShare实战:Python获取A股行情数据与量化回测全流程

简介:TuShare是一个专注于国内股票市场的金融数据工具,覆盖从数据采集、清洗加工到本地存储的完整流程,适用于量化交易策略研发、历史行情回测和数据分析学习等多种场景,能显著减少手工获取与整理数据的成本。压缩包内共包含118个文件,整体约4.91MB,主要文件类型包括67个Python源码脚本、14个RST文档、10个PDF说明及15个PNG、2个JPG图片示例,其中源码为核心接口实现,文档和图片则负责补充调用说明与界面示意,结构清晰便于按需取用。资源上传至CSDN后已有1407人次学习浏览,适合不同阶段的读者参考。与官网分散信息相比,这里整合了库的主体文件与常见说明,可帮助使用者快速完成环境配置与接口调用,理解返回数据的字段含义,并掌握股票历史数据的落盘方法;附带的公众号二维码和社群入口也可帮助关注更新、获取交流支持。

1. 抓A股数据别再从零爬起:TuShare到底解决什么问题

做量化策略或者金融数据分析时,最耗时间的往往不是模型调参,而是数据获取和清洗。我一开始也试过自己写爬虫去抓股票历史数据,结果遇到反爬、动态IP、停牌、复权这些问题,改了两周还在处理异常。后来换用TuShare这个Python库,原本要折腾一天的数据采集,几行代码就拿到了结构化DataFrame。TuShare是一个面向中国A股市场的金融数据接口库,覆盖行情、基础信息、交易日历、复权因子等常见数据,特别适合用Python做股票市场研究、回测和实时行情快照的从业者。下面我会把环境配置、常用接口、增量更新和踩坑记录完整走一遍。

2. 环境与入门:把TuShare装进Python项目里的正确姿势

2.1 安装与Token获取

TuShare目前有两套接口风格,一套是早期的直连接口,比如ts.get_hist_data,调用简单但数据覆盖不如新版全;另一套是Pro版接口,通过ts.pro_api()返回一个客户端,数据更规范,字段更完整,也是我日常主要使用的。无论用哪套,都需要先安装tushare库,同时准备好pandas用于数据处理。

pip install tushare pandas

安装完成后,去TuShare官网注册账号,然后在个人设置里生成一个token,本质上是一串十六进制字符串。这个token就是你调用接口的钥匙,积分等级都挂在它下面。

import tushare as ts ts.set_token('你的token字符串') pro = ts.pro_api()

这里有个习惯我建议从一开始就立好:不要把这个token直接写到代码文件里,因为它会被git记录然后上传到代码仓库。常见的做法是放进环境变量或者在项目根目录放一个config.ini,读取时再加载:

import os import tushare as ts ts.set_token(os.environ.get("TUSHARE_TOKEN")) pro = ts.pro_api()

这样做的好处是换电脑或者换账号时,不用改业务代码。初始化之后,pro对象几乎可以调用所有Pro接口,比如获取股票列表、日线行情、交易日历等。

为什么非要token?因为这个服务需要对调用者做身份识别和权限控制。不同接口对应的积分门槛不一样,新注册的零积分账号只能碰一些基础接口,高级接口调用时会提示权限不足或者直接返回空。所以在环境准备阶段,最好先确认自己账号的积分能覆盖要用的接口,不然后面所有脚本都会莫名其妙翻车。我一般会在项目的README里写清楚依赖的是哪些接口,方便下次直接查看。

2.2 第一个拉取日线数据的脚本

写第一个脚本不需要太复杂,目标就一个:拿到某只股票一段时间内的日线价格。TuShare Pro里最常用的是daily接口。这里用某个A股代码做示例,注意代码必须带交易所后缀,比如沪市代码以.SH结尾,深市代码以.SZ结尾。

import tushare as ts ts.set_token('你的token字符串') pro = ts.pro_api() df = pro.daily( ts_code='000001.SZ', start_date='20230101', end_date='20231231' ) print(df.head())

运行后你会看到返回一个DataFrame,字段包含trade_date、open、high、low、close、vol和amount。trade_date是字符串型的日期,vol是成交量,单位是手,amount是成交额,单位是千元。这几个单位我一开始没注意,后来算量比时差了一百倍,排查了半小时。

关于参数,有两点需要特别说明。第一,日期参数必须是纯数字格式YYYYMMDD,不能带-或/;第二,如果只传ts_code不传日期范围,接口默认返回最近一段时间的数据,但不同接口默认长度不一样,最好养成显式传入日期区间的习惯。返回的数据在默认情况下是未复权的,也就是说如果这一年里有分红送股,价格会出现跳空,直接拿来做收益计算会失真,后面复权部分会单独讲。

2.3 接口返回结构与pandas对接

TuShare Pro最大的好处是所有接口返回的都是pandas DataFrame,这跟Python金融数据分析的工作流天然吻合。但返回DataFrame不代表数据质量直接可用,做完初筛后通常还要做几步处理。

第一,trade_date是字符串,要转成datetime才能做时间序列;第二,数据默认升序排列,但部分接口可能顺序不稳定,最好显式排序;第三,把trade_date设为索引会更方便。这里给一个简单的处理模板:

import pandas as pd df['trade_date'] = pd.to_datetime(df['trade_date']) df = df.sort_values('trade_date') df = df.set_index('trade_date')

set_index之后,你可以直接用df.loc['2023-06-01']按日期切片,画图时也能直接使用索引刻度。如果你要同时操作多只股票,建议保留ts_code列,不要把它丢弃,因为后续合并数据时需要通过代码区分标的。

还有一个容易忽略的细节:接口返回的字段顺序和类型在不同版本可能略有差异。比如amount可能是浮点数,也可能因为某些数据缺失变成NaN。接入阶段我一般会用df.info()检查字段类型和缺失值,再决定填充策略。不要假设所有股票的数据都一样完整。

这一小节的核心就一句话:让数据在进入分析前统一索引、统一类型、统一单位。别小看这几步,我之前跳过这些直接算收益率,结果一个数据对齐bug让整个回测结果都废了。

3. 核心接口拆解:股票列表、交易日历、复权因子与实时快照

3.1 股票列表与交易日历:搭建全市场数据底座

做量化研究第一步不是拉K线,而是知道这个市场里有哪些股票、哪些日子交易。TuShare Pro里对应两个接口:pro.stock_basic和pro.trade_cal。

stock_list = pro.stock_basic( exchange='', list_status='L', fields='ts_code,name,industry,list_date' ) print(stock_list.head()) cal = pro.trade_cal( exchange='SSE', start_date='20230101', end_date='20231231' ) trade_days = cal[cal['is_open'] == 1]['cal_date']

stock_basic的list_status参数有几个值很关键:L代表当前在市,D代表已退市,P代表暂停上市。如果你做的是全历史回测,就不能只看当前在市股票,否则会有幸存者偏差。所以我才强调先拉全量列表,再按list_date筛选。trade_cal接口里is_open为1代表当天开市,为0代表休市。用这个字段去生成交易日期序列,比用pd.bdate_range硬算要靠谱得多,因为春节和国庆的休市安排是动态的。

我一般会把这些结果缓存到本地CSV,每周刷新一次。后续所有增量拉取任务都对照这份日历启动,不会在休息日做出无效请求。

3.2 复权因子与价格计算:前复权、后复权与常见误区

股票除权除息会导致价格在某一交易日突然变化,但真实投资者并没有凭空亏损或盈利。为了衡量真实收益率,需要把历史价格统一到一个基准上,这就是复权。TuShare Pro提供pro.adj_factor接口返回复权因子,通常每个交易日一个因子。

df_daily = pro.daily(ts_code='000001.SZ', start_date='20220101', end_date='20231231') df_adj = pro.adj_factor(ts_code='000001.SZ', start_date='20220101', end_date='20231231') df = df_daily.merge(df_adj, on='trade_date', how='left') df['close_hfq'] = df['close'] * df['adj_factor'] first_factor = df['adj_factor'].iloc[0] df['close_qfq'] = df['close'] * df['adj_factor'] / first_factor

这段代码做了什么?先把日线行情和复权因子按日期对齐,然后用当日因子直接乘原始收盘价得到后复权价格;要得到前复权价格,需要把当日因子除以这段区间第一天的因子,再乘原始收盘价。两者都是时间序列的相对价格,绝对值没有意义,意义在于保证了除权日前后收益率的连续性。

常见误区是只取close * adj_factor当作前复权。如果是从中间某天开始取数据,第一天的因子不是1,直接乘会得到一套被缩放过的价格。所以必须归一化。另外,复权因子的对齐要注意merge时两边trade_date类型一致,如果一边是从数据库读出来变成了别的类型,会产生很多null值。我会在原数据导入时另外保存一份原始价格字段,不直接覆盖close,万一复权逻辑写错还能恢复。

3.3 实时行情快照:盘中验证信号的轻量方案

TuShare不仅提供历史数据,还有实时行情快照接口。老版本有ts.get_realtime_quotes,可以直接拉取当前盘口报价,适合用来做盘中监控和信号触发验证。Pro部分接口也支持实时数据,但积分要求更高,普通账号未必能调用。

import tushare as ts df_rt = ts.get_realtime_quotes(['000001', '600000']) df_rt['price'] = pd.to_numeric(df_rt['price'], errors='coerce') print(df_rt[['code', 'name', 'price', 'time']])

这里需要注意,老接口返回的price列其实是字符串,直接运算会报错,所以先用pd.to_numeric转成浮点数。code参数传6位数字即可,不用带后缀。实时快照本质是当下时点的一次查询,不是连续推送,不能当高频数据源用。

我在实际项目里一般用它做回测信号盘中的抽样验证,比如策略在开盘后5分钟发出买入信号,我就用这个接口拉一下当前的报价,和回测系统的假设做对比。实时接口调用频率很敏感,不要放在循环里几秒钟刷一次,容易被临时限制。建议每次调用后至少间隔3到5秒。

3.4 全量数据拉取与分页限制

当你需要拉取某只股票从上市首日到今天的全部日线数据时,pro.daily接口对单次返回行数有限制,通常单次请求最多返回几千行。如果区间跨越十几年,就需要按日或者按年份分片请求,不能一把梭直接拉。

def fetch_daily_range(pro, ts_code, start_date, end_date): frames = [] for year in range(int(start_date[:4]), int(end_date[:4]) + 1): y_start = f'{year}0101' y_end = f'{year}1231' df_year = pro.daily(ts_code=ts_code, start_date=y_start, end_date=y_end) if df_year is not None and not df_year.empty: frames.append(df_year) return pd.concat(frames)

按年份拆分的好处是每次请求的数据量可控,不容易触发行数上限,也方便在异常时定位是哪一年的数据出了问题。如果按照年份分别存储,之后做增量更新时可以直接从拆分点生效,逻辑更干净。

4. 数据落地与增量更新:别把全量数据天天重拉

4.1 本地存储结构设计

很多项目一开始是直接调用接口取数,然后落地成单个CSV,简单粗暴。但数据量上来后,读写速度和灵活性都不够。我常用的方案是按层分目录,基础信息单独存,行情数据按年份存,文件名为股票代码加后缀。

data/ calendar.csv stocks.csv daily/ 2023/ 000001.SZ.csv 600000.SH.csv

这种结构在拉取新数据时只需要追加对应年份的文件,回测时也可以只读某一年份,减少IO。如果未来数据量继续增长,可以过渡到DuckDB或ClickHouse,但目录结构可以保持一致,迁移成本很低。

4.2 增量更新脚本

增量更新的核心是记录上次更新到哪个交易日。最简单的方法是用本地文件的最新日期作为断点。比如某股票CSV最后一行的日期是2023-12-15,那么下次就从日历里找出这个日期之后的下一个交易日,只拉这段区间的数据。

def update_daily(pro, ts_code, last_date, end_date): cal = pro.trade_cal(exchange='', start_date=last_date, end_date=end_date) trade_days = cal[cal['is_open'] == 1]['cal_date'].tolist() frames = [] for day in trade_days: df_day = pro.daily(trade_date=day, ts_code=ts_code) if df_day is None or df_day.empty: continue frames.append(df_day) df_new = pd.concat(frames) return df_new

这里用trade_date=day的方式逐日拉取,每次请求日线数据量很小。缺点是请求次数多,所以一定要配合限流和重试机制。如果last_date传了空值,那就走全量初始化,按年份分片拉取。把这两个路径分开,维护成本会清晰很多。

4.3 数据完整性与停牌处理

增量更新后,必须检查有没有漏数据。常见做法是用交易日历和已存数据做差集,找出该有却没有的日期。注意停牌股票在停牌日不会返回数据,这是合理的缺失,不能当作错误。但你需要在库里留下标记,否则回测时会把停牌日当作未开盘处理,导致交易信号在不能交易的日子下单。

missing_days = set(trade_days) - set(df_existing['trade_date']) # 需要区分停牌和接口失败,我一般会增加 is_suspended 字段

这里我不建议直接删除缺失日,而是增加一列is_suspended,分别标记停牌和接口失败。这样后续模型可以明确知道当天是否可交易。数据校验不要放在更新之后才做,而是每次更新完立刻执行一小段断言,比如最新一天必须是今天或上一个交易日,否则就报警。

4.4 读取时去重与排序

增量更新过程中可能因为重试导致同一日期写入了多次。所以在读取数据时,我通常会统一做一次去重和排序。

df = pd.read_csv('data/daily/2023/000001.SZ.csv') df = df.drop_duplicates('trade_date', keep='last') df = df.sort_values('trade_date').reset_index(drop=True)

drop_duplicates基于trade_date去重,keep='last'保留最后一次写入的结果。这样做能容忍更新脚本的幂等性问题,不用在写入端强制加锁。但如果多次写入的数据内容不同,最好还是在写入时就使用先读后写的合并逻辑,别把去重当成万能后悔药。

5. 避坑与排查:TuShare使用中的5个典型翻车点

5.1 接口返回空列表,但代码看上去没问题

有时候调用pro.daily(ts_code='000001.SZ', start_date='20230101', end_date='20231231')返回一个空DataFrame,打印出来只有列名没有行。你以为代码写错了,反复试,换股票代码也还是空,或者只有部分股票有数据。

这种情况超过一半是积分权限问题。TuShare Pro接口对不同积分等级开放不同权限,未达标的接口不会报错,而是安静地返回空结果。另一个可能是指定日期区间不在该股票上市区间内,比如某股票2022年才上市,拉2020年的数据自然为空。

解决建议:先用pro.stock_basic验证代码和上市日期;再在官网查看账号权限,比对要调用的接口。我一般会在代码里加一个检查函数:如果返回为空,把股票代码和日期区间写入日志,而不是静默通过。这样批量任务里即使单只股票失败也能事后追溯。

5.2 日期参数带横杠导致的玄学失败

同一个代码,日期写成'2023-01-01'返回空或者报错,写成'20230101'就正常。有人觉得是接口不稳定,其实是参数格式导致的玄学失败。

TuShare Pro的日期参数是定制化约定,统一使用YYYYMMDD字符串。它没有像pandas那样宽松的解析能力,横杠或斜杠都会被当成非法输入,有些接口直接返回空,有些抛异常。另外,trade_date字段返回的也是无横杠字符串,写入数据库时如果没转格式,下次查询就会吃哑巴亏。

解决建议:写一个日期转换辅助函数,所有入口统一处理:

def fmt_date(d): if isinstance(d, str) and '-' in d: return d.replace('-', '') if isinstance(d, (pd.Timestamp, datetime)): return d.strftime('%Y%m%d') return str(d)

把这个函数用到所有start_date、end_date参数上,能让整个项目少踩一半的坑。

5.3 前复权价格算错:负数和放大问题

复权后计算收益率,发现某些股价变成负数,或者与行情软件的前复权价格差异巨大。原因出在前复权公式上。

前复权不是简单的乘法。很多人知道要用复权因子,但不知道需要取区间第一天因子做归一化。如果直接close * adj_factor,得到的是后复权价格,数值可能远大于原始价格;如果在区间中间开始,first_factor取了全局最早值而不是区间第一行,结果也会整个偏移。

正确的做法是排序后按第一行因子归一化:

df['close_qfq'] = df['close'] * df['adj_factor'] / df['adj_factor'].iloc[0]

iloc[0]必须是在排序后的数据框上取,并且确保adj_factor没有空值。多只股票分别处理时,需要对每只股票单独计算,不能在同一个DataFrame里跨股票取第一个因子。

5.4 字段单位与字段名不一致

用vol直接当作股数算换手率,结果量级差了几十倍;或者老接口返回的字段和Pro接口不一致,导致拼接时报错。这是最常见的数据污染来源。

TuShare老接口和Pro接口的字段并不完全一致。Pro里vol单位是手,amount是千元。老接口里volume和amount的单位可能不同,字段命名也不同,比如老接口用代码列名,Pro用ts_code。

解决建议:在项目入口专门建立一个字段映射表,把不同来源的字段统一成内部标准。

FIELD_MAP = { 'ts_code': 'code', 'vol': 'volume', 'amount': 'amount', }

写入数据库前就完成重命名,分析代码里只用内部字段名。这样即使上游接口升级改字段,也只改一处映射。

5.5 循环批量请求触发限流与封禁

写一个循环批量拉取500只股票的日线,跑到约几十只时接口开始抛异常,提示请求太频繁,再跑就被临时封禁IP。

TuShare不同积分档位有每分钟和每天请求次数上限。批量脚本如果没有间隔控制,会在极短时间内发出大量请求,触发风控。免费档位的限制尤其严格,不只是表面写的每秒一次,实际阈值可能更低。

解决建议:在循环中加入随机sleep,比如time.sleep(random.uniform(0.3, 0.8)),同时给请求函数包一层带重试的装饰器,捕获异常后等更长间隔再重试。更稳妥的方案是用本地缓存,把历史数据落盘,每次更新只请求增量,而不是全量重拉。我自己写过一个简单装饰器,放在第6章供参考。

6. 进阶:把TuShare数据接进自己的量化回测管道

6.1 用一个装饰器统一处理限流与重试

作为进阶,我把日常用的限流重试装饰器分享在这里。它不解决积分问题,但能帮你把偶发失败降到最低。这个装饰器每次遇到异常就指数退避等待,然后重试,最多试三次。

import time import random from functools import wraps def retry_call(max_retries=3, base_wait=1.0): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception: if attempt == max_retries - 1: raise wait = base_wait * (2 ** attempt) + random.uniform(0, 0.5) time.sleep(wait) return None return wrapper return decorator @retry_call(max_retries=3) def fetch_daily_with_retry(pro, ts_code, trade_date): return pro.daily(ts_code=ts_code, trade_date=trade_date)

装饰器的解析:第一次失败等1 + random(0,0.5)秒,第二次等2 + random秒,第三次等4 + random秒。这样的随机抖动比固定间隔更不容易被风控识别成机器人请求。

6.2 数据对齐的最后一道工序

把前复权价格、日线行情和股票基础信息合并成一张宽表时,注意索引一致性。我用最终合并表时,严格按键去重,并且用assert检查行数与交易日历匹配。

assert len(df_final.index.unique()) == len(trade_days), "数据行数与交易日不一致"

这个断言写在数据管道的最后,能挡住大多数因增量更新遗漏而产生的脏数据。

从那以后,我每次新接一个数据源都会强制走一遍:先看字段单位,再算一遍复权,最后做增量校验。这三步做下来基本不会翻车。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 12:37:05

Ubuntu下SFTP服务端配置与实战:从权限隔离到断线续传

说个我自己遇到的场景。之前有个团队,内部文件分发一直用老式FTP,隔三差五出现传一半断掉、重传还得从头开始的问题,后来整体换到SFTP,运维才消停下来。什么叫SFTP?它不是一个独立安装的文件服务,而是基于S…

作者头像 李华
网站建设 2026/10/20 12:36:53

弹幕防遮系统实战:基于语义分割的像素级避让方案

弹幕防遮这个方向,我关注很久了。看视频开弹幕本来图个热闹,结果满屏飞字把演员脸挡得严严实实,体验直接归零。市面上大部分播放器要么弹幕全关,要么透明度调低硬看,都谈不上"聪明"。用深度学习语义分割把画…

作者头像 李华
网站建设 2026/10/20 12:36:26

开源AI编程教程被大模型推荐:从2万Star到被引用的方法论

1. 一个教程项目被大模型推荐,这件事为什么值得聊先交代背景。我维护了一套面向零基础开发者的 AI 编程入门教程,纯开源、纯文档,没有花过一分钱推广。前段时间有读者截图给我看,说在某个国产 AI 助手里问"AI 编程怎么入门&q…

作者头像 李华
网站建设 2026/10/20 12:35:59

从Tesla AI Day看自动驾驶技术栈:纯视觉、自研芯片与数据引擎拆解

1. 先聊聊为什么这场发布会在业内炸了锅2021年的Tesla AI Day,说实话,信息量比很多人预想的要大得多。本来以为就是例行展示一下自动驾驶进展,结果官方把从芯片到算法、从数据引擎到训练集群的整个技术栈全部摊开来讲了一遍。更关键的是&…

作者头像 李华
网站建设 2026/10/20 12:35:23

上下文模式:从代码、日志到AI辅助的实战指南

做技术这些年,我越来越觉得“上下文”这个词,是所有疑难杂症的根因。线上接口超时,翻日志只有一行timeout,没有请求ID、没有用户标识、没有调用链信息,你盯着屏幕只能干瞪眼;代码里一个函数忽然要拿用户身份…

作者头像 李华
网站建设 2026/10/20 12:33:26

身份证OCR识别API接入实战:Python与Java踩坑经验

直接对接身份证OCR识别API这件事,看上去就是“传一张图,拿一段JSON”,但真正落地的时候,坑比想象中多。图片方向反了、身份证号识别串位、有效期字段死活解析不对、并发一高就开始超时……这些我都踩过。这篇文章就围绕“身份证OC…

作者头像 李华