FinBuddy 市场监控与弹窗系统
· FinBuddy
今日进展
- 新增市场监控服务:异常检测引擎 + 订阅管理 + 通知分发,用户可订阅个股/板块的涨跌异动
- 新增弹窗系统:Toast(行情提醒/批量任务/信息提示)、Modal(服务端通知)、弹窗历史面板
- 新增自选股静默预取服务:后台预加载自选股数据,打开详情页秒出
- 新增 FC 拦截层(interceptor_core):统一的功能计费拦截,所有 API 调用前先检查积分
- 对话引擎全面分析与逻辑审查报告:梳理了意图解析到 Swarm 执行的完整链路
- 前端新增 Monitor 订阅面板、Popup 上下文、K 线图组件优化
关键代码/伪代码
市场监控服务 — 异常检测 + 订阅推送
# 市场监控是 FinBuddy 从"被动问答"走向"主动推送"的关键一步
# 之前用户得自己问"XX股票今天怎么样"
# 现在可以订阅关注,异动自动推
CLASS MarketMonitorService:
"""市场监控 — 异常检测 + SSE 推送"""
ASYNC DEF check_anomalies(self):
# 遍历所有订阅,检查是否有异常
subscriptions = AWAIT self.subscription_mgr.get_active()
FOR sub IN subscriptions:
quote = AWAIT self.get_realtime_quote(sub.stock_code)
# 异常检测引擎:涨跌幅/成交量/换手率
anomaly = self.anomaly_engine.detect(quote, sub.rules)
IF anomaly:
# 通过 SSE 推送到前端
AWAIT self.notification_dispatcher.push(
user_id=sub.user_id,
event="stock_alert",
data=anomaly
)
CLASS AnomalyEngine:
"""异常检测 — 规则引擎,不依赖 LLM"""
DEF detect(self, quote, rules) -> AnomalyResult | None:
# 涨跌幅超阈值
IF abs(quote.change_pct) > rules.change_threshold:
RETURN AnomalyResult(type="price_surge", ...)
# 成交量异常放大
IF quote.volume > quote.avg_volume * rules.volume_ratio:
RETURN AnomalyResult(type="volume_surge", ...)
RETURN None
弹窗系统 — 统一事件总线
# 弹窗系统最头疼的是:各种通知来源(行情/任务/系统/服务端)
# 如果每个都自己弹,用户体验一塌糊涂
# 解决方案:统一事件总线 + 优先级队列
CLASS PopupEventBus:
"""弹窗事件总线 — 统一管理所有弹窗"""
DEF emit(self, event: PopupEvent):
# 按优先级入队:系统通知 > 行情提醒 > 信息提示
priority = self._calc_priority(event)
self.queue.push(event, priority)
# 如果当前没有弹窗显示,立即弹出
IF NOT self.is_showing:
self._show_next()
DEF _show_next(self):
# 从队列取出最高优先级的弹窗
event = self.queue.pop()
IF event.type == "toast":
# Toast:3秒自动消失(行情提醒、批量任务进度)
ToastContainer.show(event)
ELIF event.type == "modal":
# Modal:需要用户确认(服务端通知、版本更新)
BaseModal.show(event)
# 弹窗关闭后,检查队列是否还有待显示的
event.on_dismiss = self._show_next
FC 拦截层 — 统一计费
# 之前计费逻辑散落在各个 API route 里
# 有的扣了费,有的忘了扣,有的扣了两次
# 拦截层:所有 API 调用前先过一遍计费检查
CLASS InterceptorCore:
"""FC 拦截层 — 统一功能计费"""
ASYNC DEF before_request(self, feature: str, user_id: str):
# 1. 查询功能定价
pricing = AWAIT self.get_pricing(feature)
# 2. 检查用户积分是否足够
balance = AWAIT self.credit_repo.get_balance(user_id)
IF balance < pricing.cost:
RAISE InsufficientCreditsError(
f"需要 {pricing.cost} FC,当前余额 {balance} FC"
)
# 3. 冻结积分(防止并发扣费)
AWAIT self.credit_repo.freeze(user_id, pricing.cost)
# 4. 记录拦截日志
logger.info("intercept_pass", feature=feature, cost=pricing.cost)
ASYNC DEF after_request(self, feature: str, user_id: str, success: bool):
# 请求成功 → 结算扣费
IF success:
AWAIT self.credit_repo.settle(user_id, feature)
# 请求失败 → 退还冻结积分
ELSE:
AWAIT self.credit_repo.refund(user_id, feature)
遇到的问题
- 弹窗优先级冲突:行情提醒和系统通知同时来的时候,用户容易错过重要的系统通知。解决方案:系统通知走 Modal(必须确认),行情走 Toast(自动消失)
- 市场监控的 SSE 连接在 Electron 里不稳定:Electron 的渲染进程休眠时会断开 SSE。解决方案:主进程维护 SSE 连接,通过 IPC 转发到渲染进程
- FC 拦截层的冻结-结算-退款需要事务保证:并发请求时可能出现重复扣费。解决方案:冻结时加分布式锁
明日计划
- 市场监控增加更多异常规则:均线突破、MACD 金叉等
- 弹窗系统增加"免打扰"时段设置
- FinClaw_Web 端错误统一处理架构落地