From 5a86f4b2c49cdf500e87a3f98b0c86470131ab54 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:59:43 +0800 Subject: [PATCH] fix: label quant health alerts as lifecycle signals Co-Authored-By: Codex --- ops/quant-monitor/AGENTS.md | 2 +- ops/quant-monitor/scripts/health_cycle.py | 12 ++++++++++-- .../tests/test_monitor_fail_closed.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ops/quant-monitor/AGENTS.md b/ops/quant-monitor/AGENTS.md index 557b0ecb..329cb8ff 100644 --- a/ops/quant-monitor/AGENTS.md +++ b/ops/quant-monitor/AGENTS.md @@ -21,7 +21,7 @@ VPS Codex 定时监控(`codex-quant.timer` 每 30 分钟)+ 收盘简报(`c 1. `sync_strategy_repos.sh` — `git pull` 四策略仓 + QPK 2. `health_cycle.py` — `build_dashboard` + `run_drift_detection` -3. `overall_score < 60` → Telegram 量化哨兵 +3. lifecycle `overall_score < 60` → Telegram 量化哨兵(研究/回测健康,不代表平台执行故障) 4. drift ≥ 0.50(~2σ)→ `create_issues_for_domain` 开 Issue(不 @) 5. drift ≥ 0.75(~3σ)→ Telegram + Issue **@owner** diff --git a/ops/quant-monitor/scripts/health_cycle.py b/ops/quant-monitor/scripts/health_cycle.py index 3a1f8cf8..47004ae2 100755 --- a/ops/quant-monitor/scripts/health_cycle.py +++ b/ops/quant-monitor/scripts/health_cycle.py @@ -161,11 +161,19 @@ def _strategy_health_alert(row: dict[str, Any]) -> tuple[str, str] | None: profile = str(row.get("strategy_profile") or "?") domain = str(row.get("domain") or "?") return ( - f"[{domain}] {profile}: health_score={score:.1f}", + f"[{domain}] {profile}: lifecycle_health_score={score:.1f}", f"strategy_health_below_{SCORE_ALERT:g}:{domain}:{profile}", ) +def _build_alert_body(lines: list[str]) -> str: + return ( + "🚨 quant-monitor strategy_lifecycle\n" + "• scope: research/backtest lifecycle health; not platform runtime execution\n" + + "\n".join(f"• {line}" for line in lines) + ) + + def _alert_state_path(root: Path) -> Path: return root / _ALERT_STATE_RELATIVE_PATH @@ -380,7 +388,7 @@ def main() -> int: telegram_sent = False duplicate_alert_suppressed = False if notify_lines: - body = "🚨 quant-monitor health_cycle\n" + "\n".join(f"• {line}" for line in notify_lines) + body = _build_alert_body(notify_lines) fingerprint = _alert_fingerprint(alert_identities) duplicate_alert_suppressed = _is_duplicate_alert(root, fingerprint) if not duplicate_alert_suppressed: diff --git a/ops/quant-monitor/tests/test_monitor_fail_closed.py b/ops/quant-monitor/tests/test_monitor_fail_closed.py index 58a0845c..6bac09d6 100644 --- a/ops/quant-monitor/tests/test_monitor_fail_closed.py +++ b/ops/quant-monitor/tests/test_monitor_fail_closed.py @@ -202,6 +202,22 @@ def test_health_cycle_score_changes_keep_the_same_incident_fingerprint(self) -> HEALTH_CYCLE._alert_fingerprint([second_identity]), ) + def test_health_cycle_labels_strategy_scores_as_lifecycle_not_runtime(self) -> None: + line, _identity = HEALTH_CYCLE._strategy_health_alert( + { + "domain": "us_equity", + "strategy_profile": "example", + "overall_score": 59.9, + } + ) + + body = HEALTH_CYCLE._build_alert_body([line]) + + self.assertIn("quant-monitor strategy_lifecycle", body) + self.assertIn("research/backtest lifecycle health", body) + self.assertIn("not platform runtime execution", body) + self.assertIn("lifecycle_health_score=59.9", body) + def test_health_cycle_non_object_alert_state_is_a_cache_miss(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp)