
做量化、写策略或者单纯想拿A股历史行情来复盘的朋友应该都体会过“数据从哪来”这个老大难问题。我自己最早是去财经网站一个个翻页面后来改成写爬虫去抓接口折腾半天拿回来的数据还经常有缺失、对不上行情软件。后来换到Tushare才总算把这件事彻底理顺了。Tushare本质上是开放API数据接口它跟传统爬虫的最大区别是你不用去逆向网页、不用维护反爬策略注册拿token之后就能按文档调用简单粗暴。这篇就把我用Tushare爬取股票历史数据的完整思路、代码、踩过的坑一次说清楚从环境准备、接口选型、批量抓取、增量更新到数据落库适合刚开始接触量化数据的新手也适合想把自己数据基建搭干净一点的进阶玩家。1. 为什么我最终选了Tushare而不是硬怼网页爬虫1.1 自己爬网页的痛谁爬谁知道我拿新浪和东方财富的行情接口练过手那段时间真是心力交瘁。这些页面虽然是半公开的数据但接口参数、返回值格式、token校验说变就变。今天写好的脚本运行得好好的一周后莫名其妙挂了一查发现对方接口加了字段、换了加密方式。而且财经网站的数据接口大多是面向前端展示的拿到的数据字段少、复权处理基本没有成交量单位有时候是股有时候是手全靠自己猜。更麻烦的是历史数据分页。你要拉某只股票十年的日线就得循环翻几百页做好限速、代理、重试。一个程序里写了大半天本质工作没干多少全耗在“和数据源搏斗”上了。1.2 Tushare的优势一句话说清楚Tushare把底层数据已经整理成了标准化的表结构。它提供的daily接口、pro_bar接口、trade_cal接口、stock_basic接口包括财务数据、资金流向、分钟线全都用统一参数和Python函数返回DataFrame。调用一次接口拿到的就是规整的数据表连清洗的功夫都省了一大半。我拿它和自研爬虫做了一次对比对比维度自己爬网页接口Tushare API数据规范性返回字段混乱需大量清洗标准表格结构字段统一历史数据完整性经常只有近一两年1990年至今的完整日线复权处理需要自己拉复权因子计算pro_bar直接返回前复权/后复权反爬风险高随时可能失效无正规API使用成本免费但时间成本高积分制基础数据免费选型之前我也考虑过买商业行情源问了下价格直接放弃了一年大几万的数据费对个人来说实在没必要。Tushare这种“免费额度积分升级”的模式反而更适合起步阶段基础日线、复权数据都够用。1.3 Tushare的积分门槛实际够用就行Tushare Pro目前是积分制注册之后会送基础积分部分接口有积分门槛。以最常用的daily日线接口为例120积分就能调用。另外通过完善个人信息、社区贡献等途径也能攒积分不是非得充值。我自己一开始只有100多积分就把日均线、周线这些常用功能测了一遍。注意积分不只是决定你能不能调某个接口还影响到每分钟的调用频率。积分低的账户每分钟能调的次数有限批量拉数据时如果不做限速很容易触发限流提示。2. 环境准备与Tushare接口基础2.1 注册、拿到Token并完成第一行代码Tushare的使用逻辑是“token认证”你去官网注册账号后在个人主页能找到一串token字符串。这个token是后续所有接口调用的通行证相当于爬虫场景里的cookie。安装库很简单直接用pip装pip install tushare pandas用的时候先设置token再初始化接口import tushare as ts import pandas as pd # 这里填你自己的token官网个人主页查看 ts.set_token(你的token字符串) # 初始化pro接口 pro ts.pro_api()执行完这一步你就能调用Tushare Pro下的所有授权接口了。如果代码运行报错提示权限不足或token无效优先检查token是否复制完整前后有没有多余空格。2.2 daily接口最基础的日线行情数据daily接口返回的就是股票日线行情。我实际跑的代码长这样df pro.daily(ts_code000001.SZ, start_date20230101, end_date20231231) print(df.head())返回的字段包括字段名含义单位/说明ts_code股票代码如000001.SZ带交易所后缀trade_date交易日期格式YYYYMMDDopen开盘价元high最高价元low最低价元close收盘价元pre_close昨收价元change涨跌额元pct_chg涨跌幅百分比vol成交量手amount成交额千元我特别提醒一下字段单位vol单位是手amount单位是千元不是元。刚开始用的时候我做回测脚本把amount当成元去算换手率数据差了三位数查了半天才发现问题出在单位上。2.3 pro_bar接口能直接拿复权数据做历史回测的朋友要特别注意复权概念。股票分红送股后价格会除权除息K线图会出现跳空。pro_bar接口通过adj参数直接返回复权后的数据省去自己计算复权因子的功夫。# 前复权 df_qfq ts.pro_bar(ts_code000001.SZ, adjqfq, start_date20230101, end_date20231231) # 后复权 df_hfq ts.pro_bar(ts_code000001.SZ, adjhfq, start_date20230101, end_date20231231) # 不复权 df_raw ts.pro_bar(ts_code000001.SZ, adjNone, start_date20230101, end_date20231231)用pro_bar前必须先用ts.set_token()完成认证。如果你调daily很顺利但调pro_bar报错大概率是token没设置好或者顺序错了。3. 爬取股票历史数据的完整实操过程3.1 先获取股票列表构建遍历池要批量把全市场历史数据拉下来第一步是拿到所有股票的代码列表。Tushare的stock_basic接口正好干这个stock_info pro.stock_basic(exchange, list_statusL, fieldsts_code,symbol,name,area,industry,list_date) print(stock_info.shape) print(stock_info.head())list_statusL表示上市状态正常的股票这样能过滤掉退市和暂停上市的。我建议fields里把list_date也带上后面做次新股筛选也好用。拉到的结果是全市场5000多只股票ts_code就是这个股票的全局唯一标识。3.2 批量拉取全市场日线数据拿到股票列表之后剩下的就是写循环遍历了。我先把单只股票的拉取封装成函数这样逻辑清晰且方便复用import time def fetch_daily(ts_code, start_date, end_date): try: df ts.pro_bar(ts_codets_code, adjqfq, start_datestart_date, end_dateend_date) if df is not None and not df.empty: df[ts_code] ts_code return df except Exception as e: print(f{ts_code} 拉取失败: {e}) return None批量遍历时有一个细节必须注意Tushare对每分钟调用次数有限制。积分不高的账户连续高频调用会被限流。我的做法是每次请求之间sleep 0.3到0.5秒批量几千只股票的时候分时段跑别一次性在后台硬怼all_data [] for code in stock_info[ts_code].tolist(): df fetch_daily(code, 20200101, 20231231) if df is not None: all_data.append(df) time.sleep(0.3) # 温柔限速实测下来最稳 fin_df pd.concat(all_data, ignore_indexTrue) print(fin_df.shape)批量拉完建议分股票、分年份保存不要全塞一个文件不然数据量大了内存直接爆掉。3.3 增量更新策略省时省力的关键全量拉取一次几千万行情数据如果每次都从头来就太蠢了。实际操作中我更推荐“全量初始化增量更新”两个阶段。第一次跑全量建底仓之后每天收盘后只拉当天数据。核心思路是先查本地数据里每只股票的最大交易日期下次只拉这个日期之后的数据def get_latest_date(local_file000001.SZ.csv): import os if not os.path.exists(local_file): return 20000101 df_local pd.read_csv(local_file) return str(df_local[trade_date].max()) latest get_latest_date(000001.SZ.csv) df_new fetch_daily(000001.SZ, latest, 20241231) print(f本地最新日期: {latest}, 新增数据: {df_new.shape[0]} 条)增量更新不只是省时间还可以显著减少对接口的调用次数降低限流概率。我实际测试下来每天收盘后更新一次全市场增量数据大概一两分钟就跑完了体验很稳。3.4 数据落库用CSV还是SQLite数据存下来才算真正拥有。我试过两种方式CSV简单直接适合单只股票或小批量数据用pandas直接读写非常方便。缺点是增量写入麻烦每次要读全量再写全量数据多了之后速度很慢。SQLite轻量级数据库单文件存储支持SQL查询和增量插入个人项目首选。我个人的最终方案是用SQLite。建表、插数据都很干净import sqlite3 conn sqlite3.connect(stock_data.db) fin_df.to_sql(daily_qfq, conn, if_existsappend, indexFalse) conn.close()SQLite的增量插入天然支持不会像CSV那样反复读写整个文件。想查询某只股票某段时间的数据一句SQL就出来了query SELECT * FROM daily_qfq WHERE ts_code000001.SZ AND trade_date BETWEEN 20230101 AND 20231231 df_query pd.read_sql(query, sqlite3.connect(stock_data.db))关键是建索引CREATE INDEX IF NOT EXISTS idx_ts_code_date ON daily_qfq (ts_code, trade_date);没加索引前查单只股票全历史要几百毫秒加了这个复合索引后基本十几毫秒搞定差别非常明显。4. 数据处理绕不开的几个细节4.1 复权因子的本质与选择复权问题的本质是分红送转导致的股价不连续。如果直接拿原始价格做技术指标比如计算MACD或者均线除权日会出现假突破、假均线下穿这些信号会误导判断。我用生活化类比解释一下你花10块钱买了1斤苹果商家第二天搞促销“买一斤送半斤”苹果报价虽然变成6.67块但你的资产并没有缩水。股价除权就是这个道理不调整价格序列的话K线图上会无故多出一个“暴跌”。前复权qfq是以当前价格为基准往回调整好处是价格跟现价衔接自然能直观看到当前价位下的历史形态适合看K线图做技术分析。后复权hfq则是以上市首日价格为基准往前调整好处是能从历史价格看出真实的长期涨幅和收益率适合做定量回测。如果你做的是买入持有策略的回测我建议用后复权数据去算收益率这样不会因为复权基准不同而扭曲收益曲线。如果是看盘画线前台展示用前复权更顺手。4.2 交易日历与停牌数据对齐的必修课A股有休市日、节假日、临时停市不是每天都开盘。Tushare提供了trade_cal接口拿交易日历cal pro.trade_cal(exchangeSSE, start_date20230101, end_date20231231) trade_days cal[cal[is_open] 1][cal_date].tolist() print(f2023年共有 {len(trade_days)} 个交易日)为什么必须拿交易日历因为很多股票在非交易日也可能因为公告停牌等原因没有行情记录。如果你直接按自然日序列去对齐数据会有大量空洞日期导致你画图、算收益率时出现NaN满天飞的情况。我的做法是拿交易日历作为标准时间轴再让每只股票的数据左连接到这个日期轴上import pandas as pd df_trade pd.DataFrame({trade_date: trade_days}) df_stock df_trade.merge(df_stock, ontrade_date, howleft)停牌当天没有成交记录合并后就是NaN这属于正常情况。处理的时候你可以选择前向填充或者干脆在计算指标时跳过。4.3 数据质量的校验方法从接口拿到的数据不能直接信我每次拉完都会做几道简单校验检查每只股票的记录数是否合理。比如沪深300成分股一年至少应该有240多条日线记录如果某只股票只有100条大概率是拉漏了。检查close和pre_close的关系。除权日、除息日前后close/pre_close的比例一般会接近复权因子若出现明显的异常跳变多半是复权数据没对齐。检查重复记录。增量更新时最容易出现重复的日期行merge或to_sql时设置主键规避就行。我在代码里常用一个简单函数做最后检查def check_data(df, ts_code): dup df[df.duplicated(subsettrade_date, keepFalse)] null_cnt df[close].isnull().sum() print(f{ts_code}: 重复日期 {len(dup)} 条, close空值 {null_cnt} 条)实测下来好多时候数据问题都是因为脚本增量更新写重复了用这个检查方法能快速定位。5. 常见问题与排查技巧实录5.1 权限不足与限流问题用得多了最容易遇到的报错是权限和频率两回事。问题症状直接原因解决办法提示“抱歉您没有权限访问该接口”积分不满足接口要求升级积分或换低门槛接口提示“每分钟调用次数超限”请求太频繁加time.sleep控制调用频率返回数据为空股票代码或日期范围写错检查ts_code格式、起止日期我被限流卡过很多次尤其写了大循环之后前100只股票跑得好好的突然报错中断。后来固定写法是每调一次就sleep不会因为“想跑快一点”而省掉节奏。注意行情数据接口的限流是每分钟级别的不是每天。短时间密集调用再频繁触发可能会导致账户临时受限。建议批量脚本里加上随机等待时间比如每次等待0.2到0.5秒随机。5.2 数据缺失与空值排查拉回来的数据偶尔有股票缺某几天的记录。遇到这种情况我一般从两个角度排查第一是确认该股票当天是否正常交易。用交易日历和停牌信息交叉比对就能确定。第二是换接口重试。有时daily接口返回为空但pro_bar接口能正常返回反之也有可能这可能跟Tushare内部不同数据源同步效率有关。我踩过最典型的坑是次新股。有些次新股上市时间短在daily接口里没有数据但pro_bar可以拉到那是因为它的上市日期设置略有不同。解决办法很简单统一用pro_bar做数据源daily作为备选补充。5.3 高频调用优化的三个技巧合并请求同一只股票能一次拉全历史的就别分月份循环拉多次减少请求次数。缓存到本地拉过的数据先存本地下次增量时优先读本地而不是每次都去请求接口。错峰执行大盘收盘后是接口调用高峰我习惯在下午4点半之后跑批量脚本响应速度明显更快限流概率也小。6. 进阶方向Tushare不只是日线数据6.1 分钟线与复权数据做日内策略的朋友可以试试分钟线接口。它的使用逻辑和daily几乎一样只是参数多了一个freqdf_min ts.pro_bar(ts_code000001.SZ, freq30min, start_date20240101 09:00:00, end_date20240131 15:00:00)需要注意的是分钟线对积分的门槛更高权限不足的话会直接报错。我用下来把日线策略跑通了再去申请分钟线权限一步步来更稳。6.2 财务数据与基本面指标Tushare有非常丰富的财务数据接口比如income利润表、balancesheet资产负债表、cashflow现金流量表还有fin_indicator这种汇总好的财务指标。我最常用的是fin_indicator它直接返回ROE、净利润增长率、毛利等常用指标省去了自己算的麻烦df_fin pro.fin_indicator(ts_code000001.SZ, start_date20230101, end_date20231231)把这些基本面数据按季度对齐到日线数据上就能构建所谓的“多因子选股”数据集能玩的东西一下子多很多。6.3 指数成分与板块数据除了个股行情Tushare还有指数行情、指数成分、行业板块等接口。我拿指数成分接口来动态跟踪沪深300成分股变化然后构建自己的股票池避免手工维护过时的成分列表。实际操作感受你一旦把数据管道建好后面想做策略的“原料”就一直有了。这也解释了为什么我那么强调把本地数据仓库建扎实——后续的一切都基于一份干净可靠的历史行情库。结尾最后分享一个我个人的使用习惯。每天收盘后我会跑一条增量更新脚本把当天全市场的日线数据落进SQLite长期积累下来本地就有一份完全属于自己的历史行情库。以后再写策略、做回测、画K线图数据随取随用几秒就出结果根本不用临时去调接口既省积分也省心。Tushare用到现在我的最大体会是工具选对了数据基建打牢了后续的分析工作就轻松了。希望这篇分享能帮你少走点弯路把数据坑一次填平。