33
44from __future__ import annotations
55
6+ import hashlib
67import json
78import os
89import subprocess
1617SCORE_ALERT = 60.0
1718DRIFT_REVIEW = 0.50
1819DRIFT_CRITICAL = 0.75
20+ _ALERT_STATE_RELATIVE_PATH = Path ("data/alert-state/health_cycle.json" )
21+
22+
23+ def _collect_drift_results (run_drift_detection , * , domains = DOMAINS ):
24+ results : dict [str , list [Any ]] = {}
25+ errors : list [dict [str , str ]] = []
26+ for domain in domains :
27+ try :
28+ results [domain ] = list (run_drift_detection (domain ))
29+ except Exception as exc :
30+ errors .append (
31+ {
32+ "domain" : domain ,
33+ "code" : "drift_data_unavailable" ,
34+ "error_type" : type (exc ).__name__ ,
35+ }
36+ )
37+ return results , errors
38+
39+
40+ def _alert_fingerprint (lines : list [str ]) -> str :
41+ payload = "\n " .join (sorted (str (line ) for line in lines ))
42+ return hashlib .sha256 (payload .encode ("utf-8" )).hexdigest ()
43+
44+
45+ def _alert_state_path (root : Path ) -> Path :
46+ return root / _ALERT_STATE_RELATIVE_PATH
47+
48+
49+ def _is_duplicate_alert (root : Path , fingerprint : str ) -> bool :
50+ try :
51+ payload = json .loads (_alert_state_path (root ).read_text (encoding = "utf-8" ))
52+ except (OSError , json .JSONDecodeError ):
53+ return False
54+ return str (payload .get ("fingerprint" ) or "" ) == fingerprint
55+
56+
57+ def _record_alert (root : Path , fingerprint : str ) -> None :
58+ path = _alert_state_path (root )
59+ path .parent .mkdir (parents = True , exist_ok = True )
60+ temp_path = path .with_suffix (".tmp" )
61+ temp_path .write_text (
62+ json .dumps (
63+ {
64+ "schema_version" : "quant_monitor_alert_state.v1" ,
65+ "fingerprint" : fingerprint ,
66+ },
67+ sort_keys = True ,
68+ )
69+ + "\n " ,
70+ encoding = "utf-8" ,
71+ )
72+ temp_path .replace (path )
73+
74+
75+ def _clear_alert (root : Path ) -> None :
76+ try :
77+ _alert_state_path (root ).unlink ()
78+ except FileNotFoundError :
79+ pass
1980
2081
2182def _send_telegram (text : str ) -> bool :
@@ -26,8 +87,7 @@ def _send_telegram(text: str) -> bool:
2687 try :
2788 from quant_platform_kit .notifications .telegram import send_telegram_message
2889
29- send_telegram_message (bot_token = token , chat_ids = chat , text = text )
30- return True
90+ return bool (send_telegram_message (bot_token = token , chat_ids = chat , text = text ))
3191 except Exception :
3292 return False
3393
@@ -72,6 +132,7 @@ def main() -> int:
72132 from quant_platform_kit .strategy_lifecycle .drift_detector import run_drift_detection
73133 from quant_platform_kit .strategy_lifecycle .health_dashboard import build_dashboard
74134
135+ drift_results , drift_errors = _collect_drift_results (run_drift_detection )
75136 build_dashboard (output_dir = str (dash_dir ), output_format = "json" )
76137
77138 strategies : list [dict [str , Any ]] = []
@@ -139,7 +200,7 @@ def main() -> int:
139200
140201 issue_results : list [dict [str , Any ]] = []
141202 for domain in DOMAINS :
142- drifts = run_drift_detection (domain )
203+ drifts = drift_results . get (domain , [] )
143204 for drift in drifts :
144205 score = float (drift .drift_score or 0.0 )
145206 label = f"[{ domain } ] { drift .strategy_profile } : drift_score={ score :.2f} "
@@ -156,18 +217,36 @@ def main() -> int:
156217 body = f"Quant-monitor detected critical drift.\n \n - { line } " ,
157218 )
158219
159- notify_lines = telegram_lines + critical_lines
220+ data_error_lines = [
221+ f"[{ error ['domain' ]} ] { error ['code' ]} ({ error ['error_type' ]} )"
222+ for error in drift_errors
223+ ]
224+ if collector_payload_invalid :
225+ data_error_lines .append ("[collector] dashboard_data_unavailable" )
226+ notify_lines = telegram_lines + critical_lines + data_error_lines
227+ telegram_sent = False
228+ duplicate_alert_suppressed = False
160229 if notify_lines :
161230 body = "🚨 quant-monitor health_cycle\n " + "\n " .join (f"• { line } " for line in notify_lines )
162- _send_telegram (body )
231+ fingerprint = _alert_fingerprint (notify_lines )
232+ duplicate_alert_suppressed = _is_duplicate_alert (root , fingerprint )
233+ if not duplicate_alert_suppressed :
234+ telegram_sent = _send_telegram (body )
235+ if telegram_sent :
236+ _record_alert (root , fingerprint )
237+ else :
238+ _clear_alert (root )
163239
164240 summary = {
165241 "as_of" : datetime .now (timezone .utc ).isoformat (),
166242 "domains" : list (DOMAINS ),
167243 "strategy_count" : len (strategies ),
168244 "telegram_alerts" : notify_lines ,
245+ "telegram_sent" : telegram_sent ,
246+ "duplicate_alert_suppressed" : duplicate_alert_suppressed ,
247+ "data_errors" : drift_errors ,
169248 "issues_created" : len ([r for r in issue_results if r .get ("issue_url" )]),
170- "ok" : not notify_lines ,
249+ "ok" : not notify_lines and not collector_payload_invalid ,
171250 "collector_payload_valid" : not collector_payload_invalid ,
172251 "snapshot_data_status" : normalized_payload .get ("data_status" ),
173252 }
0 commit comments