第 18 课 · 盘中提醒闭环

异动 → 快报 → 送达 → 人工确认:给监控系统装上"最后一公里",并学会不打扰自己

预计用时:35 分钟 上一课:第 17 课 · 整合项目(上)· 每日工作流

这节课你将得到什么

第 6 课的盘中监控 Agent 有一个尴尬的缺口:快报只打印在你电脑终端里——而异动发生时,你大概率不在终端前。这节课给它装上送达层:规则触发后,快报通过 Server酱或企业微信 webhook 推到你的手机上,你点开、判断、决定动不动手。至此,"异动 → 快报 → 送达 → 人工确认"的盘中闭环完整了。

但这节课真正想让你记住的不是"怎么发 webhook"(那是 20 行代码的事),而是两条工程纪律:第一,防骚扰是监控系统的生死线——没有冷却与上限的提醒系统,一周内就会被你自己静音;第二,webhook URL 即密钥——它泄露的后果和 API Key 泄露一模一样。

一、半自动闭环全图回顾

走到第 18 课,你的辅助决策系统每一环都对应着前面某节课的实战。把这盘棋完整看一遍:

环节干什么来自哪课谁负责
① 数据行情快照,东财→新浪→合成 逐级降级第 2、6 课机器(免费接口必挂,降级链兜底)
② 规则涨速、量能、逼近涨停 硬条件过滤第 6 课机器(便宜、稳定、可回测)
③ LLM 快报把触发事实翻译成人话 + 风险提示第 3、6 课LLM(只翻译,不指挥)
④ 送达推到你的手机,含防骚扰守卫本课机器(webhook + AlertGuard)
⑤ 人决策结合 T+1 持仓状态判断,手动下单第 1、6 课你(永远是这一环的主角)

脚本 scripts/alert_dispatcher.py 直接 import 第 6 课的 intraday_monitor(规则引擎、自选股原样复用,不重复造轮子),新增的只有两块:渠道抽象防骚扰守卫。这就是"整合"的含义:每一课造的零件,拼成一台机器。

二、送达层:渠道抽象(控制台 / Server酱 / 企业微信)

送达层的核心是一个朴素的设计:所有渠道长得一样——都有 send(title, body) 方法,上层代码只管"发",不关心发给谁。脚本里四个渠道类:

渠道类作用何时启用
ConsoleChannel打印到终端,本地留痕任何模式都启用
ShadowChannelServer酱/企业微信的"影子":只打印一行说明,绝不真发演示模式
ServerChanChannelServer酱:form 表单 POST,把消息推到你微信--send 且 URL 域名为 sctapi.ftqq.com
WeComChannel企业微信群机器人:JSON POST,推到群里--send 且 URL 域名为 qyapi.weixin.qq.com

build_channels()WEBHOOK_URL 的域名自动识别该用哪个真实渠道——你以后接钉钉、飞书、Bark,都只是再加一个同样形状的类,上层一行不用改。这就是渠道抽象的价值:提醒逻辑与送达方式解耦

三、防骚扰:监控系统的生死线

先看一个必然发生的剧情:茅台 09:45 触发"涨速+量能",提醒送达;09:50 它还在涨,同样的规则再次触发;09:55、10:00……一上午你能收到几十条内容几乎相同的提醒。第三天,你开始看到提醒就划掉。第七天,一条真正重要的提醒被你一起划掉了。这叫告警疲劳(alert fatigue)——运维领域的经典教训:淹没在噪音里的告警系统比没有告警更危险,因为它给了你"有人在盯"的错觉。

脚本里的 AlertGuard 用两条规则兜住这个问题:

防骚扰守卫本身也要可测试

AlertGuard 的时钟是可以注入的假时钟(now 参数),所以 --test 能快进时间验证"30 分钟后放行"、"跨天计数清零",不用真等半小时。这类"时间相关逻辑"是监控系统最容易出 bug 的地方,把它写成可注入依赖,是顺手但关键的工程习惯。

四、webhook 安全:URL 即密钥

Server酱的 webhook 形如 https://sctapi.ftqq.com/SCTxxxxx.send,企业微信机器人的形如 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx。注意它们没有密码、没有签名——谁拿到这个 URL,谁就能往你的微信/群里发消息。它和 LLM_API_KEY 是同等级别的秘密:

五、动手:跑起来(无需网络、无需 Key)

演示模式内置了一个三轮扫描的剧本,刻意构造"重复触发 → 被冷却拦截 → 新规则放行"的完整剧情:

cd "$HOME/AI Project/Quant Learning"
source .venv/bin/activate
python scripts/alert_dispatcher.py

你会看到(我已在你的环境实测通过,输出有删减):

运行模式:预计算快报,全部渠道打印到控制台(不真发)
防骚扰:同一标的同一规则 30 分钟内不重复;每日上限 20 条

■ 扫描 09:45 —— 规则引擎先行,不触发不打扰

========================================================
异动提醒 | 贵州茅台(600519) | 触发:涨速、量能
--------------------------------------------------------
[送达·控制台] 异动提醒 | 贵州茅台(600519) | 触发:涨速、量能
  规则明细:5分钟涨速 +2.18%(阈值 ≥ +2.00%)
  规则明细:成交量为20日均量的 2.1 倍(阈值 ≥ 2.0 倍)
  规则明细:距涨停价 1628.00 元还有 5.37%(阈值 < 1.00%)
  快报:贵州茅台盘中异动:5分钟涨速超2%,资金脉冲式流入,急涨后回落风险同步放大。成交量已达20日均量2倍以上,放量异动需分辨是进攻还是出货。决策由人做出。
  持仓提示:sellable,是否行动由你判断,Agent 不替你下单。
[送达·Server酱·演示] 不真发;--send 模式此渠道将真实推送
[送达·企业微信·演示] 不真发;--send 模式此渠道将真实推送
========================================================
  - 平安银行(000001):未触发,静默
  ……(宁德时代触发"逼近涨停",同样送达一条)……

■ 扫描 09:50 —— 规则引擎先行,不触发不打扰
  - 贵州茅台(600519):涨速(冷却中,约 30 分钟后解除) → 已抑制,不送达
  - 贵州茅台(600519):量能(冷却中,约 30 分钟后解除) → 已抑制,不送达
  - 平安银行(000001):未触发,静默
  - 宁德时代(300750):逼近涨停(冷却中,约 30 分钟后解除) → 已抑制,不送达

■ 扫描 10:05 —— 规则引擎先行,不触发不打扰
  - 贵州茅台(600519):量能(冷却中,约 30 分钟后解除) → 已抑制,不送达

========================================================
异动提醒 | 贵州茅台(600519) | 触发:逼近涨停
--------------------------------------------------------
[送达·控制台] 异动提醒 | 贵州茅台(600519) | 触发:逼近涨停
  规则明细:5分钟涨速 +0.25%(阈值 ≥ +2.00%)
  规则明细:成交量为20日均量的 2.3 倍(阈值 ≥ 2.0 倍)
  规则明细:距涨停价 1628.00 元还有 0.99%(阈值 < 1.00%)
  快报:贵州茅台盘中异动:距涨停不足1%,触板后流动性骤降,若封板失败回撤会很快。决策由人做出。
  持仓提示:sellable,是否行动由你判断,Agent 不替你下单。
========================================================
  - 平安银行(000001):未触发,静默
  - 宁德时代(300750):逼近涨停(冷却中,约 30 分钟后解除) → 已抑制,不送达

对照剧本读三处关键行为:第 2 轮,同样的规则还在触发,但全部进入冷却——你的手机保持安静,这就是防骚扰的价值;第 3 轮,茅台的"量能"仍在冷却(被拦截),但新触发的"逼近涨停"是新规则,正常送达——冷却粒度是"标的 × 规则",新信息不被连坐;每条提醒末尾都带 T+1 持仓提示(sellable / frozen),这是第 1 课的分水岭在提醒里的落位:冻结持仓的异动,你只能看,不能动。

六、验证防骚扰:--test 自检

冷却和上限不能只靠"看代码觉得对",脚本内置自检,用假时钟快进时间:

python scripts/alert_dispatcher.py --test

实测输出:

防骚扰自检(假时钟快进,无需等待):
  自检 1:同一标的同一规则 30 分钟内第二次触发 → 抑制 …… 通过
  自检 2:冷却期内触发另一条不同规则 → 放行 …… 通过
  自检 3:冷却 30 分钟过后同规则再触发 → 放行 …… 通过
  自检 4:当日第 21 条起被每日上限拦截(放行 20 条) …… 通过
  自检 5:跨天后每日计数清零 → 放行 …… 通过
结果:5/5 通过

五条全过,进程返回码 0;任何一条失败会以非零码退出——这意味着你可以把它挂进 crontab 或 CI,规则改坏了立刻知道。监控系统的每一条"保护你的规则",本身都要有测试保护。

七、接入真实渠道与真实模型(可选,课后完成)

真实送达:去 Server酱官网 微信扫码登录拿 SendKEY(免费版每天 5 条,够演示;付费版无此限),或在企业微信群里添加机器人拿 webhook,然后:

export WEBHOOK_URL="https://sctapi.ftqq.com/你的SendKEY.send"
python scripts/alert_dispatcher.py --send

真实 LLM 快报则沿用前几课的约定(OpenAI 兼容接口,默认 DeepSeek):

export LLM_API_KEY="sk-你的key"
python scripts/alert_dispatcher.py --live

两个开关互相独立:--send 只管送达层、--live 只管快报生成,可以单独开也可以一起开。WEBHOOK_URL 域名不在支持列表里时,脚本会直接报错退出而不是静默丢弃。

--send--live 两条代码路径均已写好但尚未实测(你机器上还没有 webhook 和 Key):webhook 的请求格式按 Server酱与企业微信的官方文档实现,拿到 URL/Key 后各跑一次,有报错直接贴给我。真实送达的单请求超时设为 10 秒,webhook 挂了只影响送达、不会让监控崩溃(异常会沿调用栈抛出,实盘部署时应再包一层 try/except——留个心眼)。

八、批判性思考:提醒发到你手机上之后呢

闭环的最后一环是人,而人是这个系统里最不稳定的组件。三条清醒剂:

自测

凭记忆作答,答错会显示解析。

延伸阅读

下一课预告

第 19 课 · 评估你的 Agent:系统拼完了,但它到底靠不靠谱?给情绪打分、异动快报、提醒送达建立一套"命中率"台账——用数据回答"我该信它几分",而不是凭感觉。

有任何不清楚的地方(比如渠道抽象、AlertGuard 的假时钟、webhook 的申请流程),直接问我——我是你的老师。