简介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_code000001.SZ, # 股票代码深交所带.SZ后缀 start_date20240101, # 起始日期格式YYYYMMDD end_date20240131 # 结束日期 ) 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_date20240115) print(all_stock_df.shape) # 按股票取历史区间适合做个股时序分析 single_stock_df pro.daily( ts_code000001.SZ, start_date20231201, end_date20240131 ) 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_codets_code, start_datestart_date, end_dateend_date, limit5000, # 单次最多返回5000行具体看当前账号权限 offsetoffset # 从第offset行开始取 ) if part is None or part.empty: break pages.append(part) offset 5000 if len(part) 5000: break return pd.concat(pages, ignore_indexTrue)这段分页代码的逻辑每次取 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_code000001.SZ, adjqfq, # 前复权 start_date20240101, end_date20240131 ) # 后复权日线 hfq_df ts.pro_bar( ts_code000001.SZ, adjhfq, # 后复权 start_date20240101, end_date20240131 )关于 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, freq5): parts [] day_list pd.date_range(start_date, end_date, freqD) 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_codets_code, freqf{freq}min, start_datef{day_str} 09:30:00, end_datef{day_str} 15:00:00 ) if part is not None and not part.empty: parts.append(part) return pd.concat(parts, ignore_indexTrue)这个循环做的事情把日期范围展开成每一天再对每一天单独发起一次请求最后用 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( exchangeSSE, start_date20240101, end_date20240201 ) 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_code000001.SZ, period20231231, # 报告期 fieldsts_code,ann_date,f_ann_date,end_date,revenue,operate_profit,total_profit,n_income_attr_p ) # 资产负债表 balance_df pro.balancesheet( ts_code000001.SZ, period20231231, fieldsts_code,end_date,total_assets,total_liab,total_hldr_eqy_exc_min_int ) # 现金流量表 cashflow_df pro.cashflow( ts_code000001.SZ, period20231231, fieldsts_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_code000001.SZ, period20231231, fieldsts_code,end_date,roe,netprofit_margin,grossprofit_margin,debt_to_assets ) # 每日基本面指标 basic_df pro.daily_basic( ts_code000001.SZ, start_date20240101, end_date20240131, fieldsts_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], howleft ) # 姿势二按时间就近合并解决财报频率不齐的问题 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_ontrade_date, right_onend_date, byts_code, directionbackward )第一种姿势适用于两个接口都有相同日期的字段比如 pro.daily 和 pro.daily_basic它们都有 trade_date 且是同一频率直接 merge 就不会错位。第二种姿势用来处理财报这种低频数据和日线高频数据之间的对齐directionbackward 表示对每一条日线记录取“小于等于当天”的最新财报。这种对齐方式解决了财报发布日滞后的问题也避免把未来数据带到历史截面里。对齐之后要检查一次列名冲突尤其是两边都有 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( nametable_name, conconn, if_existsappend, indexFalse )这段代码把 DataFrame 追加写入指定表。if_existsappend 表示表存在时追加而不是覆盖。配合唯一索引去重就能做到“只增不减”。注意 to_sql 默认不会去重每次请求前要先删掉重复记录或者用数据库层级的 unique index 兜底。6.2 增量更新从最新日期继续抓数据不是一次性抓完就结束了每天开盘前要做增量更新。增量更新的关键是记住上次抓到的最大日期从这个日期开始抓而不是每次重头来。def get_latest_date(conn, table_name): sql fSELECT 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_code000001.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 出现连续相同值或多处突变。这是一个快捷的数据质量探针不用复杂的回测框架就能看出本地数据是否可靠。从那以后我每次更新完数据都强制走一遍这段验证脚本确认信号分布合理才继续做分析希望帮到你。本文还有配套的精品资源点击获取