☰
Tushare避坑指南:从Token权限到数据清洗的五大实战要点
2026/10/2 8:57:48 网站建设 项目流程

在量化交易和数据挖掘这个圈子里,Tushare 大概是国内个人开发者接触最多的一套金融数据接口了。从最开始的攻克积分门槛,到后来每天定时拉取行情、清洗入库,这套流程几乎成了很多人的数据启蒙课。但恰恰是“启蒙”这两个字,让不少人走了一段弯路——很多隐藏规则文档里一句带过,只有自己踩进去才会发现,坑比想象中深得多。今天我把这几年用 Tushare 攒下的问题集中梳理一下,不讲那些官网写清楚的基础用法,只挑最常见的 5 个坑来拆。这 5 个坑分布在数据获取和数据清洗两个环节,每一个我都见过不止一次,也帮人排查过不止一次,相信对正在折腾 Tushare 的朋友会有帮助。

1. 第一个坑:Token 与积分权限,数据还没拿到就先被卡在门外

1.1 Token 获取的正确姿势,别再往代码里硬编码了

很多新手第一次接触 Tushare,最容易卡在 Token 这一步。其实 Token 就是你的身份凭证,注册登录 tushare.pro 官网之后,鼠标放到右上角头像上,下拉菜单里能看到一个“接口TOKEN”入口,点进去就能复制到一串很长的字符串。这个字符串就是调用接口的钥匙,初始化的时候用:

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

这里想说句实在话,我见过不少人在群里直接把 Token 贴在代码里发出来,然后被别人拿去白嫖积分额度。Token 这个东西本质上就是你的账户钥匙,泄露之后别人能调用你的接口权限,额度耗尽你的程序就废了。建议的做法是放到环境变量里,比如在.env文件中配置,然后代码里通过os.getenv('TUSHARE_TOKEN')读取,这样既不会泄露在代码仓库里,换机器也方便。

另外一个容易忽略的问题是,Token 是有积分绑定的。注册之后的基础积分只有 120,而不少接口都有积分门槛,比如财务三大报表接口一般要求 2000 积分以上。所以初始化成功了不代表所有接口都能用,调用之前先去官网文档里查一下目标接口的积分要求,不然会白白踩一脚“抱歉,您没有权限”的报错。

1.2 积分不够,接口再好也白搭

Tushare 的积分体系刚开始看会觉得有点绕,但核心逻辑其实很简单:越核心、越精细的数据,对积分的要求越高。120 积分的基础账号能用日线行情、股票列表、交易日历这些基础接口,但如果想拿财务数据、衍生指标、分钟数据,基本上得往上攒积分。

攒积分的方式官网写得很清楚,无非是完善个人信息、绑定手机号、关注公众号、参与社区活动这些。这套体系确实有点门槛,但换个角度想,它也是 Tushare 能持续提供稳定服务的商业逻辑。我的建议是:动手之前先去官网文档中心把你需要的接口挨个查一遍,确认自己当前积分够不够。如果不够,别硬等积分增长,可以先用手头能拿到的数据把整个流程跑通,等积分够了再补充细节数据。

注意:Token 泄露之后,别人挥霍的是你的调用额度,轻则限流,重则封号。务必像保管密码一样保管 Token。

2. 第二个坑:接口参数的隐藏规则,字符串和日期格式坑到怀疑人生

2.1 trade_date 必须是字符串,这个细节卡住了一半新人

Tushare 的接口参数对类型有严格的要求,最典型的就属trade_date了。官网示例里写的是trade_date='20240101',看起来平平无奇,但如果你图省事直接传一个整数 20240101,接口立刻就报错。类似的情况还有start_date、end_date,全部要求字符串格式,且必须是YYYYMMDD这种八位格式,带横杠的2024-01-01也不行。

这个坑特别隐蔽的地方在于,如果你用 pandas 读入日期列,然后再传给接口,经常会因为类型已经变成了 Timestamp 或 int64 而报错。所以正确操作是,在调接口之前统一做一次字符串转换:

date_str = '20240101' df = pro.daily(trade_date=date_str)

如果你是从 DataFrame 里取日期,强烈建议提前用astype(str)转好,不要图一时方便直接塞进去,否则报错之后你还得回头排查是哪个环节把类型改了,纯属浪费时间。

2.2 ts_code 的格式也藏着门道

另一个参数坑是ts_code。Tushare 的股票代码格式是“代码 + 交易所后缀”,例如平安银行是000001.SZ,贵州茅台是600519.SH。上交所的股票以6开头,后缀是.SH;深交所的股票以0或3开头,后缀是.SZ;北交所的股票后缀则是.BJ。

这个后缀一旦写错,比如把上证股票写成.SZ,接口要么返回空数据,要么报错。有些朋友从网上爬到的股票列表里只有纯代码,没有后缀,直接拿去调 Tushare 就会一头雾水。解决办法也很简单,用pro.stock_basic()拉一次全市场股票列表,从里面拿标准的ts_code字段,不要自己拼。

2.3 参数优先级的坑:trade_date 和 ts_code 同时传会怎样

第三个隐藏规则是参数优先级。以pro.daily为例,这个接口允许你按股票代码拉历史行情,也允许你按交易日拉全市场行情。官方文档的规则是,ts_code和trade_date两个参数二选一,如果同时传入,接口会报错。这其实很好理解,按关键字段查还是按时间点扫全市场,这是两种完全不同的查询模式,混在一起语义就会乱。

理解了这一点,你就知道为什么有些人的程序跑着跑着就报“参数错误”了——大概率是循环里既给了ts_code,又在上一次迭代里漏清了trade_date。排查思路也很简单:每次都重新构造参数 dict,不要复用同一个 dict。

3. 第三个坑:循环拉取被限流,效率瓶颈不在网速在调用姿势

3.1 全市场按股票一个个拉,慢到想砸电脑

先说一个我特别常见到的场景:有人要拿全市场几千只股票的日线数据,于是写了一个 for 循环,挨个给pro.daily(ts_code=xxx)发请求。跑起来之后发现速度慢得离谱,几千只股票跑完可能要几个小时,跑着跑着还频繁报错。

这个姿势的问题在于,Tushare 日线接口支持按交易日拉取全市场快照。也就是说,你只需要传入一个trade_date,就能一次拿到当天所有有交易的股票数据,根本不需要一股一股地去查。按交易日循环,一年也就两百多个交易日,比按股票循环几千次要高效太多了。

我整理了一个简单的对比:

拉取方式请求次数耗时(估算)
按股票循环 5000 只5000 次25~50 分钟+频繁限流
按交易日循环 250 天250 次2~5 分钟,稳定不报错

差距就是这么大。做全市场数据,首选按交易日拉,然后再根据ts_code分组使用。

3.2 限流机制与重试策略,别再闷头撞墙了

Tushare 对接口调用频率是有限制的,具体的频次和你的积分等级挂钩。低积分账号如果短时间内请求太密集,控制台会直接返回“频率限制”之类的错误。这个机制本身是为了保护服务器,但对于不熟悉的人而言,往往表现为“跑着跑着程序突然中断”或者“明明刚才还能调,现在全在报错”。

我的做法是两层防护。第一层是主动降速,每次请求之间加一个time.sleep(0.2~0.3),宁可慢一点也别触发限流。第二层是做好失败重试,捕获异常后先等几秒再重试,连续失败好几次才放弃,并打印出错的日期方便排查:

import time def fetch_with_retry(func, **kwargs): for attempt in range(3): try: return func(**kwargs) except Exception as e: print(f'第 {attempt + 1} 次请求失败: {e}') time.sleep(2) return None

3.3 分段缓存,防止拉到一半全部重来

还有一个效率相关的经验是:拉数据一定要做增量缓存。比如你要拉过去五年的日线数据,一次性全拉完当然可以,但只要中途网络抖一下、程序崩一下,之前拉的全白费了。更稳的做法是按年或按季度分段拉取,每一段拉完就落盘成 CSV 或 Parquet 文件,最后再统一读出来合并。这样就算中断,也只需要补跑缺失的那几段,不用从头再来。

4. 第四个坑:复权计算搞错一次,回测结果全得推翻

4.1 不复权、前复权、后复权,到底该用哪个

Tushare 的pro.daily接口返回的是不复权价格,这一点很多人在一开始根本没注意到。不复权价格有个很显著的特征:遇到股票除权除息的日子,价格会突然向下跳空,但成交量、市值这些其实没变。如果直接用不复权价格计算收益率,回测结果会惨不忍睹,尤其遇到分红送股比较多的股票,历史收益会被严重扭曲。

复权本质上就是把除权除息造成的价格断层修补起来,让你看到一条连续的、反映真实涨跌的曲线。前复权是以当前价格为基准,把历史价格向下调整;后复权是以历史价格为基准,把当前价格向上调整。两者的最终计算结果是等价的,但有个重要的区别:前复权价格会随着时间推移、最新价的变化而不断变化,也就是说同一段历史数据,你上个月下载的前复权价和这个月下载的前复权价可能是不同的。所以在回测中我更推荐用后复权价格,因为它确定性强,历史数据一旦算出就是固定的,不会因为今天股价涨了就被整体重写。

4.2 用复权因子计算前复权和后复权价格

新版 Tushare Pro 接口里,pro.daily是没有adj参数可以直接返回复权价的。正确做法是再调一次pro.adj_factor拿到复权因子,然后自己计算。复权因子的计算逻辑是:

  • 后复权价格 = 不复权收盘价 × 当日复权因子
  • 前复权价格 = 不复权收盘价 × 当日的复权因子 / 最新交易日的复权因子

代码如下:

import tushare as ts import pandas as pd pro = ts.pro_api() # 1. 拉取不复权行情 df = pro.daily(ts_code='000001.SZ', start_date='20230101', end_date='20240101') # 2. 拉取复权因子 adj = pro.adj_factor(ts_code='000001.SZ', start_date='20230101', end_date='20240101') # 3. 合并 df = df.merge(adj[['ts_code', 'trade_date', 'adj_factor']], on=['ts_code', 'trade_date']) df = df.sort_values('trade_date').reset_index(drop=True) # 4. 计算复权价 latest_factor = df['adj_factor'].iloc[-1] df['close_qfq'] = df['close'] * df['adj_factor'] / latest_factor # 前复权 df['close_hfq'] = df['close'] * df['adj_factor'] # 后复权

这段代码交付出去之后,我经常会补一句提醒:做因子研究时,涉及价格的一定要检查一下复权口径。我遇到过一个把前复权当后复权存库的案例,结果某只股票在分红之后的历史收益曲线完全对不上,排查了很久才发现是复权因子除以最新因子这一步多算了一次。这种错误一旦发生,回测结果就是错的,再好看的策略曲线也是空中楼阁。

注意:如果只需要做收益率计算,还有一种更不容易出错的思路——直接用 Tushare 返回的pct_chg字段。但需要注意这个字段同样基于不复权价格计算,除权日那天的涨跌幅会异常。稳妥起见,还是用复权后的收盘价自己算一遍收益率。

5. 第五个坑:数据清洗的隐形陷阱,trade_date 居然变成了浮点数

5.1 读取 CSV 后日期列变成浮点数或者整数

数据拉取下来之后,很多人习惯直接to_csv保存,等下次要用的时候再read_csv读回来。这个操作看起来人畜无害,其实埋了一个大雷:如果你没有显式指定 dtype,pandas 在读 CSV 的时候会把trade_date这种全是数字的列自动解析成 int64,而一旦这一列里有空值,整个列就会在读取时变成 float64。最终你看到的trade_date不是20240101,而是20240101.0。

这个坑特别隐蔽,原因是程序不会报错,但后续所有基于日期的字符串操作、拼接、比较都会出问题,甚至你根本察觉不到。解决办法有两个,任选其一:

# 方法一:读入时指定列类型 df = pd.read_csv('daily.csv', dtype={'trade_date': str}) # 方法二:读入后统一转换 df['trade_date'] = df['trade_date'].astype('Int64').astype(str).str.replace('.0', '')

更稳健的做法是不要用 CSV 存中间结果,改用 Parquet 格式,这样可以在存储时把字段类型固定好,避免反复转换带来的数据隐患。刚开始可能不习惯,但用一次就回不去了。

5.2 数据去重与排序,一次到位别拖泥带水

Tushare 接口在正常情况下返回的数据不会重复,但在自己拼接数据的时候很容易引入重复行,尤其是按交易日拉全市场,然后多个日期数据做 concat,如果中途补跑了一段、重复拉了某一区间,最后拼出来的 DataFrame 就会有重复记录。

这种重复很难用肉眼发现,但一旦进入计算,最后统计出来一个离谱的数字,你又得回头查数据质量。所以合并之后立刻做一步去重和排序,是成本最低的保险:

df = df.sort_values(['ts_code', 'trade_date'], ascending=[True, True]) df = df.drop_duplicates(subset=['ts_code', 'trade_date'], keep='last') df = df.reset_index(drop=True)

排序和去重的顺序有讲究:先排序再去重,可以保证keep='last'留下的是时间更靠后的记录。如果你先去了重再排序,谁会被留下就完全随机了。

5.3 缺失值的处理,不是所有 NaN 都该用 0 填充

最后一个清洗层面的坑是关于缺失值的。Tushare 返回的数据里出现 NaN 是很正常的,原因各不相同。比如某只股票当天停牌,按交易日拉全市场时就可能没有这一天的数据;又比如某些次新股上市较晚,在上市之前当然没有行情记录。

有些人图省事,拿到数据之后直接fillna(0),这种做法非常危险。价格列是 0 的话,计算收益率时会直接出现 -100% 的荒谬数值;成交量是 0 的话,一些流动性指标也会被污染。正确的思路是:区分数据是“真的没有”还是“应该是 0”。价格、市值、成交额这些字段出现 NaN,大部分情况都应该用前值填充或者剔除该行;涨跌幅、成交量这类字段如果是停牌导致的,填充为 0 是可以接受的,但需要明确记录这一步。

另外,如果想做面板数据,经常需要把宽表转成长表或者反过来。这种转换过程中 pandas 的pivot会自动产生 NaN,这时候的 NaN 代表“该股票在该时间点没有交易数据”,处理方式和上面类似,先想清楚业务含义再动手。

6. 可直接抄作业的完整代码框架

前面把坑拆开讲了,这一节给出一套完整的、可以直接拿来改改就用的流程,把获取、清洗、保存串联起来。注意替换成自己的 token,并根据实际需要调整起止日期。

import tushare as ts import pandas as pd import time import os # ---------- 初始化 ---------- ts.set_token(os.getenv('TUSHARE_TOKEN')) pro = ts.pro_api() # ---------- 获取交易日历 ---------- cal = pro.trade_cal(exchange='SSE', start_date='20240101', end_date='20240201', is_open='1') trade_dates = cal['cal_date'].tolist() print(f'共 {len(trade_dates)} 个交易日') # ---------- 按交易日拉取全市场日线行情 ---------- all_daily = [] for date in trade_dates: try: df = pro.daily(trade_date=date) if df is not None and not df.empty: all_daily.append(df) time.sleep(0.3) # 主动降速,避免触发限流 except Exception as e: print(f'{date} 拉取失败: {e}') time.sleep(2) # ---------- 合并与清洗 ---------- result = pd.concat(all_daily, ignore_index=True) result['trade_date'] = result['trade_date'].astype(str) result = result.sort_values(['ts_code', 'trade_date'], ascending=[True, True]) result = result.drop_duplicates(subset=['ts_code', 'trade_date'], keep='last') result = result.reset_index(drop=True) # ---------- 计算后复权价(如果需要) ---------- # 说明:全市场批量计算复权时,建议先按 ts_code 分组再逐组调用 adj_factor # 下面的代码是单只股票的示例,批量场景请在此基础上用 groupby 封装 # ---------- 保存 ---------- result.to_csv('daily_data.csv', index=False, encoding='utf-8-sig') print(f'清洗完成,共 {len(result)} 条记录')

这套代码里每行都有它的作用,不是花架子。比如time.sleep(0.3)就是专门用来对抗限流机制的;先排序再去重是为了确保留下后面那条数据;encoding='utf-8-sig'是为了在 Excel 里打开 CSV 不乱码。这些细节单拎出来看都不起眼,但结合起来就是一套非常耐用的数据拉取管道。

7. 常见问题速查表(附排查思路)

最后把上面提到的坑整理成一张速查表,方便以后遇到问题直接对号入座。

现象原因解决方案
调用接口报“抱歉,您没有权限”当前积分不满足接口要求先去官网文档查目标接口的积分门槛,再确认自己的积分
报错提示参数类型错误trade_date传了 int 或 Timestamp统一用'YYYYMMDD'格式字符串,用astype(str)提前转换
循环拉取全市场数据太慢按股票循环,请求次数爆炸改成按交易日循环,一次拿当天全市场数据
跑着跑着大量请求失败触发每分钟调用频率限制每次请求之间加time.sleep(0.2~0.3),并做好重试机制
收益率计算结果异常,出现剧烈跳变除权日数据未复权用adj_factor计算前复权或后复权价格后再计算收益率
读回 CSV 后trade_date变成20240101.0列类型被 pandas 推断为 float读取时指定dtype={'trade_date': str},或改用 Parquet 格式存储
数据量对不上,疑似有重复concat 时区间重叠或重复拉取用drop_duplicates(subset=['ts_code', 'trade_date'])去重
某只股票在某天没有记录停牌或未上市按业务含义决定是前向填充还是剔除,不要所有 NaN 一刀切填充为 0

这张表不是给你背的,而是建议你收藏起来,等哪天程序出问题的时候翻一翻。数据获取和数据清洗这个领域,很多问题不是逻辑多复杂,而是细节太多,一步没注意就会绕远路。我这几年用 Tushare 攒下来的经验,说到底也就一句话:在做任何计算之前,先确认数据是怎么来的、经过了哪些转换、类型是什么、有没有重复和缺失。数据管道建得越稳,后面做分析和建模就越省心。

另外多说一句,Tushare 的功能不止日线行情这一块,像资金流向、龙虎榜、财务指标这些接口都是好东西,等基础流程跑通之后可以慢慢往上加。数据源本身只是一个起点,怎么把数据变成可靠、干净的分析素材,才是真正需要花时间打磨的核心能力。

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

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

立即咨询