资讯详情

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

📅 2026/10/20 12:37:50 | 华诺云谱 👁 阅读
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_code000001.SZ, start_date20230101, end_date20231231 ) 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_statusL, fieldsts_code,name,industry,list_date ) print(stock_list.head()) cal pro.trade_cal( exchangeSSE, start_date20230101, end_date20231231 ) 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_code000001.SZ, start_date20220101, end_date20231231) df_adj pro.adj_factor(ts_code000001.SZ, start_date20220101, end_date20231231) df df_daily.merge(df_adj, ontrade_date, howleft) 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], errorscoerce) 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_codets_code, start_datey_start, end_datey_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_datelast_date, end_dateend_date) trade_days cal[cal[is_open] 1][cal_date].tolist() frames [] for day in trade_days: df_day pro.daily(trade_dateday, ts_codets_code) if df_day is None or df_day.empty: continue frames.append(df_day) df_new pd.concat(frames) return df_new这里用trade_dateday的方式逐日拉取每次请求日线数据量很小。缺点是请求次数多所以一定要配合限流和重试机制。如果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, keeplast) df df.sort_values(trade_date).reset_index(dropTrue)drop_duplicates基于trade_date去重keeplast保留最后一次写入的结果。这样做能容忍更新脚本的幂等性问题不用在写入端强制加锁。但如果多次写入的数据内容不同最好还是在写入时就使用先读后写的合并逻辑别把去重当成万能后悔药。5. 避坑与排查TuShare使用中的5个典型翻车点5.1 接口返回空列表但代码看上去没问题有时候调用pro.daily(ts_code000001.SZ, start_date20230101, end_date20231231)返回一个空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_retries3, base_wait1.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_retries3) def fetch_daily_with_retry(pro, ts_code, trade_date): return pro.daily(ts_codets_code, trade_datetrade_date)装饰器的解析第一次失败等1 random(0,0.5)秒第二次等2 random秒第三次等4 random秒。这样的随机抖动比固定间隔更不容易被风控识别成机器人请求。6.2 数据对齐的最后一道工序把前复权价格、日线行情和股票基础信息合并成一张宽表时注意索引一致性。我用最终合并表时严格按键去重并且用assert检查行数与交易日历匹配。assert len(df_final.index.unique()) len(trade_days), 数据行数与交易日不一致这个断言写在数据管道的最后能挡住大多数因增量更新遗漏而产生的脏数据。从那以后我每次新接一个数据源都会强制走一遍先看字段单位再算一遍复权最后做增量校验。这三步做下来基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑