简介围绕 tushare 最新源代码整理的开源项目包面向已有 Python 基础、希望从接口调用进阶到源码理解的金融数据分析学习者与量化爱好者。压缩包共 44 个文件以 25 个 Python 源码文件为主体搭配 7 个 rst 文档、3 个 txt 说明以及 md、yml、Makefile 等配置与说明文件整体约 269KB兼顾核心实现、测试脚本和文档结构。已有 1494 人学习下载。源码覆盖股票、期货、基金等数据的接口封装、HTTP 请求与解析、异常处理、数据库存取、数据清洗及多线程与异步 IO 等环节读者可据此理解财经数据获取链路的内部机制并借鉴并发调度、日志记录和测试组织方式。文档与测试文件便于按模块对照学习其中对 SQLite、MongoDB 等存储方式与可视化调用的实现也为构建个人数据管道、排查网络延迟和格式转换问题提供了可复用的代码线索适合作为二次开发、性能优化和排错思路的参考样本。1. tushare 最新源代码拿到手先别急着 pip install上一份 tushare 源代码多数人的第一反应是装完跑个pro.daily()看到有数据就收工。真到批量拉全市场、字段对不上、接口突然返回空表的时候才发现自己手里只有一个黑匣子——不知道请求是怎么拼的、参数在哪被改写、错误在哪一层被吞掉。这份最新源代码的价值恰恰在这儿它把接口清单、参数字典、请求拼装和异常处理全摊开给你看能解决接口行为跟预期不一致时无从下手的问题。适合三类人要把行情数据接进自己回测/看板流水线的开发者、需要核对字段口径的数据分析岗、以及想读懂第三方数据 SDK 设计思路的工程师。下面按拆结构 → 跑起来 → 调参数 → 避坑 → 二次封装的顺序走一遍每一步都能直接抄。2. 拆源码包分层结构与你看到的接口是怎么被点出来的拿到一个 SDK 源码我一般先做两件事看目录骨架判断代码新旧再看入口文件判断接口是硬编码还是动态生成。这个顺序能省掉大量盲读。2.1 目录骨架与模块分工解开源码包顶层目录大致是这么几块不同小版本的命名会有出入但职责划分基本稳定路径职责是否需要常看tushare/__init__.py包入口暴露set_token、pro_api、老版行情函数必看改动风险最高tushare/pro/新版接口层含请求客户端与接口清单必看参数口径都在这tushare/stock/老版行情抓取类模块走页面解析按需做兼容时才翻tushare/util/日期处理、代码格式转换、通用请求封装常看排错一半时间在这tushare/data/打包进源码的静态数据如交易日历、代码对照表按需做离线校验时有用__init__.py通常只做三件事把核心对象导入到包命名空间、维护一个全局 token 状态、在pro_api()里做 token 优先级判断显式传入 环境变量 本地配置文件。很多人遇到的我明明 set_token 了为什么还报错答案往往就在这十几行里——比如某个分支下 token 被判空后直接走了匿名请求。util/目录值得单独说一句。日期字符串的补零、600000补成600000.SH这类代码后缀拼接、空值转None都是在这里做的。你从上层传进去的参数到真正发请求时可能已经被改写了两三轮排查参数问题时把断点打在这里比打在上层有效得多。2.2 接口方法不是一个个写的动态挂载机制这是整套源码里最值得看的设计。几百个接口作者不可能手写几百个def实际做法是维护一份接口清单接口名 → 参数定义 → 返回字段说明在客户端对象初始化时遍历清单用setattr把方法挂到实例上。所以你在 IDE 里补全出来的pro.daily、pro.stock_basic、pro.income全都是运行时生成的。这带来两个直接后果一是静态类型检查基本失效二是接口清单才是唯一权威文档网上二手教程写的字段名很可能已经过期。先把这个清单挖出来看看import inspect import tushare as ts # token 换成自己的下面所有示例都一样 pro ts.pro_api(你的token) # 过滤掉内置方法剩下的基本都是接口名 api_names [n for n in dir(pro) if not n.startswith(_)] print(接口数量:, len(api_names)) print(前 20 个:, api_names[:20]) # 单个接口的形参顺序比看文档快 print(inspect.signature(pro.daily)) # 典型输出: (ts_code, trade_date, start_date, end_date, ...) # 接口自带的说明文档字段口径以这里为准 print(pro.daily.__doc__[:600])逻辑说明dir()拿到的是实例上所有属性名去掉下划线开头的内置项后剩下的就是挂上去的接口inspect.signature()读的是实际函数的形参默认值这个默认值列表直接告诉你哪些参数可以空、空的默认是什么__doc__里通常带着返回字段的中文说明字段含义有歧义时以它为准别信搜索引擎里抄来抄去的老文章。参数说明dir()的结果包含你自己 setattr 上去的东西脚本里别的地方如果给 pro 对象挂过属性会被一起捞出来所以做接口遍历时最好在干净进程里跑。另外__doc__可能为None作者没写取值前判空。如果你想进一步确认清单本身长什么样可以在tushare/pro/下找那个维护接口定义的模块大概率是一个大字典或一个.json资源文件。把它的 key 全部打印出来你就得到了一份不会过期的接口目录做数据仓库映射表的时候直接拿它当主键来源。2.3 源码安装与发布版安装什么情况下必须用源码这一步是选型问题不是操作问题。pip install tushare装的是打好的发布包优点是干净、可复现缺点是有可能落后于主干字段修正和小 bug 往往先在源码里出现。pip install -e .是可编辑安装源码目录被链接进环境你改一行立刻生效。我的一般判断标准只调数据、不改行为直接pip install别折腾。要核对某个字段的原始拼装逻辑源码可编辑安装打日志看中间变量。要加本地适配比如统一重试、加缓存钩子源码可编辑安装但把改动放在自己的封装层别动tushare/pro/里的请求逻辑否则下次更新源码会被覆盖。多环境部署锁版本号装发布包源码只用来读。这里有个容易翻车的点源码目录里如果有setup.py且packages字段是用find_packages()自动发现的那么你在源码目录里临时建的子目录如果带了__init__.py可能被打进包里。做实验时把临时脚本放在仓库外的同级目录别塞进去。3. 从源码跑到第一个请求环境、安装与落库3.1 环境准备与依赖先看依赖。源码包里一般有requirements.txt或setup.py里的install_requires核心依赖通常是pandas、requests老版抓取模块还会依赖 HTML 解析库。Python 版本按setup.py里python_requires的声明来别拿太老的解释器硬跑。我习惯用独立虚拟环境因为 tushare 对 pandas 版本比较敏感大版本跨越时DataFrame.append之类的老 API 被移除会直接报AttributeError# 建环境解释器版本按 setup.py 里的声明选 python -m venv venv_ts source venv_ts/bin/activate # Windows 用 venv_ts\Scripts\activate # 先看源码包声明的依赖再决定装什么版本 cat requirements.txt 2/dev/null || grep -A20 install_requires setup.py逻辑说明第一行建隔离环境隔离的是 pandas 版本冲突不是 tushare 本身。第二行先读依赖声明再装避免一上来pip install tushare把全局环境保护的 pandas 顶掉。如果你机器上已经有别的项目在跑 pandas这一步不做后面大概率会遇到昨天还好好的今天就报错。参数说明requirements.txt不存在时退回读setup.pyinstall_requires是官方声明的依赖清单extras_require里通常还有可选依赖比如数据库驱动按需装。3.2 三种安装方式的选择# 方式一从源码目录可编辑安装改代码立即生效 cd tushare-master pip install -e . # 方式二常规安装走发布包 pip install tushare # 方式三不安装直接把源码目录挂到 sys.path临时验证用 # 在脚本开头加sys.path.insert(0, /path/to/tushare-master)逻辑说明方式一适合我要读代码并验证我的理解-e装完之后import tushare指向的就是那个目录你在源码里加个print立刻能看到。方式二适合生产环境版本可锁定。方式三只用于临时验证因为它跳过了setup.py里的元数据安装某些通过pkg_resources读版本号的逻辑会失效。参数说明-e后跟的是含setup.py的目录路径别写错层级写成pip install -e .时当前目录必须在源码根目录。如果你装了方式一又想退回方式二必须先pip uninstall tushare否则两个安装会打架import出来的到底是哪份取决于路径顺序这种玄学问题很难查。装完验证一下来源import tushare print(tushare.__file__) # 确认到底加载的是哪份代码 print(getattr(tushare, __version__, 未声明版本))__file__打印出来是源码目录说明可编辑安装成功是site-packages下的路径说明走的是发布包。排错时这行一定要先跑我见过太多次改了没生效其实是环境装串了。3.3 token 配置与最小可运行脚本token 是这套接口的准入凭证不同接口对权限等级的要求不一样这一点后面避坑章节会细说。配置方式有优先级源码里通常是显式传参 环境变量 本地配置文件。import os import tushare as ts # 推荐走环境变量别把 token 写死在脚本里提交到仓库 os.environ[TUSHARE_TOKEN] 你的token # 一次 set_token 之后同进程内 pro_api() 可以直接调 ts.set_token(os.environ[TUSHARE_TOKEN]) pro ts.pro_api() # 最小可运行请求单只标的、小时间窗口 df pro.daily( ts_code000001.SZ, # 代码带市场后缀格式错了直接返回空表 start_date20240102, end_date20240131, fieldsts_code,trade_date,open,high,low,close,vol,amount ) print(df.shape) print(df.head())逻辑说明set_token的作用是把 token 写进模块级全局状态pro_api()不传参时就读这个状态。请求侧我刻意用了小窗口和明确的fields目的是让第一次运行的结果可肉眼核对——行数不对、字段缺列、值全是 NaN三种情况分别指向不同的失败原因先用最小样本定位比直接拉三年全市场快得多。参数说明ts_code必须是代码后缀的形式纯数字会被判成非法参数start_date/end_date是YYYYMMDD字符串不是datetime对象传错类型有的版本抛异常、有的版本静默返回空表后者更坑fields传逗号分隔的字符串不传则返回全字段字段多的时候传输和内存开销会明显上来。如果返回空表先别怀疑网络按这个顺序查代码格式 → 日期是否落在交易日 → 该接口权限是否够。这三件事占了我排查时间的八成。3.4 把返回的 DataFrame 稳定落库拉下来的数据总得存最省事的方案是 SQLite单文件、零部署、适合个人流水线import sqlite3 import pandas as pd conn sqlite3.connect(market.db) def save_daily(df: pd.DataFrame, table: str daily) - int: if df is None or df.empty: return 0 # 主键去重重复拉同一区间时走替换而不是重复插入 df df.drop_duplicates(subset[ts_code, trade_date]) n df.to_sql(table, conn, if_existsappend, indexFalse) # 建唯一索引第二次跑同一区间时冲突可直接跳过 conn.execute( fCREATE UNIQUE INDEX IF NOT EXISTS idx_{table}_pk fON {table}(ts_code, trade_date) ) conn.commit() return n save_daily(df) print(pd.read_sql(SELECT COUNT(*) AS c FROM daily, conn))逻辑说明drop_duplicates处理的是同一次返回内部的重复跨批次的重复靠唯一索引兜底。用if_existsappend而不是replace是因为数据是分批增量拉的replace会把之前批次全清掉。索引在插入之后建避免每次插入都维护索引拖慢速度但如果表已经很大先建索引再插反而更快这个要按数据量试。参数说明subset必须用业务主键不同接口的主键不一样行情类通常是代码交易日财报类是代码报告期选错了去重就是无效的indexFalse不加的话 pandas 会把行号写进去多一列脏数据conn.commit()别漏某些驱动下不提交连接关闭时数据会丢。4. 参数怎么设日期、字段裁剪、限频重试与本地缓存接口能不能稳定跑八成取决于参数怎么给。这一章按最容易出错到最能提效的顺序排。4.1 日期与代码类参数格式、边界与空值日期参数统一是YYYYMMDD八位字符串中间不要带横杠不要传datetime.date。源码里虽然有一层转换尝试但转换失败时不一定抛错可能直接把非法值拼进请求参数服务端返回空结果你看到的就是一张空表——这是最典型的看起来成功其实失败。边界问题更隐蔽start_date和end_date一般是闭区间但如果你只传trade_date而同时传了start_date不同接口的优先级处理不一致有的以trade_date为准有的直接忽略后者。我的规矩是同一类参数只传一组要么传单日trade_date要么传区间别混着给。代码参数同理。ts_code带后缀symbol不带后缀同一份源码里两套命名并存混用就返回空。写代码时把格式校验前置import re from datetime import datetime DATE_RE re.compile(r^\d{8}$) def normalize_date(s: str) - str: 把 2024-01-02 / 20240102 / datetime 统一成 YYYYMMDD if isinstance(s, (datetime,)): return s.strftime(%Y%m%d) s str(s).replace(-, ).replace(/, ).strip() if not DATE_RE.match(s): raise ValueError(f日期格式不合法: {s}) return s def normalize_code(code: str, suffix: str SZ) - str: 纯数字补后缀已带后缀的原样返回 code str(code).strip().upper() if . in code: return code if len(code) ! 6 or not code.isdigit(): raise ValueError(f代码格式不合法: {code}) return f{code}.{suffix}逻辑说明这两个函数干的事就是在入口处把脏数据拦住。normalize_date兼容常见三种写法并统一输出最后一道正则校验保证拼进请求的永远是八位数字。normalize_code保留已有后缀避免把000001.SZ又拼成000001.SZ.SZ。参数说明suffix默认值要按你的实际标的池调整A 股里不同板块后缀不同硬编码一个默认值有风险更稳的做法是维护一张代码对照表从源码目录的静态数据里加载。raise而不是return None是为了让问题在入口暴露而不是等到服务端返回空表再回头查。4.2 fields 裁剪省一半传输和内存不传fields时接口返回全字段财报类接口动辄几十上百列全量拉三年数据内存能吃掉好几个 G。传fields只取你要的列效果立竿见影。DAILY_FIELDS ts_code,trade_date,open,high,low,close,pre_close,vol,amount df pro.daily( ts_code000001.SZ, start_date20240101, end_date20240329, fieldsDAILY_FIELDS ) print(df.memory_usage(deepTrue).sum() / 1024 / 1024, MB)逻辑说明字段列表写成一个模块级常量好处是要加字段只改一处而且能直接拿这个常量去校验返回的列是否齐全。参数说明字段名拼错不会抛错服务端会忽略无法识别的字段你拿到的是少几列的结果。所以落库前一定要做一次列校验把set(DAILY_FIELDS.split(,)) - set(df.columns)打出来非空就说明有字段名写错了或者接口口径变了。这个校验我放在封装层里强制执行宁可报错也别悄悄少列。4.3 限频与重试把调用包一层接口有访问频率限制按账号权限等级区分超了会抛异常异常信息里通常带每分钟访问次数之类的提示。不包重试层批量拉数据必然中断。import time from functools import wraps def with_retry(max_retry: int 4, base_wait: int 15): 针对限频做线性退避重试其他异常直接抛出 def deco(fn): wraps(fn) def wrapper(*args, **kwargs): for i in range(max_retry): try: return fn(*args, **kwargs) except Exception as e: msg str(e) if 每分钟 in msg or limit in msg.lower(): wait base_wait * (i 1) # 15s / 30s / 45s / 60s print(f触发限频{wait}s 后重试 ({i1}/{max_retry})) time.sleep(wait) continue raise # 权限、参数类错误不重试重试也不会成功 raise RuntimeError(f重试 {max_retry} 次仍失败) return wrapper return deco with_retry() def fetch_daily(pro, **kwargs): return pro.daily(**kwargs)逻辑说明核心是区分可重试异常和不可重试异常。限频是瞬时的等一会儿就好权限不足、参数非法是确定性的重试一百次结果一样还白等几十分钟。判断依据目前只能靠异常信息字符串匹配比较脆弱所以我在封装层留了个日志点接口文案变了能第一时间发现。参数说明base_wait设成 15 秒是经验值太短会被连续打回太长浪费时间max_retry4配合线性退避总计约 150 秒够覆盖一次限频窗口。如果你是多线程并发退避时间要乘线程数否则几个线程同时冲上去还是会被限。提示重试层只应该包在最外层调用点不要在源码里改请求函数加重试两处都加会叠加放大等待时间日志也会重复。4.4 本地缓存同一份数据不拉第二遍历史数据基本不变日线最多在收盘后更新一次。缓存方案我用得最多的是按接口名 参数哈希存 Parquetimport hashlib, json, os import pandas as pd CACHE_DIR ./cache os.makedirs(CACHE_DIR, exist_okTrue) def cache_key(api: str, params: dict) - str: raw api json.dumps(params, sort_keysTrue, ensure_asciiFalse) return hashlib.md5(raw.encode()).hexdigest() def cached_call(api: str, fn, **params): path os.path.join(CACHE_DIR, f{api}_{cache_key(api, params)}.parquet) if os.path.exists(path): return pd.read_parquet(path) df fn(**params) if df is not None and not df.empty: df.to_parquet(path, indexFalse) return df df cached_call(daily, pro.daily, ts_code000001.SZ, start_date20240101, end_date20240329)逻辑说明sort_keysTrue是关键参数字典顺序不同不应该产生两份缓存。命中就直接读 Parquet未命中才发请求并落盘。Parquet 比 CSV 小、读写快、还保留 dtype日期列不会读回来变成字符串。参数说明json.dumps里ensure_asciiFalse不是必需的但不加的话中文参数会被转义哈希值依然稳定只是可读性差。缓存文件不设过期时间所以当日数据不要走缓存——做增量更新时把end_date之前的部分走缓存最近 N 天强制重新拉这个策略在封装层用参数控制即可。5. 避坑与排查五个血泪翻车记录这一章按现象 → 原因 → 解决列都是我或者同行实际踩过的。5.1 现象本地正常服务器上全部返回空表原因token 的权限等级是按账号算的但还有一层是调用来源的环境限制。更常见的真实原因是服务器上的环境变量没配pro_api()走了匿名分支服务端对匿名请求返回空结果而不是报错表现就是代码没问题、结果没有。解决部署脚本里加一条启动自检把 token 的前后四位和长度打出来不要打全部同时确认tushare.__file__指向的版本。我把这一步写进了启动检查比线上接手服务后再查省事得多。5.2 现象请求返回空 DataFrame但手工用同样的参数在别的工具里能查到原因九成是参数格式问题。日期带了横杠、代码少了后缀、区间写成了start_date end_date这三种都会静默返回空表。接口层不会因为你参数不合法就抛异常它只是把参数拼进请求服务端按没匹配到数据处理。解决在封装层强制参数校验normalize_date和normalize_code各跑一遍校验失败直接抛。另外加一条守则任何返回空表的请求先把最终请求参数字典打印出来看一遍别靠脑子记。5.3 现象复权后的价格和别处对不上差异还挺大原因复权有前复权、后复权、不复权三种口径不同来源用的基准日不一样前复权是以最新价为基准往前推所以历史数据会随新数据变化。你今天拉的、上周拉的、别人给的三份数据基准日不同数值自然对不上。解决落库时把复权口径作为一个字段存下来别混在一张表里。做回测时统一用后复权基准固定历史值不会变展示时再转前复权。这个坑我在一个回测项目里连续踩了两周才发现纯粹是口径不一致。5.4 现象老版接口突然大面积返回空或者直接报错原因老版行情模块走的是页面解析页面结构一改就全废这类问题不是你的代码错。新版接口层走 HTTP 接口稳定性高得多但仍然存在字段调整的可能。解决新项目一律用 pro 接口层老版只用来做历史数据兼容。同时把字段校验做成硬性检查列名对不上立刻报警而不是写进库。如果确实需要解析页面加一层结构指纹校验解析前先确认关键元素存在不存在就抛错别让空结果流进下游。5.5 现象同一份代码昨天返回 5000 行今天返回 3000 行原因分批拉取时没有固定排序或者区间边界重合/遗漏导致重复或缺失。另一类原因是接口对单次返回条数有上限超过上限会被截断而你拿到的df.shape看起来完全正常。解决按交易日切分批次每个批次拉完立刻校验行数落在合理区间比如单日全市场股票数上下浮动 10% 以内超出范围就拆小窗口重拉。切分逻辑写成函数import pandas as pd def split_by_trade_date(cal: pd.DataFrame, start: str, end: str, step: int 20): 按交易日切批次step 控制每批包含的交易日数 days cal[(cal[cal_date] start) (cal[cal_date] end)] days days.sort_values(cal_date)[cal_date].tolist() for i in range(0, len(days), step): chunk days[i:i step] yield chunk[0], chunk[-1], len(chunk)逻辑说明cal是从接口拿到的交易日历表用它而不是自然日能避开节假日空跑。sort_values保证批次顺序稳定避免集合遍历顺序带来的随机性。参数说明step20是折中值太小请求次数多太大单次返回可能触碰条数上限被截断按你账号权限和接口实际返回上限调。yield出起始日、结束日和交易日数后者拿去和实际返回的行数做交叉校验。6. 进阶把源码当采集层的底座加一层自己的契约读完源码之后我做的最有价值的一件事是把接口调用和数据契约拆开。接口层只负责发请求、拿 DataFrame绝不做业务判断业务层持有字段清单、主键定义和行数期望任何不符立刻抛错。这样一来接口字段变更时你会在第一分钟就知道而不是在两周后的回测结果异常里发现。下面是我常用的字段契约表用字典表达直接可以被封装层消费接口主键必备字段单次行数预期备注dailyts_code trade_date开高低收、量额单日全市场正数单只标的按区间返回stock_basicts_code名称、行业、上市状态变化缓慢可周更更新频率低适合缓存trade_calcal_date是否交易日年为单位所有分批逻辑的前置依赖财报类接口ts_code 报告期报告期、科目按报告期分批字段多务必传fields校验函数长这样REQUIRED { daily: [ts_code, trade_date, close, vol], stock_basic: [ts_code, name, list_status], } def check_contract(api: str, df: pd.DataFrame, min_rows: int 1): if df is None or df.empty: raise ValueError(f{api} 返回空表) missing set(REQUIRED.get(api, [])) - set(df.columns) if missing: raise ValueError(f{api} 缺字段: {missing}) if len(df) min_rows: raise ValueError(f{api} 行数异常: {len(df)} {min_rows}) return True逻辑说明三个检查按成本从低到高排——空表检查最便宜字段检查次之行数检查需要你事先知道预期值。REQUIRED用字典管理接口清单变了只改数据不改函数。参数说明min_rows在不同场景下取值差别很大做单只标的区间拉取时设 1 就够做全市场单日拉取时要设成股票总数的九成左右设太低等于没校验。有一个细节我踩过校验放在落库之后。听着离谱但当时是为了先把数据留下再校验结果脏数据进了库清理比重拉贵十倍。从那以后我每次接新接口都强制让校验跑在写库之前并且拿最小样本先跑一遍字段和行数确认无误才放开批量。希望这些能帮你少走几段弯路。本文还有配套的精品资源点击获取