☰
FinHack-Collecter实战:构建可重跑的A股行情与财务数据库
2026/10/26 21:37:48 网站建设 项目流程

简介:这是一份面向金融数据分析场景的开源工具包,基于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(exchange='SSE', start_date=start_date, end_date=end_date, is_open='1') for item in business_dates['cal_date'].values: if item in local_dates: continue # 本地已有则跳过,保证可重跑 try: df = pro.daily(trade_date=item) if df is not None and not df.empty: df.to_sql("daily_bar", conn, if_exists="append", index=False) print(f"OK {item} rows={len(df)}") time.sleep(0.5) # 控制调用频率,避免触发限流 except Exception as e: print(f"FAIL {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里的exchange='SSE'指定交易所,这里用上交所的日历就够,因为沪深两市交易日一致;is_open='1'过滤出开市日。pro.daily(trade_date=item)是拿单日全市场数据,一次调用返回约 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_date="20241015", fields="ts_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"], how="left") 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_on="trade_date", right_on="ann_date", by="ts_code", direction="backward" )

merge_asof是财务类因子计算里最顺手的工具:它会把每条行情记录对应到“最近一次已公告”的财报,天然避免了未来函数。direction='backward'的含义是,找小于等于当前交易日的最新公告日。这三段代码跑通,你的本地数据就不仅仅是一堆表,而是一套可以直接喂给策略的因子生产线了。

说回我自己的教训:以前上线采集任务图省事,行情和财务共用一套调度配置,结果年报季财务任务把接口额度吃光,当天行情停采,第二天屁股后面追着补数据。从那以后我每次部署采集,第一件事就是分开行情与财务的连接池、频率配置和日志文件,数据校验脚本也必须跑完才能进入下一步。这个习惯帮我挡掉了太多后半夜的告警电话,希望你也能提前用上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询