基金数据管道的七层铠甲:从 Sina API 到微信推送的完整流水线
写完实时跟踪面板之后,有个问题一直没展开聊:数据是怎么来的。
不是「写个 cron 调一下就行了」那么简单。这条管线每天从早上九点半跑到下午三点,每 10 分钟采集 7 只基金 + 4 大指数的实时估值,收盘后跑 DCA 引擎算盈亏,22:00 生成日报,周末做周记,月末做月记。中间任何一个环节断了,不是「少一个数字」,是整条后续链路全哑火。
这篇拆开讲整条管线的架构。
一、管线全景
先把各环节摊开看:
09:30–15:00 每 10 分钟
Sina API ──→ realtime-6.py (v7) ──→ fund-tracker.py ──→ {date}.json
↓
大盘指数快照
22:00 收盘后
{date}.json ──→ daily-pl.json (DCA引擎) ──→ Flask /api/fund-pl ──→ 网页图表
↓
微信日报推送
每周五 21:00 周记草稿
每月末 21:00 月记草稿
核心就三层:
- 采集层:realtime-6.py — 从公开 API 抓实时数据
- 存储层:fund-tracker.py — 解析、对齐、落盘 JSON
- 消费层:DCA 引擎 → Flask API → 网页 / 微信推送
每一层都有自己的防崩机制。
二、采集层:三级降级链
realtime-6.py 是整条管线的最前线。它要解决一个核心问题:基金实时估值 API 已经死了。
天天基金的 fundgz.1234567.com.cn 是几乎所有基金跟踪工具依赖的数据源。它在 2026 年上半年彻底下线,一夜之间所有基于这个接口的工具全部瘫痪。
我花了差不多一周的时间,试出了现在的三级降级链:
L1:Sina ETF 实时价(主力)
6 只基金有对应的 ETF 可以映射:
008086(5G) → sh515050
017853(云) → sh516510
020631(芯片) → sh512760
017470(科芯) → sh588200
022429(A500) → sz159338
002611(黄金) → sh518880
Sina 的 ETF 行情接口是 hq.sinajs.cn,返回格式固定、解析快、不需要 Referer 校验。6 只 ETF 一枪打完,耗时不到 1 秒。
剩下那一只是 信澳 5G (016370),没有对应 ETF。它走的是另一条路:天天基金持仓 API(fundf10.eastmoney.com)拿到前十大重仓股 + 权重,再用东方财富 Push2 接口拉个股实时股价,按权重加权算估值。
这个步骤比 ETF 慢很多:持仓 API 要 2-3 秒,个股行情要再 2-3 秒。所以 L1 只对 016370 这么干,那 6 只直接走 ETF。
L2:天天基金持仓 + 东方财富(纯 HTTP 降级)
如果 Sina 挂了,或者 ETF 行情接口改格式了,切到 L2:
- 天天基金持仓 API(
fundf10.eastmoney.com)→ 拿到基金的重仓股列表 - 东方财富 Push2 接口 → 拿到每个重仓股的最新价
- 按权重加权计算估值
重点:Referer 必须填 fund.eastmoney.com,填 quote.eastmoney.com 会 404。 这个坑我踩了两天才定位到:接口返回的是中文「请检查链接是否正确」,不是 HTTP 404,日志里根本看不出来。
L2 比 L1 慢,7 只基金各拉一次持仓,再合起来拉几十只个股行情。但它是纯 HTTP,没有 Playwright 的开销。实测 7 只全走 L2 耗时约 12 秒。
L3:同花顺 Playwright(手动兜底)
前两层都挂了,还有最后一层:同花顺爱基金的网页版。这需要起一个 Playwright headless 浏览器,模拟点击和滚动去抓页面上的净值数字。
但它是手动触发,因为太慢(单个基金就要 10+ 秒)、资源开销大、而且有反爬风险。L3 存在的意义是:万一前两层全部失效,至少还有一个能用的方案,不至于全线空白。自 v6 以来实际上从未触发过 L3。
指数抓取
4 大指数(上证、深证、创业板、科创50)走的是 Sina 的另一个接口,没有基金估值那么脆弱。但 v6 踩过一个坑:Sina 的指数代码有两套格式:sh000001 和 s_sh000001。不同时间段返回可能不一样。现在的代码两套都试,谁先返回用谁。
三、存储层:时间对齐 + 容差匹配
采集层返回的数据是一堆文本行,需要解析成结构化 JSON。
这里最关键的逻辑是 match_std_time():把实际采集时间(比如 10:09:43)对齐到标准时间点(10:10),容差 ±5 分钟。
为什么要这么做?
因为 cron 的调度精度不是秒级的。那个每 10 分钟跑一次的 cron 可能在 10:09:30 触发,也可能在 10:10:45 才跑完 realtime-6.py。如果直接把时间戳写入 JSON,那不同日子的同一个「10:10 快照」可能实际是 10:09 或 10:11,后续画图时横轴就对不齐了。
容差 5 分钟意味着:10:05-10:15 的采集都归入 10:10 时点。所有后续图表和表格都用这 26 个标准时间点做横轴,不会因为 cron 抖动而乱套。
落盘格式是 ~/.hermes/fund-tracker/{YYYY-MM-DD}.json,每天一个文件:
{
"date": "2026-07-30",
"snapshots": [
{"time": "09:30", "indices": {"上证": 3401.23, ...}, "funds": {"008086": 1.2345, ...}},
...
]
}
盘中的采集脚本(cron e8210df305b4)每 10 分钟跑一次,往当天文件里追加一个快照。收盘后文件定型,不再写入。
四、消费层:DCA 引擎 + 多端消费
收盘之后,原始快照进入两条消费线。
消费线 A:DCA 引擎(daily-pl.json)
~/.hermes/fund-tracker/daily-pl.json 是整个基金 OS 的核心数据文件。它不是简单的「涨了多少跌了多少」,而是基于 DCA(定投模拟)的完整盈亏计算:
- 期初持仓从
fund-holdings.json读取(包含每只基金的真实买入记录) - 每日模拟定投:按设定金额自动「买入」份额
- 当日盈亏 = 当日市值 − 昨日市值 − 当日定投金额
- 累计盈亏 = 当日市值 − 累计投入成本
这个计算看起来简单,但有一个致命陷阱:DCA 引擎需要知道基金的历史净值来模拟定投。每天的净值数据来自实时采集的快照,如果某天的快照缺失,DCA 引擎的买入记录就会断档。
这也是为什么数据完整性如此重要:不是「少了一天的页面」,是「DCA 引擎的定投曲线永久断一个点」。
消费线 B:多端消费
DCA 引擎产出的 daily-pl.json 被 Flask 的 /api/fund-pl 端点读取,然后:
- fundtracker.html(盘中实时面板):读原始快照,画 26 个时点的折线图
- fundreturns 月视图:读 DCA 结果,画月累计盈亏
- pltrack 周/日视图:读 DCA 结果,做多粒度分析
- 真账周记/月记:
journal-draft-cron.py读 DCA 结果,生成草稿 → 微信短摘要推送 - 每日收盘报告(22:00 cron):读 DCA 结果 + Sina 收盘快照 → 生成完整日报
同一份数据、同一个 DCA 引擎。这是数据一致性的根基。任何新页面都走 /api/fund-pl 这一个端点,不会出现「demo1 和 demo4 的盈亏不一样」的问题(是的,这个 bug 发生过)。
五、数据完整性:三层防线
实时系统最大的敌人是静默失败:cron 显示 ok,但数据没更新。
针对这个问题,我上了三层防线:
第一层:脚本级重试
realtime-6.py v7 内置了 1 次 HTTP 重试(间隔 2 秒),覆盖瞬时网络抖动。超时从 v5 的 8 秒调到了 15 秒,天天基金的持仓 API 在网络差的时候确实需要 10+ 秒。
第二层:cron 级校验
daily-pl-append-actual.py(23:00 cron)会把收盘净值写入 daily-pl.json,然后做一次完整性检查:今天的数据点够 26 个吗?如果不够,标记为异常,次日手动补。
第三层:收盘后补漏
daily-pl-notify.py(23:45 cron)做最终兜底:如果 daily-pl.json 里今天的净值还没写入,或者 DCA 结果为空,触发告警推送到微信。
这三层不是一次设计出来的,而是一层一层被崩出来的。23:45 补漏是某天发现「22:00 报告正常但 daily-pl 空的」之后加的。
六、最后一公里:微信推送
数据到了 daily-pl.json,还有一个最终消费端:推送到用户手机。
微信推送走的是 Hermes cron 的 deliver 机制:
deliver: weixin:o9cq80yk5k3wsCO2FkZXTbe6LCSY@im.wechat
每个消费侧 cron 在创建时必须显式设这个 deliver。Web UI 里创建的 cron 默认推回 Web UI 会话,不会自动走到微信。
推送的内容策略也不同:
- 每日收盘报告:完整日报,包含涨跌幅、驱动因素、QDII 净值预估
- 基金异动预警(每 10 分钟):只推涨跌幅 > ±3% 的基金,短文本格式
- 周记/月记:微信只推一条短摘要 + 链接,完整草稿落盘到
demo2/*.md
微信推送有一个硬限制:iLink 速率限制。如果密集推多条,会被 cooldown。所以盘中异动预警用的是「合并推送」:一个周期内如果多只基金同时异动,合成一条消息,不是推 N 条。
七、这套管线的哲学
写到这里已经讲了将近三千字,但我真正想说的是:管线的复杂度不是一次性堆积上去的,是每一个故障都变成了代码。
- Sina ETF 接口偶尔返回乱码 → 加了解析容错
- 天天基金持仓 API 间歇性 403 → 加了 Referer 校验和备用 Header
- fundgz 彻底死了 → 建了三级降级链
- daily-pl 某天空了 → 加了 23:45 补漏
- 两个页面盈亏不同 → 统一到
/api/fund-pl
这条管线现在有 16 个基金相关的 cron 作业在跑,但不是一开始就这样的。它是被真实踩过的坑一点一点堆出来的。
如果你也在做类似的东西,一个建议:先搞核心采集链,跑通了再加防线。不要一开始就设计完美的容错体系,因为真正的错误模式只有跑起来之后才能碰到。
本文涉及的代码和配置全部在 venxine.vip 的生产环境里实际运行。管线的 Python 脚本在 ~/.hermes/skills/finance/ 下,网页路由在 fund-web/app.py 里,cron 作业在 ~/.hermes/cron/jobs.json。