From 56e9dc66a2a047adae7348b2e8d40bf3ba5dbab6 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Mon, 22 Jun 2026 11:33:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(monitor):=20=E4=BF=AE=E5=A4=8D=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E4=B8=AD=E5=BF=83=E8=87=AA=E5=AE=9A=E4=B9=89/AI=20?= =?UTF-8?q?=E7=AD=96=E7=95=A5=E8=A7=A6=E5=8F=91=E8=AE=B0=E5=BD=95=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=B8=8D=E6=AD=A3=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 监控中心触发记录此前只显示「策略监控」等通用 type 标签,缺少策略名和股票名, 看起来像只有内置策略能正常显示 (实际是 demo 数据的假象)。 根因: 1. 触发记录的标签写死为 TYPE_LABEL[source],未使用事件里已有的 rule_name 字段 2. 真实告警的 name 字段全为 null — enriched DataFrame 在 pipeline 计算后 drop 了 name 列 (indicators/pipeline.py),导致 row.get('name') 永远返回 None 3. 策略告警的 message 空泛 (仅「策略触发」),前端高亮 regex 无法匹配 修复 (向 demo 数据质量对齐): - 后端 MonitorRuleEngine: 新增 set_name_map(),触发时对 None 的 name 从 instruments 表回填 - 后端 _evaluate_monitors: 每轮评估前注入 symbol→name 映射 - 后端 _default_message: 策略类型改为生成「策略名」买入/卖出信号,对齐 demo 模板格式 - 前端 AlertsList: 标签优先显示规则名 (截取「策略监控 · 空中加油」→「空中加油」) 效果: 自定义/AI/内置策略统一走相同代码路径,触发记录均显示策略名+股票名+格式化消息 --- backend/app/services/quote_service.py | 11 ++++++++ backend/app/strategy/monitor.py | 36 ++++++++++++++++++++++++--- frontend/src/pages/Monitor.tsx | 7 +++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/backend/app/services/quote_service.py b/backend/app/services/quote_service.py index fdd6392..f4d4918 100644 --- a/backend/app/services/quote_service.py +++ b/backend/app/services/quote_service.py @@ -462,6 +462,17 @@ class QuoteService: if self._app_state: engine = getattr(self._app_state, "monitor_engine", None) if engine and engine.rule_count > 0: + # 预构建 symbol → name 映射 (enriched 已 drop name 列, 引擎触发时回填用) + try: + inst_df = self._app_state.repo.get_instruments() + if not inst_df.is_empty() and "symbol" in inst_df.columns and "name" in inst_df.columns: + engine.set_name_map({ + row["symbol"]: row["name"] + for row in inst_df.select(["symbol", "name"]).iter_rows(named=True) + if row.get("name") + }) + except Exception as e: # noqa: BLE001 + logger.debug("name_map 构建失败 (不影响监控): %s", e) rule_events = engine.evaluate(enriched_today) if rule_events: # 落盘到 alerts.jsonl diff --git a/backend/app/strategy/monitor.py b/backend/app/strategy/monitor.py index 10f3b32..03cdc5f 100644 --- a/backend/app/strategy/monitor.py +++ b/backend/app/strategy/monitor.py @@ -268,11 +268,21 @@ class MonitorRuleEngine: # (rule_id, symbol) → 上次触发时间戳(秒)。用于 cooldown 去重。 self._last_fire: dict[tuple[str, str], float] = {} self._strategy_engine = None # 延迟注入, type=strategy 规则用它读策略信号 + # symbol → 股票名 (enriched DataFrame 已 drop name 列, 触发时从此映射回填) + self._name_map: dict[str, str] = {} def set_strategy_engine(self, engine) -> None: """注入 StrategyEngine, type=strategy 规则据此读策略的 entry/exit_signals。""" self._strategy_engine = engine + def set_name_map(self, name_map: dict[str, str]) -> None: + """注入 symbol → 股票名 映射, 用于在告警事件里回填 name 字段。 + + enriched DataFrame 在 pipeline 计算后不含 name 列 (见 indicators/pipeline.py), + 触发时从 instruments 表预构建此映射, 保证 AlertEvent.name 有值。 + """ + self._name_map = name_map or {} + # ── 规则管理 ─────────────────────────────────────── def set_rules(self, rules: list[dict]) -> None: """批量设置规则 (覆盖)。用于启动时 reload。""" @@ -363,6 +373,8 @@ class MonitorRuleEngine: if last is not None and (now - last) < cooldown: continue # 冷却期内, 跳过 self._last_fire[key] = now + # enriched DataFrame 已 drop name 列 → 从注入的 name_map 回填 (instruments 表) + resolved_name = name if name else self._name_map.get(sym) ev = { "ts": int(now * 1000), "rule_id": rule["id"], @@ -370,7 +382,7 @@ class MonitorRuleEngine: "source": source, "type": ev_type, "symbol": sym, - "name": name, + "name": resolved_name, "message": message, "price": price, "change_pct": pct, @@ -461,8 +473,24 @@ class MonitorRuleEngine: results.append((sym, name, price, pct, hit_sigs)) return results - @staticmethod - def _default_message(rule: dict) -> str: + def _default_message(self, rule: dict) -> str: + """生成默认 message。策略类型带策略名 + 方向 (对齐 demo 模板格式, 让前端可高亮)。""" rtype = rule.get("type", "signal") - name_map = {"signal": "信号触发", "price": "价格触发", "market": "市场异动", "strategy": "策略触发"} + if rtype == "strategy": + direction = rule.get("direction", "entry") + action = {"entry": "买入", "exit": "卖出", "both": "买卖"}.get(direction, "买入") + # 从 StrategyEngine 取策略名; 失败则退化为 rule_name 里截取的部分 + sname = "" + sid = rule.get("strategy_id") + if sid and self._strategy_engine is not None: + try: + s = self._strategy_engine.get(sid) + sname = s.meta.get("name", "") or s.meta.get("id", "") + except Exception: # noqa: BLE001 + sname = "" + if not sname: + rn = rule.get("name", "") + sname = rn.split(" · ", 1)[1] if " · " in rn else (rn or "策略") + return f"策略「{sname}」{action}信号" + name_map = {"signal": "信号触发", "price": "价格触发", "market": "市场异动"} return name_map.get(rtype, "监控触发") diff --git a/frontend/src/pages/Monitor.tsx b/frontend/src/pages/Monitor.tsx index 5c8d354..0310398 100644 --- a/frontend/src/pages/Monitor.tsx +++ b/frontend/src/pages/Monitor.tsx @@ -292,7 +292,12 @@ function AlertsList({ alertsQuery, confirmClear, setConfirmClear, total, enterTs ) })()} - {TYPE_LABEL[ev.source] ?? ev.source} + {(() => { + // 优先用规则名 (如 "策略监控 · 空中加油" → "空中加油"); 退回到 type 标签 + const rn = ev.rule_name ?? '' + const dotIdx = rn.indexOf(' · ') + return dotIdx >= 0 ? rn.slice(dotIdx + 3) : (rn || (TYPE_LABEL[ev.source] ?? ev.source)) + })()}