☰
Tushare数据接口避坑指南:从权限到复权的完整解析
2026/9/29 19:49:14 网站建设 项目流程

1. 权限与额度:还没开始写代码就可能卡住的坑

很多人第一次用Tushare,第一印象是“官网写得挺清楚”,结果一跑代码就报错。那种感觉我太熟了。我第一次调Tushare接口的时候,刚把pro = ts.pro_api()写完,满心欢喜地执行pro.daily(ts_code='000001.SZ'),结果直接给我弹了一行抱歉,您没有访问该接口的权限。我当时还以为是网络问题,换了好几个环境重试,最后翻了一圈文档才发现是积分不够。

1.1 读接口权限报错的关键信息

Tushare底子是免费开放的,但不同接口有不同积分门槛。积分不够时,报错信息其实已经写得很明确了,问题在于很多人一看到“抱歉”两个字就直接放弃了,根本没细看后面的提示。实际上,这类报错一般会带上code或msg字段,常见的有2001、40002这类错误码。意思是:你的账号权限等级没到,临时额度也没了,或者token配置有问题。

排查这类问题,先把官方文档的“积分和权限”页面翻出来,对照自己账号的积分值去对比接口要求。我踩过的一次比较典型的坑是:用pro.stock_basic()这个基础接口时,老版本不需要多少积分,但后来官方调整了规则,要求至少有120积分才能稳定调用。这类调整文档里更新过,但多数人根本不会主动回去看。后来我养成了一个习惯:每次大版本更新,先把__version__打出来,再去官网看一下接口文档有没有变更说明。

1.2 免费额度与调用频率的隐藏规则

除了接口权限,另一个容易让人困惑的是调用频率。Tushare免费版对单次接口访问是有频控的,比如每分钟最多调用多少次,超了就会报频率限制之类的错误。我看到很多初学者在循环里逐只股票拉日线数据,拉到一半突然批量报错,然后就开始怀疑是数据坏了。其实不是,就是频控触发了。

解决频控问题,最朴素的做法是在每次请求之间加一个sleep,比如time.sleep(0.2)。但更体面的做法是:拿到数据后,直接按股票代码循环拉取全部历史,不需要频繁往接口打。每次调用前用pro.trade_cal()判断一下交易日历,也可以减少无效请求。还有个小技巧:如果只是研究用,完全可以把数据一次性拉到本地存成csv或者parquet,后续分析都从本地读,别老去折腾免费接口。

2. 参数格式与返回结构:数据获取环节最扎心的一类报错

接口通了,权限有了,下一个坑通常是参数格式。Tushare的参数看着简单,实际坑很多,尤其是ts_code、trade_date、start_date、end_date这几个高频参数。字符串格式错一个字符,轻则报错,重则数据为空,而空数据排查起来比报错更让人抓狂。

2.1 股票代码是“数字字符串”而不是数字

ts_code在Tushare里是类似000001.SZ的格式,由六位数字加交易所后缀组成。有人图省事直接传000001或者1,返回结果不是空就是报参数错误。这里有个容易忽视的点:000001.SZ是平安银行,而000001.SH是上证指数,两者前缀完全一样,但后缀不同代表完全不同的标的。拿到数据后一定要先确认一下返回内容,别把指数当成股票拿去算收益率,分析方向整个就偏了。

2.2 日期参数永远传字符串

我在社群里看过太多代码,写start_date=20230101这种数字格式,结果接口返回数据为空。Tushare的日期参数要求的是YYYYMMDD格式的字符串,不是整数,也不是datetime对象。数字20230101在某些语言里会被当成整数处理,Tushare服务端解析不出来,最后返回一个空DataFrame。这种问题不在代码运行时报Python异常,而是返回了空数据,很多人就陷入“难道是行情真的没有?”的自我怀疑中。

正确的写法是start_date='20230101',或者用datetime.now().strftime('%Y%m%d')动态生成。还有一点:沪深两市虽然大部分时候交易日一致,但偶尔会有单独休市的情况。用日期参数拉数据时,最好先调trade_cal看看哪些是交易日,免得因为某个日期不开市而漏数据。

2.3 返回字段大小写与字段名的“玄学”

Tushare返回的DataFrame,字段名一般是小写加下划线,比如trade_date、open、high、low、close、pre_close、change、pct_chg、vol、amount。有人拿Excel里的数据习惯,上来就写df['Close'],结果KeyError。还有vol字段,注意它含义是成交量(手),不是我们常说的“股市波动率”的那个vol。我在给团队做分享的时候经常强调:拿到数据第一步,print(df.columns.tolist())和print(df.dtypes),先看一眼列名和类型,再开始分析。很多人觉得这一步多余,其实大部分字段相关的Bug都是因为跳过这一步才导致的。

3. 复权因子与价格计算:最容易算错却不自知的坑

如果说前两个坑是“报错了才被发现”,那么复权这个坑属于“算错了也看不出来”的类型。很多人搞不清楚前复权、后复权、不复权三者的区别,直接用close去计算收益率或者画K线图,结果图表看起来没问题,一到回测就发现收益曲线和行情软件对不上。相关热搜词里那个“tushare如何计算复权”其实就是被问烂了的问题。

3.1 不复权、前复权、后复权到底怎么选

用一句话概括:不复权就是原始成交价;前复权是以当前价格为基准,把历史价格往下调整;后复权是以最早上市日为基准,把后面价格往上调整。选哪种取决于你的使用场景。单纯看历史每天真实成交价,用不复权没问题;算策略收益率、回测,建议用后复权;画技术分析图,用前复权比较直观,因为它保证了最新价格就是真实价格,同时历史形态是连续的。

但这里有个极其阴间的细节:Tushare的daily接口返回的是不复权数据,adj_factor接口返回的是复权因子,官方文档里还提供了一套计算前复权和后复权数据的逻辑。很多人只拉daily,拿着原始收盘价直接算收益率,遇到分红送股的日子,收益率就凭空多出一个跳跃。比如一只股票10派5,除息日当天价格会突然掉一块多,不明真相的人还以为市场暴跌。真正的收益率计算,应该基于复权后的价格。

3.2 用复权因子计算时的常见错误

Tushare里面复权因子存在adj_factor接口里,按股票代码和交易日给一个数值,含义是后复权因子。官方推荐的做法是:后复权价格 = 不复权价格 × 当日adj_factor / 最新一日adj_factor?不,实际要看你参考哪一天。比如要把历史价格调整为前复权(以最新日期为基准),就需要拿当日不复权价 × 当日adj_factor / 最近交易日adj_factor。

我第一次写这个逻辑的时候,犯了个错误:把所有股票的adj_factor混在一起取最大值,导致某些停牌很久的老股票全部算错。正确做法是:对每一只股票单独计算,取该股票时间序列里最后一个交易日的adj_factor作为分母。如果你用groupby('ts_code')处理,一定要在分组内做transform('last'),而不是全局取最大值。

import tushare as ts import pandas as pd pro = ts.pro_api() # 拉取不复权日线数据和复权因子 df = pro.daily(ts_code='000001.SZ', start_date='20230101', end_date='20231231') adj = pro.adj_factor(ts_code='000001.SZ', start_date='20230101', end_date='20231231') # 合并 m = pd.merge(df, adj[['ts_code', 'trade_date', 'adj_factor']], on=['ts_code', 'trade_date'], how='left') m = m.sort_values('trade_date').reset_index(drop=True) # 计算前复权价(以最新交易日为基准) latest_adj = m['adj_factor'].iloc[-1] m['close_qfq'] = m['close'] * (m['adj_factor'] / latest_adj) m['open_qfq'] = m['open'] * (m['adj_factor'] / latest_adj)

这里有个细节,m['adj_factor'] / latest_adj得到的是每个交易日相对于最新交易日的复权比例,用它乘以原始价格,就得到“前复权价格”。这个逻辑本身不难,但很多人默认以为daily接口返回的close已经是复权价,实际上并不是。官网文档里关于复权的说明分散在不同页面,不仔细看确实很容易误解。

3.3 复权计算中“停牌”导致因子缺失

还有一个很隐蔽的坑:股票长期停牌时,adj_factor接口可能没有该交易日的数据,合并之后因子列是NaN。如果你没做填充,算出来的复权价格就全是缺失值,后续绘图的线条出现断层。遇到这种情况,用ffill()向前填充因子比较稳妥,前提是公司没有在此期间发生新的除权除息。如果真的发生了,那就需要以公司公告为准手动修正,这种情况比较少见,但做长周期回测时会遇到。

# 因子缺失填充 m['adj_factor'] = m['adj_factor'].fillna(method='ffill')

填充之后再计算复权价,就能避免因子缺失导致的整段数据为空。这也是我说“复权是最容易看不出来错”的原因——不是代码报错,而是算出来的数字和你的券商软件对不上。排查方式很简单:把某只股票在某几次分红除息日附近的复权数据,和任意一款行情软件上的前复权K线对比一下,有差异就说明因子处理有问题。

4. 日期、索引与缺失值:时间序列处理里暗藏的坑

数据拉回来了,复权也算了,下一步就是做分析。但这一步很多人也会翻车,因为Tushare返回的trade_date虽然是字符串格式YYYYMMDD,但排序、切片、重采样这些操作都要求你把它转成真正的日期类型。直接拿字符串排序,虽然按照字典序也能排对,但一旦涉及resample、rolling这类时间序列操作,就会报错或者结果混乱。

4.1 日期字符串与DatetimeIndex的切换

建议一拿到数据就把trade_date转成pd.to_datetime并设为索引:

m['trade_date'] = pd.to_datetime(m['trade_date']) m = m.set_index('trade_date').sort_index()

这是操作时间序列的“地基”。如果不设成索引,后面想按日期切片、按月汇总、画图时标注时间轴,都会很不顺手。还有一个非常常见的错误:pd.to_datetime解析20230101时,如果传的是整数,会得到2023-01-01,但某些版本可能把它解析成1970-01-01加偏移秒数,这属于老版本的坑。用字符串传入就不会有这个问题。

4.2 缺失值与停牌数据怎么处理

股票停牌期间,daily接口往往没有记录。这导致时间序列不连续,有人一上来就dropna(),结果把大段停牌数据全删了,回测时仿佛这只股票不存在,资金占用、交易成本全算错。正确处理方式取决于场景:如果是计算日收益率,可以保留NaN,顺便把它当成“当日没有交易”;如果是计算累计收益,可以ffill()填充收盘价,表示持仓市值在停牌期间不变。至于停牌期间的开盘价、最高价、最低价,一般不建议填充,否则做突破策略时会触发误信号。

还有个股上市首日、退市整理期等特殊阶段,数据可能只有几天甚至半天。回测时最好过滤掉上市前几个月的数据,以免新股上市初期的暴涨暴跌干扰策略统计。这些都是细节,平时不提没人管,一旦真踩到就是“跑出来的结果和别人对不上”的灵异事件。

4.3 交易日历的正确打开方式

判断某个日期是否为交易日,别自己写if weekday < 5就完事,A股有调休、节假日,这种硬编码必然出错。用Tushare的trade_cal接口,传入exchange='SSE',拿到一整年的交易日列表:

cal = pro.trade_cal(exchange='SSE', start_date='20230101', end_date='20231231', is_open='1') trade_days = set(cal['cal_date'].tolist())

有了这个列表,就可以在做时间序列对齐时,把缺失的非交易日直接过滤掉。我见过一个比较离谱的案例:有人自己硬编码了一个“2023年A股休市表”,结果9月某天不巧记错了,导致一整天数据没拉到,回测结果偏了一个点。后来改用trade_cal接口,再也没出过这种问题。

5. 增量更新与数据落库:长期跑数据时必踩的坑

如果你只是临时拉一次数据做分析,前面四个坑基本够用了。但很多人跑量化策略或者做数据研究,需要每天、每周更新数据。这时候增量更新的坑就会集中爆发:要么数据重复,要么数据漏更新,要么本地存储越来越大越来越慢。

5.1 重复拉取与主键冲突

Tushare的daily接口,如果指定了start_date和end_date,返回的是这个区间内的全部日线数据。有人图省事,每次更新都全量拉一遍,然后往同一个表里append,结果一年之后表里堆满了重复行。如果用的是关系型数据库,最好在入库前加一个“唯一键约束”,比如ts_code + trade_date联合唯一,插入时用INSERT OR REPLACE或者先查后插。如果是存csv,那就得每次拉完之后去重:

df = df.drop_duplicates(subset=['ts_code', 'trade_date'], keep='last')

更省事的方案是直接存parquet格式,配合pyarrow按日期分区分文件夹,每次只写入当天的新数据。这样既能保持目录结构清晰,又能用pandas.read_parquet快速读取。

5.2 数据更新的“滞后性”与“修正性”

Tushare的数据并非实时,日线数据通常要等收盘后一段时间才会更新。不同接口的更新延迟略有差别,有的在下午五六点就有了,有的要等到晚上。做当日策略信号时,如果下午三点收盘立刻去拉当天数据,很可能拉到的是空数据或者昨天的数据。一个常见的解决方式:在流程里加一个交易日判断和重试机制,比如过了18:00再尝试拉取,失败则等待30分钟后重试,最多三次。还有一点容易被忽视:上市公司偶尔会修正历史财报或者复权因子,如果你只做增量更新,历史数据可能永远都是旧版本。为了应对这个问题,每周或者每月做一次全量刷新。听起来麻烦,但对数据质量要求高的场景很有必要。

5.3 本地缓存策略与接口频控的配合

长期更新数据,最怕的就是接口频控。Tushare免费接口每分钟可能只允许调用几十次,如果你有几千只股票要更新,一次性循环跑下来,大概率中途被限流。可行的方案是:把股票代码分成多批,每一批之间sleep一点时间;或者用本地SQLite/CSV先保存上一次抓取的时间,每次只抓“上次成功日期 + 1个交易日”到“今天”之间的数据。这样单次请求返回行数不会太多,接口压力小,也不容易触发频控。

import sqlite3 import time conn = sqlite3.connect('daily_price.db') # 假设表结构为 (ts_code, trade_date, open, high, low, close) # 查询每只股票最近更新的日期 for code in stock_list: local_last = pd.read_sql( f"SELECT MAX(trade_date) as last_date FROM daily_price WHERE ts_code='{code}'", conn )['last_date'].iloc[0] start = local_last if local_last else '20200101' new_data = pro.daily(ts_code=code, start_date=start, end_date=today_str) if len(new_data): new_data.to_sql('daily_price', conn, if_exists='append', index=False) # 去重防重复 conn.execute( f"DELETE FROM daily_price WHERE rowid NOT IN " f"(SELECT MAX(rowid) FROM daily_price GROUP BY ts_code, trade_date)" ) time.sleep(0.3)

这段逻辑并不复杂,但它解决了两个问题:一是避免了全量拉取,大幅降低接口压力;二是即使重复插入,最终也能通过去重兜底。不过DELETE语句在数据量大时性能一般,生产环境建议用唯一索引替代。

6. 一套普适的排查思路:从报错到数据的全链路自检

最后分享一套我自己的排查流程,不一定只针对Tushare,任何金融数据接口都适用。遇到“数据对不上”或者“莫名的报错”时,按这个顺序检查,能省下大量时间。

6.1 从“接口调用”到“数据处理”的分层定位法

第一步,确认接口本身是否报错。单独跑一条最简代码,比如拉一只股票一天的行情,看返回是否正常;第二步,确认数据版本和字段。用print(df.info())看有没有全空列、类型有没有被读成object;第三步,检查数据范围。用min(trade_date)和max(trade_date)看看区间是否符合预期,有没有漏掉某个时间段。这个步骤能快速定位问题出在接口层、代码层还是逻辑层。

6.2 常见问题速查表

错误表现可能原因解决方案
返回空DataFrame参数格式错误、日期非交易日、权限不足检查ts_code和日期字符串格式,查询交易日历
提示无权限积分不足或接口未开通到官网积分页面查看分值,对比接口要求
数据有重复行全量追加数据后未去重用drop_duplicates或数据库唯一键
复权价格和行情软件对不上复权因子分母选错或未处理停牌缺失按股票分组,每组取最后一个交易日因子作为分母
频率受限报错单位时间内调用次数过多增加sleep等待,改为增量更新,减少请求次数
时间序列排序乱日期是字符串且未设为索引使用pd.to_datetime并set_index
数据突然中断或空缺当天未开盘、停牌或接口还没更新用trade_cal判断交易日,必要时fillna

6.3 “免费”的代价与替代方案

关于Tushare和东方财富、Wind的区别,简单说几句。Tushare胜在免费、接口干净、文档体系完整,适合个人研究者;东方财富的数据在网页上也有免费接口,但稳定性、完整性和代码封装程度不如Tushare;Wind是最专业的金融终端,Python接口强大,但价格门槛高,不是个人玩家的首选。如果你只是做学术研究或者个人量化策略,Tushare免费积分基本够用。要是数据量需求极大、接口调用频繁,再考虑用Wind或者其他商业数据源做补充。

写在最后

我个人的体会是,数据分析过程中的报错其实并不是最可怕的,真正隐蔽的问题是数据“看起来正常,实际是错的”。复权、缺失值、增量更新这几个环节,恰恰是这些问题最容易藏身的地方。上面这5个坑,有些我踩过,有些看别人踩过,把它整理出来,也是希望后来的朋友能少走点弯路。

最后再分享一个小技巧:处理任何金融时间序列数据,都先画个图,把原始数据和复权数据叠在一起,用眼睛扫一遍。很多时候,曲线断层、价格跳变这类问题,看图一眼就能发现,比在数据表里翻找高效得多。

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

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

立即咨询