资讯详情

Tushare在Python量化中的实战:行情数据、复权因子与增量更新

📅 2026/10/14 6:19:47 | 华诺云谱 👁 阅读
Tushare在Python量化中的实战:行情数据、复权因子与增量更新
1. 从“查行情”到“认真用”一个使用的转折1.1 为什么一开始没把它当主力几年前我第一次接触 Tushare其实只是把它当成一个“查行情”的工具想要某只股票的历史K线写两行代码拉下来画张图看看均线形态然后就没有然后了。那时候我对接口的要求很简单——能跑通就行字段对不对、复权口径准不准、数据更新是否及时我基本没有认真考虑过。后来做 python 量化慢慢深入才意识到问题不在“有没有数据”而在“数据是不是可以被信任地用于研究”。我用其他渠道拿到的行情数据经常遇到这些情况同一个交易日两个来源的收盘价差了零点几分除权除息之后K线图上出现一根莫名其妙的大阴线停牌股被算作涨跌幅 0干扰了因子计算日期字段格式不统一有的带时分秒有的只有日期。这些坑在回测阶段可能只是数字上的差异到了实盘环境就是策略逻辑判断错误。于是我开始重新审视 Tushare认真把它作为数据分析基础设施的一部分来用。用了大概两个月之后我的结论是它不一定适合所有场景但对于个人开发者做量化研究来说是一个性价比很高的数据底座。1.2 让我留在 Tushare 的几个理由认真用下来Tushare 最吸引我的并不是单个接口而是整体设计字段命名稳定、覆盖面广、接口之间有清晰的主键关联。比如说行情数据用ts_code加trade_date就能关联到复权因子、交易日历、资金流向等数据不需要自己费劲做字段映射。另一个让我留下来的原因是“可持续性”。我见过不少免费数据源今天还能访问明天就改了接口地址或者突然停服。Tushare 虽然有积分限制但至少接口演进是渐进式的文档也一直在补充。对个人量化研究来说一个稳定、口径清晰的数据源比单纯“字段多”更重要。还要说的是Tushare 的社区资料很丰富。遇到“为什么某接口返回为空”“积分要怎么分配”这类问题搜索一下基本都能找到参考案例。这对初学者非常友好也是我开始向身边 A同学、B同学推荐它的原因。认真用不是因为它完美而是因为它在“够用、稳定、可追溯”这三件事上做得比较均衡。2. 核心思路把它当数据工程来用而不是简单调接口2.1 数据接口在量化里的真实位置很多刚接触 python 量化的人会把大部分时间花在写策略、调参数上数据部分只用一句ts.pro_bar()带过。我最早也是这样。后来踩了几次坑才想明白数据是策略的地基地基不平策略再漂亮也没用。一个完整的量化研究流程大概是数据获取与清洗因子计算与特征构造回测验证与参数筛选实盘执行与风险监控。Tushare 在这个链条里主要解决第 1 步但它影响后面所有步骤。比如回测时如果直接拿不复权价格计算涨跌幅遇到分红送股就会在除权日出现价格跳空策略看到的是“暴跌”而不是“正常除权”信号就被污染了。所以我现在用 Tushare 的adj_factor复权因子统一处理前复权价格保证收益率序列相对真实。另外一个容易忽视的点是“未来函数”。Tushare 的财务数据接口里有ann_date公告日期和end_date报告期如果做回测时用end_date去对齐数据就会提前用到当时还没公告的信息。正确做法是用ann_date或f_ann_date判断数据在 D 日是否已经公开。这一点我会在后面的示例中重点演示。2.2 先想清楚要拿什么数据再写代码我开始认真用 Tushare 以后做的第一件事不是写代码而是列一张数据需求表。目的很简单搞清楚每个任务到底需要哪些字段、更新频率是多少、应该在什么时间点拉取。参考格式如下研究任务对应接口关键字段更新频率说明日常行情入库pro_bar/dailytrade_date, open, high, low, close, vol, amount日频每日收盘后建议取前复权价用于研究复权因子adj_factorts_code, trade_date, adj_factor日频事件驱动除权除息日会变化交易日历trade_calcal_date, is_open低频每年更新判断策略调仓日是否可交易财务指标fina_indicatoreps, roe, revenue等季频注意公告日期指数权重index_weightcon_code, weight每期调整后更新构建股票池常用这张表的意义在于帮我控制取数颗粒度。Tushare 是带有积分和频率限制的接口如果每次跑策略都全量拉一遍数据既慢又浪费。提前设计好字段就能把取数次数降到最低。我的原则是能低频拉的数据不高频拉能存本地的不重复请求能用日期条件过滤的不全表下载。2.3 积分和调用频率怎么安排合理积分是绕不开的话题。Tushare 的部分接口有积分门槛每个接口每天的调用次数也有上限。对个人研究来说关键是别把所有数据请求写成无脑 for 循环。我常用的处理方式有两个第一个是“按需取数”。一个策略需要近三年的日线就只拉近三年的区间不要顺手把十年数据都下下来。另一个是“本地缓存”。把已经拉过的数据存成 SQLite 或 Parquet后续策略直接读本地文件只有首次下载或更新时才访问 Tushare。调频率控制方面我习惯在请求函数外面加一个简单的时间间隔避免连续高频请求触发限制。示例import time import tushare as ts def fetch_with_interval(func, interval0.4, **kwargs): df func(**kwargs) time.sleep(interval) return df这里的interval可以根据接口限制调整我一般设为 0.3 到 0.5 秒。别小看这个细节批量更新几十只股票时它能避免你频繁撞到限频。3. 几个很实用的用法与完整代码示例3.1 基础用法历史日线行情 复权因子先说最基本的取某只股票的历史日线。我通常用ts.pro_bar()它能一行代码返回行情并且支持adj参数直接拿前复权、后复权或不复权数据。import tushare as ts ts.set_token(你的token) pro ts.pro_api() # 获取前复权日线行情 df_qfq ts.pro_bar( ts_code000001.SZ, adjqfq, start_date20230101, end_date20231231 ) print(df_qfq.head())这里有几个字段需要特别说明vol成交量单位是手amount成交额单位是千元trade_date交易日期格式是字符串 YYYYMMDDpct_chg涨跌幅单位是百分比。拿到行情后我一般会单独拉一份复权因子做交叉验证。Tushare 的adj_factor接口返回的就是复权因子它和行情通过ts_code trade_date关联。adj pro.adj_factor( ts_code000001.SZ, start_date20230101, end_date20231231 ) print(adj.head())为什么要额外关注复权因子因为pro_bar返回的前复权价格虽然可以直接用于研究但如果你想自己实现“分红再投资”或者“指定除权日调整”就必须亲手计算。我的习惯是日常因子计算用pro_bar的前复权价做事件研究或分红回测时再回到adj_factor自己重建价格序列。3.2 选对日期骨架交易日历与交易日判断在策略里判断“某一天是否交易”听起来简单实际很麻烦。周末和法定节假日会导致交易日不连续如果只用 pandas 的date_range生成自然日就会出现把非交易日当成有效调仓日的问题。Tushare 提供了交易日历接口直接用is_open字段判断cal pro.trade_cal( exchangeSSE, start_date20240101, end_date20241231, fieldscal_date,is_open ) open_days cal[cal[is_open] 1][cal_date].tolist() print(open_days[:5])拿到open_days之后我一般会把它缓存成本地列表供所有策略脚本共用。计算持仓周期、判断调仓日、对齐换仓信号时都以这个列表为准而不是简单加一天。有一次我在模拟项目 X 里做“每周调仓”的策略最初写的是每隔 5 个自然日调一次仓结果遇到长假调仓日落在非交易日回测收益和预期差了很多。后来把日历改成open_days按交易日索引模型就顺了。这个细节对做日频以上策略的人特别重要。3.3 财务数据快照与增量更新财务数据是量化选股里很常用的一类数据但它的时间口径比行情复杂得多。我常用fina_indicator获取基本面指标df_fina pro.fina_indicator( ts_code000001.SZ, start_date20220101, end_date20231231, fieldsts_code,ann_date,end_date,eps,roe,netprofit_yoy ) print(df_fina.head())这里我特意保留了两个日期字段end_date报告期截止日ann_date实际公告发布日。做回测时一个常见的坑是直接用end_date对齐财务数据。实际上年报可能到第二年 4 月才公布如果回测在 1 月就“用”了这份年报数据就属于未来函数。正确的做法是在 T 日使用财务数据时必须满足ann_date T。所以我会把财务数据再处理一步# 生成“在某个调仓日可用的最新财务指标快照” def get_latest_fina_on_date(df_fina, ts_code, date): available df_fina[ (df_fina[ts_code] ts_code) (df_fina[ann_date] date) ].sort_values(ann_date) return available.iloc[-1] if not available.empty else None这种处理思路能让因子计算更贴近真实交易环境也是我从“随便拿数据跑一跑”到“认真做量化”的重要转折点。3.4 分析场景用 Tushare 数据快速做一轮质量检查数据拿到手第一件事不是直接算指标而是先做质量体检。我写了一个固定的检查流程每次从 Tushare 拉完数据都会跑一遍。import pandas as pd # 假设 df 是从 Tushare 获取的日线行情 def check_data_quality(df): # 1. 重复检查 duplicates df.duplicated(subset[ts_code, trade_date]).sum() print(f重复记录: {duplicates}) # 2. 缺失检查 print(f缺失值统计:\n{df.isnull().sum()}) # 3. 日期连续性 df_sorted df.sort_values(trade_date) print(f日期范围: {df_sorted[trade_date].min()} - {df_sorted[trade_date].max()}) print(f记录数: {len(df_sorted)}) # 4. 极端涨跌幅检查 extreme df_sorted[(df_sorted[pct_chg] 21) | (df_sorted[pct_chg] -21)] print(f超出常规涨跌幅的记录数: {len(extreme)})上面第 4 条是针对 A 股市场涨跌幅限制的常规检查。如果出现超过 20% 的涨跌幅记录就要警惕是不是数据口径出错、复权异常或者是新股上市首日、退市整理期等特殊情况。把这些异常标记出来而不是信以为真可以避免后面因子计算时被离群值带偏。4. 实操过程与关键环节实现4.1 本地存储怎么设计从 CSV 到 SQLite刚开始用 Tushare 时我习惯把每次拉到的数据保存成 CSV代码很直接df.to_csv(daily_000001.csv, indexFalse)但用了一个月问题就出来了文件越攒越多每次读取要拼接还要自己处理字段类型。CSV 只适合临时看数据不适合做长期积累。后来我把存储换成了 SQLite轻量、单文件、可以建立索引对个人量化项目完全够用。建表逻辑大概是这样的import sqlite3 conn sqlite3.connect(market_data.db) df_qfq.to_sql( daily_qfq, conn, if_existsappend, indexFalse )第一次写入时我会给数据表增加索引conn.execute( CREATE INDEX IF NOT EXISTS idx_daily_ts_date ON daily_qfq(ts_code, trade_date) )这样后续查询“某只股票某段时间”的行情会快很多。说到数据类型我建议把trade_date直接保存为整数或字符串YYYYMMDD不要存成带时区的 datetime否则做连接操作非常容易踩坑。4.2 增量更新与缓存策略本地库建好之后取数逻辑就变成“增量更新”而不是每次全量拉取。核心思路是先查本地已有数据的最新日期只拉取之后的数据。示例def get_local_max_date(conn, ts_code, tabledaily_qfq): sql f SELECT MAX(trade_date) FROM {table} WHERE ts_code {ts_code} cur conn.execute(sql) row cur.fetchone() return row[0] if row and row[0] else 00000000 max_date get_local_max_date(conn, 000001.SZ) start_date max_date # 从已有数据的下一天开始拉取再配合 Tushare 的trade_cal可以算出最新交易日后应该补拉哪些日期。增量更新不仅省积分也降低接口被限频的可能性。我还会在前复权数据入库时加一个updated_at字段记录每行数据的入库时间。这样即使某天数据源有问题也能追溯是哪一批数据导致的问题。4.3 一个完整的取数任务编排示例把前面几段的内容整合起来就是一个可以每天自动运行的取数脚本。下面这个例子实现的是对一批股票做日频前复权行情更新并同时刷新交易日历。# -*- coding: utf-8 -*- import time import sqlite3 import tushare as ts import pandas as pd ts.set_token(你的token) pro ts.pro_api() STOCK_LIST [000001.SZ, 600000.SH, 000002.SZ] DB_PATH market_data.db def init_db(conn): conn.execute( CREATE TABLE IF NOT EXISTS daily_qfq ( ts_code TEXT, trade_date TEXT, open REAL, high REAL, low REAL, close REAL, pre_close REAL, change REAL, pct_chg REAL, vol REAL, amount REAL ) ) conn.execute( CREATE INDEX IF NOT EXISTS idx_daily_ts_date ON daily_qfq(ts_code, trade_date) ) conn.execute( CREATE TABLE IF NOT EXISTS trade_cal ( cal_date TEXT PRIMARY KEY, is_open INTEGER ) ) def update_trade_cal(): cal pro.trade_cal(exchangeSSE, start_date20200101, end_date20261231) cal cal[[cal_date, is_open]] return cal def update_daily(ts_code, start_date): df ts.pro_bar( ts_codets_code, adjqfq, start_datestart_date, end_date20261231 ) if df is None or df.empty: return None df df[[ts_code, trade_date, open, high, low, close, pre_close, change, pct_chg, vol, amount]] return df def main(): conn sqlite3.connect(DB_PATH) init_db(conn) # 更新交易日历 cal update_trade_cal() cal.to_sql(trade_cal, conn, if_existsreplace, indexFalse) # 更新每只股票的行情 for code in STOCK_LIST: cur_max conn.execute( SELECT MAX(trade_date) FROM daily_qfq WHERE ts_code ?, (code,) ).fetchone()[0] start cur_max if cur_max else 20200101 df update_daily(code, start) if df is not None: df.to_sql(daily_qfq, conn, if_existsappend, indexFalse) print(f{code}: 更新 {len(df)} 条记录) time.sleep(0.5) conn.close() if __name__ __main__: main()这个脚本我一般放在定时任务里每天下午收盘后跑一次。它只做增量更新重复跑不会产生重复数据所以即使某天忘了执行第二天再跑也能补上缺口。5. 常见问题与排查技巧实录5.1 权限不足或积分限制报错用 Tushare 时最常见的一类报错大概长这样抱歉您没有访问该接口的权限遇到这类提示优先检查三件事当前账号 token 是否设置正确对应接口是否有积分门槛近期是否已经在高频调用触发了分钟或每日频率限制。我的处理办法是把会频繁访问的接口如日线行情降到最低调用次数靠本地缓存扛住大部分读取需求。财务数据、指数权重这类低频数据一个月更新一次就够。只要把“增量更新”这件事做好多数情况下积分限制不会成为瓶颈。5.2 返回数据量比预期小甚至为空有一段时间我拉某只股票的日线发现数据总是少一段。排查之后发现问题出在日期参数上代码里写的是start_date20230101但 Tushare 的日期参数要求格式是YYYYMMDD我当时不小心传了带“-”的格式接口静默返回了空数据。另外还要注意股票存在退市、暂停上市、新股上市未满几日等情况。某些标的在某段区间没有行情是正常的不代表接口出错。遇到空数据我会先查交易日历确认那段时间是否有交易再把过滤条件放宽试试。5.3 复权数据和自己算的对不上前复权结合分红送股时计算结果容易让人怀疑人生为什么pro_bar返回的价格和我手动用复权因子算出来的不一致最常见的原因是口径选择不同。Tushare 的pro_bar在adjqfq时直接返回前复权价而adj_factor接口提供的是独立复权因子两者虽然可以换算但如果你在计算中把“最新交易日”选错了结果就会不同。我建议在处理前复权价格时明确记录数据截止日期不要在同一个数据集里混用不同截止日期的复权因子。还有一点分红送股会改变复权因子所以每次除权除息后历史前复权价格理论上都会被重新计算。做长期回测时前复权数据的稳定性不如后复权。如果你更关注历史价格走势形态前复权直观如果你更关注不同时间段收益率的可比性建议再拉一份后复权价做交叉验证。5.4 数据去重与入库顺序问题增量更新脚本跑了几周后有一次我发现daily_qfq表里出现了重复记录。原因是某天任务被手动重复执行两批数据范围重叠。这个问题可以在入库前加一次去重df df.drop_duplicates(subset[ts_code, trade_date])更稳妥的做法是在 SQLite 表上给(ts_code, trade_date)加唯一约束然后用INSERT OR REPLACE写入。这样即使任务重复跑也不会产生脏数据。最后再分享一个小技巧我自己的习惯是所有从 Tushare 返回的日期字段统一先处理成YYYYMMDD格式的字符串或整数再做任何排序、拼接或索引操作。这个习惯让我少踩了很多坑尤其是把行情和交易日历、财务公告日期做关联的时候。如果你刚接触 python 量化不要急着把所有接口都接一遍。先挑一个自己最熟悉的场景比如“每天更新自选股行情”把 Tushare 接到本地 SQLite跑通第一版然后再逐步加入财务数据、因子计算。数据基础设施是一点点垒起来的稳定比花哨重要。用 Tushare 认真做一轮完整的数据链路之后你会发现它带给你的不只是数据还有一套让研究过程更可控的工作习惯。
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑