监控层与数据源扩展¶
更新:2026-10-03。本文记录两件事:实时能力与数据源的调研结论,以及监控层 在本仓库的落地方式。调研分两路:对参考产品(TradingVane)的实测拆解,以及对 GitHub 开源生态的选型调查(结论均核对过仓库元数据与源码,非二手转述)。
一、参考产品的实时能力拆解(tradingvane.com)¶
TradingVane 是一个面向交易员的全球市场情报仪表盘。拆解它的实时链路,是为了 回答一个问题:"异动喊人"这件事,成熟形态长什么样。
实时链路¶
| 环节 | 观察到的实现 |
|---|---|
| 传输 | 前端 SPA 建立长连接后推送(界面态:LIVE / ▲ 新消息 / 正在建立实时情报连接…),非轮询式刷新 |
| 门控 | 首屏先过一个混淆的 tv-guard 脚本建立"短期安全视觉会话",会话不可用时降级保留静态版本而不是白屏 |
| 渲染 | /library/ 用滚动驱动的图像序列,按 devicePixelRatio × deviceMemory × connection.effectiveType 分 4k/hd 两档预加载 |
可借鉴的是降级策略:建立不了实时连接时展示最后可得的内容,而不是报错。 这与我们监控层的取向一致(见 §三「取数失败不中断循环」)。
数据源¶
| 类别 | 来源 | 备注 |
|---|---|---|
| A 股资金 | 同花顺 · 东方财富(沪深主力净额) | 页面直接标注来源 |
| 全球新闻 | GDELT | 关键词自动生成"最近 7 天"全球新闻追踪源 |
| 订阅源 | RSS 2.0 / Atom + OPML 导入 | 用户可自建追踪源 |
| 头条 | 多信源实时聚合,含"突发 · 宏观"分类 | — |
| 日历 | 经济日历(驱动"下一重大事件"倒计时) | — |
| 其他 | 全球指数、汇率、大宗、央行、冲突与灾害 | manifest 自述覆盖范围 |
一个刻意的反面样本:它内置的"披萨指数"页面自己写明 非实时 · 时段模型,
数据仅由华盛顿当地小时权重生成,不使用真实订单、客流或官方活动数据。
把不可靠的指标显式标注为不可靠——这个习惯值得我们对每个新数据源都坚持
(对应本仓库的「证明不了就别出数」纪律)。
对我们的结论¶
参考对象的价值在产品形态(订阅源可扩展、异动触发、降级兜底),不在前端工程 (AbortController 长连接、混淆守卫、逐 release canary 那套是为反克隆服务的,与 我们无关)。我们缺的是它有的常驻盯盘 + 推送,而不是它的视觉层。
二、GitHub 生态调研¶
结论 1:A 股没有免费的真流式行情,轮询是正确解¶
免费库(akshare / easyquotation / efinance / adata / mootdx)全部是 HTTP 轮询,
包括新浪 hq.sinajs.cn 和腾讯 qt.gtimg.cn 这两个"看起来像推送"的端点。
真 tick 流只有三条路:付费统一 API、TDX WebSocket 协议、券商终端(QMT/xtquant、Futu OpenD)。
因此监控层设计为轮询循环 + 可调间隔,而不是 socket 订阅者。已落地的
realtime_data.py 走腾讯主源 + 新浪兜底,这正是生态里(如 PanWatch、Ashare)
反复出现的组合。
结论 2:PanWatch 是本项目监控层最接近的架构范本¶
TNT-Likely/PanWatch(~1.9k★,MIT,Python 3.10+)
是自托管的 A 股/港股/美股监控器:规则引擎 + 调度扫描 + 多渠道通知 + 冷却/日限额/过期。
它的 price_alert_engine.py 里有三条经验,其中一条我们已经实现:
- 落盘先于通知(durability ordering) —— 原文要求"外部通道 I/O 只在命中和
收件箱事件持久化之后开始"。一次通知超时绝不能让告警丢失。
→ 本仓库已实现:
WatchEngine.tick()的顺序是store.append()→store.mark_sent()→notifier.send(),并有一条测试专门锁住这个顺序(test_callback_failure_does_not_lose_event)。 - 门控链 ——
enabled → expire_at → 交易时段 → 每日上限 → 只触发一次 → 冷却。 → 我们已实现交易时段与冷却;每日上限与过期未做(见 §五)。 - TTL 缓存 —— 报价 5s、K 线 60s,避免在轮询循环里反复捶打数据源。 → 未做,当前每个 tick 都真实请求。
调度上它用 AsyncIOScheduler,作业配置为
jitter=20, coalesce=True, max_instances=1, replace_existing=True ——
这是扫描循环的正确参数组合(防重入、防堆积、防惊群)。
结论 3:不要为六个操作符引入重量级规则引擎¶
评估了 durable_rules(MIT,但 206 个未决 issue,Rete/状态机模型远重于
"价格 > X 且 涨幅 > Y")、business-rules(面向业务决策表,不面向流式行情状态)。
结论是自己写小 DSL —— 即本仓库 monitor/rules.py 的「规则 dict + 纯函数求值器」,
约 150 行、可脱离网络单测、许可证清晰。
数据源选型(按可用性排序)¶
| 用途 | 选择 | ★ | 许可证 | Key | 判断 |
|---|---|---|---|---|---|
| 实时快照 | 腾讯 qt.gtimg.cn + 新浪 hq.sinajs.cn |
— | 端点公开 | 无 | 已采用;腾讯字段最全(含量比、涨跌停价) |
| 批量快照 | easyquotation | ~3k | MIT | 无 | 标的很多时值得引入,一次请求取一批 |
| 深度/更快 | mootdx(TDX 协议) | ~1.5k | MIT | 无 | pytdx 的维护替代品;注意 2024-07 后未更新,需钉版本 |
| 市场宽度/历史 | akshare | ~22.8k | MIT | 无 | 已是核心依赖;慢,适合 EOD/宏观,不适合进轮询循环 |
| 全球/美股/汇率 | yfinance | ~15k | Apache-2.0 | 无 | 非官方,需包一层容错 |
| 加密 | ccxt | ~35k | MIT | 无 | — |
| 宏观 | FRED | — | 官方 API | 免费 key | 接 vintage 时必须钉在 as-of 日(见 前视偏差防护 A10 缺口) |
| 新闻 | GDELT 2.0 Doc API | — | 公开 API | 无 | 直接调 HTTP,见下方风险 |
| RSS | feedparser | — | BSD-2 | 无 | 接财联社/东财/同花顺电报 |
许可证风险登记表(重要)¶
| 项目 | 风险 | 处置 |
|---|---|---|
mpquant/Ashare、sngyai/Sequoia-X |
无许可证 | 只读设计,不复制代码 |
rainx/pytdx |
已归档(2020)+ 无许可证 | 不用,用 mootdx |
freqtrade、sansan0/TrendRadar、linwoodc3/gdeltPyR |
GPL-3.0 | 本项目 MIT,仅作设计参考,不复制代码;GDELT 自己调 API |
eltdx |
许可证 NOASSERTION |
用前先确认 |
investpy |
上游 README 自称已坏 | 不用;investiny 是逆向 investing.com,脆弱且 ToS 有风险 |
durable_rules |
非许可证问题,是过重 | 不引入 |
三、本仓库已落地¶
实时行情源 dataflows/realtime_data.py¶
- 腾讯
qt.gtimg.cn主源、新浪hq.sinajs.cn兜底,均免 Key; - 解析出涨跌幅、换手率、量比、振幅、PB、总市值、涨停/跌停价等字段;
- 硬门控:调用方给了
curr_date且不等于运行当天 → 抛VendorError拒绝出数。 实时快照无法证明分析日当时可知,喂进--date回测就是前视偏差; - 按
dataflows/errors.py契约抛类型化错误(限流 →VendorRateLimitError, 全源无数据 →NoMarketDataError,传输异常 →VendorError); - 已注册进
VENDOR_METHODS["get_realtime_quote"],可走route_to_vendor。
监控层 monitor/¶
与 graph/(LLM 流水线)刻意解耦:监控层不认识 LLM,可以长时间常驻,
成本只有行情请求。
| 模块 | 职责 |
|---|---|
events.py |
MonitorEvent 事件模型 + 严重度分级(info/notice/warning/critical) |
rules.py |
规则 DSL:change_pct / price / limit_move / near_limit / turnover / amount / volume_ratio / amplitude,支持按标的覆盖 |
engine.py |
轮询循环、交易时段门控、去重、落盘、推送;on_event 钩子供上层触发深度分析 |
notify.py |
通知分发:console / webhook / 企业微信 / 钉钉 / 飞书 / Server酱 / PushPlus / Bark |
store.py |
JSONL 台账 + 冷却状态(原子写,重启不重复喊) |
设计上沿用了调研结论:落盘先于推送;单条规则写错不拖垮整批求值;通道发送 失败只记 warning 不抛出;取数失败跳过本轮而不退出进程。
CLI¶
astock-trader watch 600519 000001 --once # 跑一轮,核对规则
astock-trader watch 600519 --interval 15 # 常驻
astock-trader watch --test-notify # 验证通知通道
配置集中在 ~/.astock_trader/monitor.json(标的 + 规则 + 通知通道)。
覆盖测试¶
tests/test_monitor.py + tests/test_realtime_data.py:规则边界、冷却与重启去重、
通道分发与失败隔离、交易时段边界、引擎在取数失败/回调失败下的行为、
以及历史日期门控。
四、数据源广度:扩展清单¶
已落地:GDELT 全球新闻源 dataflows/gdelt_data.py¶
对接 GDELT 2.0 DOC API(免 Key),补上"全球事件面"的空白——地缘冲突、灾害、
政策、供应链这类会外溢到 A 股的事件,比个股新闻更早出现。已作为
get_global_news 的第三顺位(mx → eastmoney → gdelt)接入路由表。
时点门控:把查询区间上界钉死在分析日 23:59:59(enddatetime),历史运行
看不到分析日之后的报道。
两个必须知道的限制:
- GDELT 对出口 IP 限流"每 5 秒一次",429 响应里写得很直白。本模块做了
客户端节流(
_MIN_INTERVAL_S),避免多个 Agent 并发把自己打成 429。 - 共享出口会被别人拖累。实测:同一个查询在直连时 200、走本地代理时 429
—— 代理出口 IP 已被其他用户打满。用得上的话给该域名单独走直连
(
NO_PROXY=api.gdeltproject.org)。另外 DOC API 只索引最近约 3 个月, 更早的历史需要 GDELT 归档文件,此时模块抛NoMarketDataError让路由换源。
待办(按"免费 + 免 Key + 许可证干净"排序)¶
- feedparser + RSS(BSD-2)—— 财联社/东财电报流,成本极低、稳定性高。
- easyquotation(MIT)—— 监控标的变多后替换手写批量取数。
- 全球指数/汇率(yfinance,Apache-2.0)—— 给 A 股判断补宏观上下文。
- 北向资金 / 涨停板 / 板块资金流(akshare 已有对应函数,接进
VENDOR_METHODS即可)。
每接一个新源,必须过 前视偏差防护 §「新增数据源时的检查清单」。
五、仍未做(诚实清单)¶
| 缺口 | 说明 |
|---|---|
| 触发的深度分析 | WatchEngine.on_event 钩子已留好,但尚未接 analyze 流水线;即"异动 → 自动跑一次 15 角色分析"还没打通 |
| 每日触发上限 / 规则过期 | PanWatch 门控链里的这两项未实现,长时间常驻时噪音可能偏大 |
| 交易日历 | 当前只按周一至周五 + 时段判断,法定节假日会照常空转轮询(多花请求,不会出错) |
| 调度器 | 现在是朴素 sleep 循环,未换成 APScheduler(换来 jitter/coalesce/max_instances 的防重入保证) |
| 报价 TTL 缓存 | 每 tick 都真实请求;标的多、间隔短时会放大对数据源的压力 |
| 通知通道 | 自研零依赖实现,未采用 Apprise(BSD-2,~17.5k★)。取舍见下 |
关于不引入 Apprise 的取舍:Apprise 一个依赖覆盖 90% 通道、维护成本低; 但它对 企业微信/ServerChan/PushPlus 的支持较薄(PanWatch 也仍需为这几个自写 httpx 适配器),且会引入新依赖。当前自研实现覆盖了本项目需要的全部通道且零依赖, 代价是通道种类要自己维护。若后续通道需求明显变多,再评估切换。