简介这是一份面向金融数据分析场景的开源工具包基于tushare.pro平台实现A股行情与财务数据的一键采集适合金融分析师、量化投资者及有一定Python基础的开发者使用。压缩包共29个文件以19个Python脚本为主涵盖数据采集、监控告警、MySQL存储与多线程调度等模块另有XML配置、依赖清单、使用说明与许可证整体仅24KB结构紧凑便于快速部署和二次开发。目录基于master主分支整理包含命令行采集入口、核心库和IDE工程配置方便对照学习。已有179人学习下载说明其在同类数据工具中具有一定参考价值。通过阅读源码使用者可掌握tushare接口鉴权与调用逻辑、数据入库与更新方案、任务调度与触发方法并基于此扩展自定义选股或财务分析模块从而降低日常数据整理的时间成本。1. 从 FinHack-Collecter 说起A股数据采集卡在“能拿到”和“拿得全”之间做 A股量化或者财务分析的人多半有过这种经历tushare.pro 的接口文档翻了一遍daily、income、balancesheet 都试通了真到要跑全市场 5000 多只股票、再回看 5 年财务数据的时候发现不是被积分限流卡住就是日期参数忘了照顾停牌日要么是复权因子没对齐导致算出来的收益率差几个点。FinHack-Collecter 的价值就是把“能调用接口”这件事往前推一步做成“能持续采集、能对齐、能重跑”的数据管线。这篇文章我会从 token 配置讲到增量更新把它拆成一份可以直接照着做的落地笔记。适合手里有 tushare.pro 账号、想在本地维护一套 A股行情和财务数据库的开发者。2. 先读懂数据源再动手tushare.pro 接口体系与 FinHack-Collecter 的模块边界2.1 tushare.pro 的接口分层行情、财务、参考数据各管一摊tushare.pro 的接口不是一把抓的数据仓库它按使用场景分了层次。做 FinHack-Collecter 这类采集工具之前得先明白哪些接口是“按股票日期”拉的哪些是“按报告期”拉的。行情类接口比如daily、weekly、monthly都是按 trade_date 或者 ts_code 来查返回的是某一天或多只股票的交易记录。财务类接口比如income、balancesheet、cashflow则是按 ann_date公告日期和 end_date报告期来组织同一家公司 2023 年年报可能在 2024 年 4 月才披露你要的是公告日不是自然年结束日。这一层搞错了后面数据对齐全是乱的。FinHack-Collecter 在模块划分上遵循同样的边界行情采集器只管交易日历、日线、复权因子财务采集器只管三大报表和财务指标还有一块是基础信息比如股票列表、上市状态、行业分类这类数据是采集任务里最先落库的因为后面的所有循环都要拿它当遍历清单。从工程角度看为什么不把行情和财务放在同一个模块里跑原因很简单两者的拉取节奏完全不一样。行情是每天收盘后增量更新财务是集中在每年 1 月到 4 月密集更新而且财务数据有修正和补发需要按公告日期做全量重拉或去重更新。混在一个调度里要么行情任务被财务任务堵住要么财务漏了补发公告。2.2 交易日历是所有采集任务的“节拍器”交易日历这个接口很多人刚开始会忽略。它返回每个交易日的日期和是否开市标记看起来平淡无奇但 FinHack-Collecter 这类工具通常把它当作所有采集循环的基底遍历日期而不是遍历股票。比如要拉 2023 年全年日线正确做法是先取trade_cal得到当年所有开市日然后逐日调用daily接口一次拿全市场当日数据。这比“遍历 5000 只股票 × 逐票查全年”的调用次数低一个数量级。我在实际使用中会把交易日历先落成一张本地表每次任务启动前先查本地有没有目标日期区间如果没有再增量请求。这个习惯能省掉大量重复调用尤其是在 tushare.pro 有积分限制的情况下每次无效请求都在消耗额度。交易日历的数据量很小一年也就 250 条左右直接全量拉下来存着不要做片段更新。3. 行情数据采集把 daily 接口调成“可重跑”的本地数据表3.1 从单日全市场拉取到批量落库一个最小可用的采集脚本行情采集的核心逻辑不复杂按交易日循环调一次daily拿到全市场当天数据写进本地表。但“能跑”和“可重跑”之间差着几个细节。下面是我常用的一个最小采集脚本数据落地用了 SQLite方便后续查询。import sqlite3 import time import tushare as ts # 初始化连接和数据表 conn sqlite3.connect(ash_market.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS daily_bar ( 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, PRIMARY KEY (ts_code, trade_date) ) ) conn.commit() # 读取本地已采集的交易日避免重复拉取 def get_local_dates(): cursor.execute(SELECT DISTINCT trade_date FROM daily_bar) return set(row[0] for row in cursor.fetchall()) # 主采集逻辑按交易日逐日拉取 def fetch_daily_by_date(pro, start_date, end_date): local_dates get_local_dates() business_dates pro.trade_cal(exchangeSSE, start_datestart_date, end_dateend_date, is_open1) for item in business_dates[cal_date].values: if item in local_dates: continue # 本地已有则跳过保证可重跑 try: df pro.daily(trade_dateitem) if df is not None and not df.empty: df.to_sql(daily_bar, conn, if_existsappend, indexFalse) print(fOK {item} rows{len(df)}) time.sleep(0.5) # 控制调用频率避免触发限流 except Exception as e: print(fFAIL {item} err{e}) if __name__ __main__: ts.set_token(你的_tushare_pro_token) pro ts.pro_api() fetch_daily_by_date(pro, 20240101, 20240131)这段脚本有几个参数值得说清楚。trade_cal里的exchangeSSE指定交易所这里用上交所的日历就够因为沪深两市交易日一致is_open1过滤出开市日。pro.daily(trade_dateitem)是拿单日全市场数据一次调用返回约 5000 行刚好避开单次返回上限。time.sleep(0.5)是给限流留余量如果你的 tushare.pro 积分档位支持更高频可以调到 0.2 或去掉。主键(ts_code, trade_date)是关键——一旦某天拉了一半程序断了重跑时相同的记录不会插进去这就是“可重跑”的含义。3.2 复权因子别漏后复权和前复权的选择直接决定收益率计算只拉daily不够。上市公司会分红送股除权除息当天价格会跳空不复权的数据算出来的收益率是假的。tushare.pro 里有adj_factor接口返回每日的复权因子用daily的收盘价乘以复权因子就能得到后复权价格。社区里经常有这种帖子算某只股票 5 年收益直接拿不复权价格一算收益率和行情软件差出来一大截原因就是没处理分红送股。FinHack-Collecter 里应对这个问题的做法是daily_bar表收原始行情单独建一张adj_factor表存复权因子在计算阶段再合成前复权或后复权。为什么不在采集阶段就直接存复权价因为前复权价格会随最新除权事件变化今天下载的前复权数据下次除权后同一历史日期的数值会变存储层如果直接覆盖历史数据就全污染了。存原始价因子按需计算才是可回溯的方案。4. 财务数据落地三大报表之外的三个对齐细节4.1 报告期与公告日的双轨逻辑为什么必须用 ann_date 调度财务数据的采集容易踩的坑是拿end_date当拉取参数。end_date是报告期比如 20231231 表示 2023 年年报但这份财报实际披露日可能是 2024 年 3 月或 4 月。如果按报告期拉你确实能拿到最终数据但无法按公告节奏增量更新——一季度报的公告期你要等到所有公司披露完才能一起拉。FinHack-Collecter 的做法是按ann_date做增量轮询从本地记录的最大公告日期开始每天拉取新披露的财报写入本地。import tushare as ts ts.set_token(你的_tushare_pro_token) pro ts.pro_api() # 按公告日期增量拉取利润表start_date 取本地最近一次的公告日 df pro.income(ann_date20241015, fieldsts_code,ann_date,end_date, revenue,operate_profit,total_profit,net_income) print(df.head())pro.income里的ann_date参数传一个确切日期时返回的是当天所有发公告的公司的利润表数据。实际做增量更新时我会先把本地表里MAX(ann_date)查出来作为下次轮询的起点每次往前推进一天。接口一次调用能拉回多少条这取决于当天公告数量财报密集期一天可能上千条非财报期可能只有几十条。fields参数建议按需传字段别用默认全量响应更快也省流量。4.2 货币单位、合并报表类型、数据修正版本财务数据的三个隐性坑财务数据即使成功入库也不能直接信。第一个坑是单位不统一tushare.pro 的利润表里revenue单位是万元但现金流表某些字段也是万元而财务指标类接口里的eps单位又是元/股混用之前必须统一换算。第二个坑是报表类型balancesheet接口有个report_type参数1 代表合并报表2 代表母公司报表。做杜邦分析或者 ROE 计算原则上用合并报表如果直接拉默认值可能拿到的是母公司口径和行情软件里的指标对不上。第三个坑是数据修正上市公司会发布更正公告同一报告期可能出现多个版本的财报需要结合ann_date和update_flag字段判断哪一版是最终版FinHack-Collecter 在落库时通常会加一个rev_type字段标记数据版本方便回滚和重算。5. 避坑tushare.pro 采集中最常见的五个问题与排查路径5.1 积分限制导致频率超限现象、原因、解决现象连续拉了几十次接口后突然开始报抱歉您每分钟最多访问该接口X次。原因是 tushare.pro 对不同积分档位的接口频率限制不同daily和income这类高频接口限制尤其严格。解决方式有三层第一层是请求之间加time.sleep(0.5)到 1 秒第二层是任务失败后做指数退避重试比如第一次等 5 秒、第二次等 20 秒、第三次等 60 秒第三层是改换接口节奏比如可以用daily一次拉全市场而不是循环单只股票把调用次数从几千次降到一年 250 次。我自己的习惯是给每个接口单独配一个请求间隔参数分接口控制频率而不是全局共用一个 sleep。5.2 停牌日没有数据要不要补一行空值现象某只股票因为停牌在daily接口里没有当天的记录导致本地表那天缺行。原因停牌股本来就不产生交易接口自然返回空。但这个问题在全市场横截面拼接时暴露出来——如果直接用 DataFrame 透视成宽表停牌股那天就是 NaN回测时容易出现位置错位。解决不建议在采集层补 0也不建议跳过最好的做法是保留缺失的 ts_code 清单而非强行填值在交易信号里停牌本身就是一种状态应该保留空值并在策略层处理。5.3 交易日历和实际行情不同步异常开盘或临时休市现象某天trade_cal标记为开市但daily返回的数据量明显少于正常水平或者某半天全市场只有几十笔成交。原因极少数情况会出现交易所临时调整交易时间、或系统数据延迟。解决每次daily拉取后做行数健康检查正常全市场一天约 5000 行如果低于比如 4500就告警并延迟重试不要立刻覆盖本地数据。5.4 财务数据漏掉“更正公告”现象某只股票的净利润在首次公告是 1000 万三个月后发更正公告改成 800 万但本地库还是 1000 万。原因增量更新只按ann_date推进首次公告后代码不会再拉同一个报告期的数据。解决对财务表要定期做一次“重拉最近 N 天公告”比较update_flag是否为 2更正或者直接全量重拉最近 30 天的公告区间用主键去重。采集逻辑不能只看日期推进还要允许对同一报告期做回刷。5.5 本地表主键冲突导致重复插入现象程序没报错但表里出现同一天同一只股票的多行数据。原因如果写库时没建主键或主键设计不完整断点续跑时重复拉取的数据会被直接追加。解决在建表语句里必须明确主键PRIMARY KEY (ts_code, trade_date)对于财务表主键应该是(ts_code, end_date, rev_type, ann_date)多列组合因为同一报告期可能有多个公告版本。6. 从“能跑”到“靠谱”一个增量任务编排与数据校验的实操习惯6.1 用日期水位线控制采集任务的启动与回刷增量采集不怕重复拉怕的是不知道拉到哪里。我会在每张数据表旁边建一张collect_progress表字段就两个table_name、last_date。每天任务启动先查这张表决定从哪天开始拉。比如行情表水位线是今天财务表水位线是昨天那行情只需要拉今天的财务需要拉昨天和今天的——因为财报可能在前一天晚上发布。水位线不是只往前推还要支持手动回拨。比如你发现adj_factor数据有误把水位线往前调 30 天全部重新拉一遍即可。这个机制比定时任务直接拉“最近一年”要稳得多拉的数据量可控接口额度也不会被莫名消耗。6.2 数据校验三板斧行数、空值率、横截面数量数据落库后我会跑三条校验 SQL每一条都对应一类典型事故。第一条查行数是否异常SELECT COUNT() FROM daily_bar WHERE trade_date ‘20241014’如果行数少于 4500大概率那天数据缺失或接口返回不完整。第二条查空值率SELECT COUNT() FROM daily_bar WHERE trade_date ‘20241014’ AND close IS NULL行情接口一般不会有空值一旦出现多半是源数据端漏了。第三条查横截面唯一性SELECT ts_code, COUNT() FROM daily_bar WHERE trade_date ‘20241014’ GROUP BY ts_code HAVING COUNT() 1这条专抓重复插入。财务数据可以设计类似的校验但维度改成(ts_code, end_date)的组合。这样做的好处是错误在进入策略回测前就被拦住而不是等收益曲线飞出异常才回头找数据问题。6.3 从采集到本地因子一个可以直接复用的数据对齐片段数据采完、校验通过下一步通常是要算因子。这里给一个复权价和财务数据对齐的片段适合做基本面因子时直接拿来改。import pandas as pd # 假设 daily 表有 close原始价adj_factor 表有 adj_factor # 先合并算出后复权收盘价 df pd.merge(daily, adj_factor, on[ts_code, trade_date], howleft) df[close_adj] df[close] * df[adj_factor] # 财务数据按报告期合并注意使用 merge_asof 做时间对齐 # 因为财报披露日和交易日不是一一对应 finance finance.sort_values(ann_date) df df.sort_values(trade_date) df pd.merge_asof( df, finance, left_ontrade_date, right_onann_date, byts_code, directionbackward )merge_asof是财务类因子计算里最顺手的工具它会把每条行情记录对应到“最近一次已公告”的财报天然避免了未来函数。directionbackward的含义是找小于等于当前交易日的最新公告日。这三段代码跑通你的本地数据就不仅仅是一堆表而是一套可以直接喂给策略的因子生产线了。说回我自己的教训以前上线采集任务图省事行情和财务共用一套调度配置结果年报季财务任务把接口额度吃光当天行情停采第二天屁股后面追着补数据。从那以后我每次部署采集第一件事就是分开行情与财务的连接池、频率配置和日志文件数据校验脚本也必须跑完才能进入下一步。这个习惯帮我挡掉了太多后半夜的告警电话希望你也能提前用上。本文还有配套的精品资源点击获取