☰
TuShare实战:从安装配置到股票日线、复权与财务数据获取
2026/10/20 3:40:32 网站建设 项目流程

简介:TuShare是一个用于抓取中国股票历史数据的Python工具包,面向金融量化分析师与数据学习者,覆盖数据采集、清洗加工到存储的完整链路,可用于股票、期货等常见金融数据场景,大幅降低获取门槛。压缩包内含118个文件,整体仅4.91MB,结构紧凑:67个py脚本承载接口调用与处理逻辑,是最核心的可复用代码;14个rst与2个md文档说明使用方式,10个pdf可作参考,png/jpg图片和txt文件则辅助展示与配置。已有1407人学习下载,适合正在入门量化分析或需要快速搭建本地金融数据集的开发者。通过该包能系统了解TuShare的模块化设计、历史数据字段含义及本地存储思路,并直接复用脚本模板扩展自己的数据管道,相当于一份省时实用的开源参考。

1. 手里没有可靠行情数据,量化分析就是空转

做股票数据相关开发的人,大概率都经历过这种尴尬:免费行情接口要么字段残缺,要么封IP封得莫名其妙;用爬虫去蹭网页数据,对方一改版,自己写的解析代码瞬间报废。TuShare这个工具解决的正是这个痛点——它是面向 Python 生态的中国股票历史数据抓取工具,通过统一的接口把日线、分钟线、财务指标、交易日历这些数据从数据源拉下来,整理成 pandas DataFrame 直接交付。这篇文章会从安装配置讲到数据获取实战,再到踩坑排查,全程用可复现的代码说话。适合刚入门量化分析、做数据清洗,或者打算把股票数据接进本地数据库的从业者。

2. 环境搭建:从 pip 安装到跑通第一张日线表

2.1 选型理由:与其自建爬虫,不如用统一接口

早期我做股票数据抓取时,踩过一个很大的坑:用 requests 直接请求公开网页接口,解析 HTML 表格,一天能跑一万条就不错了,还经常因为请求头不够“像浏览器”被拒绝服务。后来换了 TuShare,它把数据获取的脏活累活都封装好了,返回值直接是 pandas DataFrame,不用自己拼接字符串,也不用考虑网页结构变化。

选型时还要注意一点:TuShare 分两个版本,基础版是免费开源的 tushare 库,另外一个 Pro 版本需要注册获取 token。基础版能抓部分历史日线数据,但字段和频率都受限制;做真正的量化分析、财务数据加工,还是得走 Pro 接口。本文后面的代码全部基于 Pro 接口,因为它是数据从业者最常用的路径。

还有一点值得说:不要把 TuShare 当成唯一的“永备方案”。它给的是标准化的历史数据,如果你要做实时 tick 级回放,或者毫秒级盘口数据,它就不是为那种场景设计的。选型要先看边界,TuShare 的优势在日线、分钟线、财务基本面这种“低频但量大”的数据场景。

2.2 安装与 token 配置:三个步骤拿到访问凭证

安装本身很简单,但有几个细节会影响你后面是否顺利。先执行安装:

pip install tushare

如果是国内网络环境,建议加上国内镜像源,否则下载速度会比较慢,安装完可以再用下面的命令确认版本:

pip show tushare

安装完成后,还需要在 tushare.pro 注册一个账号,然后在个人主页里找到 token 字符串。拿到 token 后有两种配置方式:一种是每次初始化时手动传入,另一种是通过环境变量或配置文件固定。我个人更推荐后者,因为代码里不出现明文 token,容易被同事审查时揪出来。

import tushare as ts # 方式一:每次初始化传入 token(不推荐长期用) pro = ts.pro_api('你的token字符串') # 方式二:通过环境变量读取(推荐,适合放在服务器或多人协作) import os token = os.getenv("TUSHARE_TOKEN", "你的备用token") pro = ts.pro_api(token)

这段代码的意图很简单:先导入 tushare,再用 pro_api 方法初始化 Pro 接口客户端。方式二的优势是 token 不直接写在脚本里,后续做版本管理时不会把凭证提交到仓库。token 字符串是账号维度的权限凭证,不同等级的账号能调用的数据接口、每分钟的请求频次都不一样,所以建议先测试你的账号能访问到哪些接口,再决定数据方案。

2.3 第一个请求:pro.daily 的字段与返回结构

初始化完成后,立刻就可以抓第一张表。下面是拿“某只股票”日线行情的最基础用法:

# 抓取指定股票最近一段时间的日线数据 df = pro.daily( ts_code="000001.SZ", # 股票代码,深交所带.SZ后缀 start_date="20240101", # 起始日期,格式YYYYMMDD end_date="20240131" # 结束日期 ) print(df.head()) print(df.dtypes)

有两点需要解释。第一,ts_code 是 TuShare 里面的核心主键,必须带交易所后缀,上交所是 .SH,深交所是 .SZ,北交所是 .BJ,写错后缀就拿不到数据。第二,返回的 DataFrame 里有 open、high、low、close、pre_close、change、pct_chg、vol、amount 这些字段,vol 的单位是手,amount 的单位是千元,这个细节搞错会让后面的收益率计算全部不可信。

跑通这一段后,你的数据链路就通了,后面的所有复杂抓取都是在这个基础上换参数、换接口。如果这一步报错,优先检查网络、token 权限和代码后缀,不要急着换方案。

3. 历史行情数据获取:日线、复权与分钟线

3.1 日线数据:pro.daily 的核心参数与字段说明

pro.daily 是历史行情最常用的接口,但有几个参数的选择会直接影响取数质量。它支持两种按时间取数的写法:一种按 trade_date 直接指定某一天,所有股票当天的数据一次性返回;另一种按 ts_code 指定某只股票的时间区间。两种写法的用途完全不同。

# 按日期取全市场当日行情,适合做截面分析 all_stock_df = pro.daily(trade_date="20240115") print(all_stock_df.shape) # 按股票取历史区间,适合做个股时序分析 single_stock_df = pro.daily( ts_code="000001.SZ", start_date="20231201", end_date="20240131" ) print(single_stock_df.shape)

trade_date 模式做横截面分析很高效,比如要抓“2024年1月15日全市场的收盘价”,一次请求就完成。但它有个隐含限制:单次返回的行数有上限,全市场五千多只股票会被分页截断,需要循环多次按页码取全。按个股取区间时,如果区间太长也会触发分页,需要接一个循环,用 offset 把分页数据累积到同一个 DataFrame 里。

常见的做法是写一个分页封装函数。我一般会先确认接口最多返回多少行,再按边界计算循环次数,避免漏数据:

def fetch_all_pages(pro, ts_code, start_date, end_date): pages = [] offset = 0 while True: part = pro.daily( ts_code=ts_code, start_date=start_date, end_date=end_date, limit=5000, # 单次最多返回5000行,具体看当前账号权限 offset=offset # 从第offset行开始取 ) if part is None or part.empty: break pages.append(part) offset += 5000 if len(part) < 5000: break return pd.concat(pages, ignore_index=True)

这段分页代码的逻辑:每次取 5000 条,如果返回的行数小于 5000,说明已经取到最后一页,终止循环;否则 offset 增加 5000 继续取。参数 limit 和 offset 是控制分页的两个关键,limit 设置单页大小,offset 控制起始位置。实际操作中要注意:不同积分等级的账号能用的单次 limit 上限不同,如果你的账号上限是 1000,硬设 5000 会被拒绝,建议先小批量测试。

拿到日线数据后,还要检查一个问题:数据的精度单位。close 字段保留两位小数,vol 是手数,amount 是千元,单位不统一时,算成交额必须乘 1000。这是一个经常被忽略的细节,可能导致最终的流动性指标偏差。

3.2 复权因子:pro_bar 的 adj 参数与除权除息处理

做量化分析的人都知道“不复权的收盘价没法直接算收益率”。因为股票除权除息后,价格会突然断裂,不复权的连续收益率会有一个虚假的向下跳空。TuShare 里专门提供了另一个接口 pro_bar 做复权行情。

# 前复权日线 qfq_df = ts.pro_bar( ts_code="000001.SZ", adj="qfq", # 前复权 start_date="20240101", end_date="20240131" ) # 后复权日线 hfq_df = ts.pro_bar( ts_code="000001.SZ", adj="hfq", # 后复权 start_date="20240101", end_date="20240131" )

关于 qfq 和 hfq 的区别:前复权保持最新价格不变,把历史价格往下调整,适合看最近的价格走势和回测最近一段时间的策略;后复权保持历史价格不变,把最新价格往上抬,适合做长期收益率计算,因为它不会因为新数据加入而改变历史价格序列。选哪一种,取决于你的策略计算逻辑。

实际使用中,pro_bar 还支持 freq 参数,可以取日线、周线、月线,以及 1、5、15、30、60 分钟线,非常方便。但它也有一个和 pro.daily 不一样的地方:pro_bar 更贴近散户视角,返回字段里没有 pre_close、change 这种直接派生字段,需要自己用 shift 去算。

需要注意复权因子的一个隐藏属性:数据源公布的是截止当前时刻的复权因子。如果做回测,你在今天取的 qfq 历史数据,和一年前取到的同一段历史数据,数值会不一样。因为新的除权事件会改变此前所有日期的前复权价格。这个特性在回测中如果不处理,就会出现“未来函数”的错觉,后面避坑章节会重点讲。

3.3 分钟线数据:freq 参数与本地拼接

分钟线是做日内策略和开盘行为的常用数据。TuShare 的分钟线通过 pro_bar 接口的 freq 参数直接获取,但它和日线比有几个不同:第一,分钟线不是随时能访问的,接口权限和积分配额会限制可回溯的历史长度;第二,单次请求最多只能返回一定天数或一定条数的数据,想拉一整年的分钟线,必须按日期分段循环再拼接。

下面是一个按日期循环获取 5 分钟线的示例:

import pandas as pd def fetch_minute_bars(pro_wrapper, ts_code, start_date, end_date, freq="5"): parts = [] day_list = pd.date_range(start_date, end_date, freq="D") for day in day_list: day_str = day.strftime("%Y%m%d") # pro_bar 里 freq 的格式:1/5/15/30/60 分钟 part = pro_wrapper.pro_bar( ts_code=ts_code, freq=f"{freq}min", start_date=f"{day_str} 09:30:00", end_date=f"{day_str} 15:00:00" ) if part is not None and not part.empty: parts.append(part) return pd.concat(parts, ignore_index=True)

这个循环做的事情:把日期范围展开成每一天,再对每一天单独发起一次请求,最后用 pd.concat 拼成一个完整的 DataFrame。为什么要按天分段?因为分钟线接口单次请求的交易日期范围有限制,跨太多天的请求会被截断或直接报错。freq 参数的格式是 "1min"、"5min"、"15min"、"30min"、"60min",注意在 pro_bar 里写作${freq}min,而不是单独的“5”或者“5m”。

分钟线的字段和日线类似,但 volume 单位同样是手。拼接完成后要做好去重和排序,因为分段请求的边缘处可能出现重复行,时间戳也有可能错位。按 trade_time 排序以后,用 drop_duplicates 处理,再重置索引,才算干净。

3.4 交易日历:用 trade_cal 判断哪些天真的有数据

拿到日线后,很多人会犯一个错误:以为自然日就是交易日。实际上 A 股有节假日调休,周末有时也会成为交易日。TuShare 提供了交易日历接口 trade_cal,专门用来判断某一天是否开盘。

cal = pro.trade_cal( exchange="SSE", start_date="20240101", end_date="20240201" ) print(cal[["cal_date", "is_open", "pretrade_date"]])

is_open 字段是 1 表示交易日,0 表示休市。pretrade_date 则给出该日期前一个交易日的日期,这个字段在做涨跌停判定和“上一交易日数据对齐”时非常有用。实际使用中,我会把交易日历先存进本地,再在抓取循环里过滤交易日,避免在非交易日发起无效请求,白白消耗接口额度。

4. 从行情到基本面:财务与交易状态数据实战

4.1 三大报表接口:income、balancesheet、cashflow

行情数据只能告诉你价格发生了什么,财务数据才能告诉你公司赚了什么。TuShare 的财务接口是分报表拆开的,分别是利润表 income、资产负债表 balancesheet、现金流量表 cashflow。取数逻辑比较接近,核心参数是 ts_code 和 period。

# 利润表 income_df = pro.income( ts_code="000001.SZ", period="20231231", # 报告期 fields="ts_code,ann_date,f_ann_date,end_date,revenue,operate_profit,total_profit,n_income_attr_p" ) # 资产负债表 balance_df = pro.balancesheet( ts_code="000001.SZ", period="20231231", fields="ts_code,end_date,total_assets,total_liab,total_hldr_eqy_exc_min_int" ) # 现金流量表 cashflow_df = pro.cashflow( ts_code="000001.SZ", period="20231231", fields="ts_code,end_date,netcash_operate,netcash_invest,netcash_finance" )

这三个接口的字段命名风格偏向数据库底层命名,比如 n_income_attr_p 代表“归属于母公司所有者的净利润”,netcash_operate 是经营活动产生的现金流量净额。第一次接触的人很容易被字段名劝退,我的建议是每次取数时都显式指定 fields,不要全字段一次性拖回来,否则数据量大不说,字段名记不住还得反复查文档。

有一个细节:period 参数表示报告期,比如 20231231 是 2023 年年报,20240331 是一季报。同一个报告期会有多条数据,因为公司会发布业绩预告、快报、正式财报等多个版本,它们会分布在不同的 ann_date 上。取数时如果不指定这些版本差异,可能会拿到重复记录,需要用 ann_date 做排序后取最新版本。

4.2 财务指标与估值:fina_indicator、daily_basic

如果说三大报表是“源数据”,那 fina_indicator 就是别人帮你算好的指标。它直接返回 ROE、毛利率、净利率、资产负债率等常用指标,省去了自己计算的麻烦。

# 财务指标 ind_df = pro.fina_indicator( ts_code="000001.SZ", period="20231231", fields="ts_code,end_date,roe,netprofit_margin,grossprofit_margin,debt_to_assets" ) # 每日基本面指标 basic_df = pro.daily_basic( ts_code="000001.SZ", start_date="20240101", end_date="20240131", fields="ts_code,trade_date,close,pe,pb,turnover_rate" )

fina_indicator 里的字段是比率型数据,单位一般是百分比,roe 返回的是 12.5 这种数字而不是 0.125,这个细节在做数据标准化时要特别注意。daily_basic 则把每日的市盈率、市净率、换手率这些交易派生的估值指标直接给出来,特别适合做截面选股。它的 trade_date 字段是 8 位日期字符串,如果想要日期类型,需要自己 to_datetime 转换。

财务数据和行情数据在时间对齐上有天然矛盾:财报是季度频率,行情是日频。最简单的方式是 fifo 对齐——每一条日线数据,往前找最近一个已经公布的报告期,把这个报告期的财务数据并过来。这种对齐方式很多量化框架的 factor 库都在用,写起来也不复杂,核心还是先排序再 merge_asof。

4.3 数据对齐:pandas merge 的两种典型姿势

拿到行情和财务两套数据后,最常见的操作是按股票和日期把它们拼到一张表里。这里有两种典型姿势,各有适应场景。

# 姿势一:横向拼接,按股票和日期等值合并 merged = pd.merge( daily_df, basic_df, on=["ts_code", "trade_date"], how="left" ) # 姿势二:按时间就近合并,解决财报频率不齐的问题 daily_sorted = daily_df.sort_values("trade_date") fina_sorted = ind_df.sort_values("end_date") aligned = pd.merge_asof( daily_sorted, fina_sorted, left_on="trade_date", right_on="end_date", by="ts_code", direction="backward" )

第一种姿势适用于两个接口都有相同日期的字段,比如 pro.daily 和 pro.daily_basic,它们都有 trade_date 且是同一频率,直接 merge 就不会错位。第二种姿势用来处理财报这种低频数据和日线高频数据之间的对齐,direction="backward" 表示对每一条日线记录,取“小于等于当天”的最新财报。这种对齐方式解决了财报发布日滞后的问题,也避免把未来数据带到历史截面里。

对齐之后要检查一次列名冲突,尤其是两边都有 close 或者 amount 字段的情况。pandas 合并后会自动生成 close_x、close_y 这样的后缀,务必在合并后手动重命名,否则后续算因子时很容易引到错误的列。

5. 避坑与常见问题:被积分、限流和复权折磨后的五条记录

5.1 现象:pro.daily 突然报“没有权限”

第一次调用财务接口时,报错信息类似“抱歉,您没有访问该接口的权限”。刚开始以为是代码写错了,反复检查 ts_code 和 fields,还是没有头绪。后来才发现,这是账号积分等级不够,某些接口(尤其是财务和分钟线)对权限有硬性门槛,报错和代码无关。

原因:TuShare Pro 的接口按积分分层开放,基础积分只能访问日线等基础接口,财务、分钟线这类接口有更高的积分要求。 解决:去官网看当前账号的积分等级,对照接口文档确认该接口的最低积分要求。短期方案是先用基础接口顶替,长期方案是在账号后台完成积分任务提升等级。如果只是个人做研究,不要盲目付费充积分,先把基础数据用熟。

5.2 现象:复权价出现“未来函数”,回测收益率好得离谱

用前复权价格算策略收益,回测曲线笔直向上,但实盘完全不是那么回事。一开始还以为找到了圣杯,后来把复权因子拉出来一看,发现最后几天的因子被“修正”过,导致历史价格被同步调整。

原因:前复权以最新价格为锚点,新的除权除息事件发生后,整个历史价格序列都会变化。回测时如果每天拉取一次数据,不同时间点取到的同一历史日期价格不同,等于把未来信息带入了历史。 解决:做历史回测时,不要用每日动态更新的 qfq 数据,而是用不复权价格自己乘以当时的复权因子,或者直接固定某一时间点一次性拉取全部历史数据,之后不再更新。我在本地缓存里会把复权因子单独存一张表,回测时用“历史快照”的因子做计算,而不是每次都请求最新因子。

5.3 现象:分钟线本地拼接后,K 线图形缺口很多

按天循环抓 5 分钟线,拼完后发现某些交易日的 K 线比其他日子少了十几根,一开始以为是接口漏数据,就加大重试次数,仍然缺。最后对比交易日历才发现,那些“缺口”的日子有的是半天的交易时段,有的则根本不是交易日。

原因:用自然日循环去请求分钟线,没有先过滤交易日;同时部分半天交易的行情本身就少,这是数据源的真实状态。 解决:在循环里用 trade_cal 的 is_open 先过滤,再对每个交易日请求分钟线。拼接完成后统计每天的 bar 数,半天交易日允许少于标准数量,但全缺失的日期要拉警戒线,人工检查是否停牌或请求异常。

5.4 现象:停牌日“没数据”不等于“数据缺失”

某只股票某一天在日线接口里查不到,第一反应是补数据,反复用不同参数重试,结果还是一样。后来查了公司公告才知道,那只股票当天停牌了,停牌日本来就没有行情。

原因:TuShare 日线只返回真实交易记录,停牌日不出记录,这是接口的语义规则,和一般数据库“必须有一行”的习惯不同。 解决:判断一只股票某天是否有行情,不能只看日线接口返回为空,要继续配合 suspend_d 接口或公告信息确认。如果是回测框架,要在数据预处理阶段为停牌日补一条标记记录,否则因子计算时会漏掉持仓状态的变化。

5.5 现象:抓全市场数据时被限流,请求直接失败

写循环抓全市场日线,跑到一半接口开始报“频率限制”,脚本崩溃,前面抓的数据也攒在内存里没落盘,损失惨重。

原因:TuShare Pro 的单分钟请求次数有限额,超过了就报错。常见的错误写法是 for 循环里不加间隔,一口气把几千次请求打出去。 解决:在循环里加 sleep 控制节奏,比如每次请求后等 0.2 秒,把频率控制在接口允许范围内。同时把每页数据及时写入本地文件或 SQLite,即使中途断掉,也能从上次断点继续,不需要从头抓。

6. 把 TuShare 接进本地数据管道:缓存、增量更新与数据验证

6.1 设计一个极简本地缓存:SQLite 会话级存储

频繁请求远程接口不是好习惯,既慢又容易被限流。我会把抓下来的数据落进本地 SQLite,后续所有分析都从本地读取。用 pandas 自带的 to_sql 就可以一步写入。

import sqlite3 from pandas import DataFrame def save_to_sqlite(df: DataFrame, table_name: str, conn): df.to_sql( name=table_name, con=conn, if_exists="append", index=False )

这段代码把 DataFrame 追加写入指定表。if_exists="append" 表示表存在时追加,而不是覆盖。配合唯一索引去重,就能做到“只增不减”。注意 to_sql 默认不会去重,每次请求前要先删掉重复记录,或者用数据库层级的 unique index 兜底。

6.2 增量更新:从最新日期继续抓

数据不是一次性抓完就结束了,每天开盘前要做增量更新。增量更新的关键是记住上次抓到的最大日期,从这个日期开始抓,而不是每次重头来。

def get_latest_date(conn, table_name): sql = f"SELECT MAX(trade_date) FROM {table_name}" latest = pd.read_sql(sql, conn).iloc[0, 0] return latest if latest is not None else "20240101"

这段代码从本地表里取出最大的 trade_date,作为新的抓取起点。第一次运行时表为空,就回退到最初的起始日期。增量更新能大幅减少请求次数,也避免重复抓取造成的额度浪费。备份时把 SQLite 文件直接复制走就行,比 CSV 目录管理方便得多。

6.3 数据验证:用普通均线策略做正确性检查

数据进库后,要验证它到底能不能用。我的习惯是拉一段日线数据,快速算一个双均线策略,看看信号是否合理,数据是否连续。如果策略输出全是极端值或乱跳,那大概率是数据对齐或复权出了问题。

import pandas as pd df = pd.read_sql("SELECT trade_date, close FROM daily_data WHERE ts_code='000001.SZ' ORDER BY trade_date", conn) df["ma5"] = df["close"].rolling(5).mean() df["ma20"] = df["close"].rolling(20).mean() df["signal"] = (df["ma5"] > df["ma20"]).astype(int) print(df.tail(10))

这段代码用 rolling 函数算 5 日和 20 日均线,再比较两者大小生成持仓信号。如果 close 含空值或日期不连续,rolling 会把缺口算进去,导致 signal 出现连续相同值或多处突变。这是一个快捷的数据质量探针,不用复杂的回测框架,就能看出本地数据是否可靠。从那以后,我每次更新完数据,都强制走一遍这段验证脚本,确认信号分布合理才继续做分析,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询