trading-system-docs/a3/rq_safety_telegram.py

134 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""
Red Queen — A3: Telegram Safety Notification Interface (§19).
KEIN Daemon, KEIN autonomer Sender. Dieses Modul liefert nur einen deterministischen
Formatter + Payload-Bausteine fuer relevante Safety-Events. Der spaetere Red Queen
Lead wuerde diese Payloads ueber einen existierenden Telegram-Kanal senden.
Anti-Spam-Regeln (TELEGRAM_MISSION_CONTROL):
* NUR signifikante Events: CIRCUIT_OPENED, CRITICAL-Severity, ESCALATION_REQUIRED,
HUMAN_DECISION_REQUIRED, RETRY_LIMIT_REACHED, OSCILLATION_DETECTED.
* Nicht jeder Retry braucht Telegram. RETRY_ALLOWED/DEBUG/info-Events -> kein Alert.
Payload ist deterministisch und grundsaetzlich REDACTED (keine Secrets, §28).
Test-Konvention: Tests pruefen Formatter-/Payload-Ausgabe (deterministisch), senden
aber NICHT real.
"""
from __future__ import annotations
from typing import Any, Dict
from rq_safety import (
EVENT_CIRCUIT_OPENED,
EVENT_CIRCUIT_RESET_REQUESTED,
EVENT_ESCALATION_REQUIRED,
EVENT_HUMAN_DECISION_REQUIRED,
EVENT_RETRY_LIMIT_REACHED,
EVENT_OSCILLATION_DETECTED,
EVENT_SAFETY_STATE_ERROR,
redact_secret,
)
# Meldungs-Praefixe (TELEGRAM_MISSION_CONTROL §5)
PREFIX_CRITICAL = "[CRITICAL]"
PREFIX_BLOCKER = "[BLOCKER]"
PREFIX_DECISION = "[DECISION REQUIRED]"
# Events, die einen Telegram-Alert ausloesen (signifikant, kein Spam).
ALERT_TYPES = frozenset(
{
EVENT_CIRCUIT_OPENED,
EVENT_CIRCUIT_RESET_REQUESTED,
EVENT_ESCALATION_REQUIRED,
EVENT_HUMAN_DECISION_REQUIRED,
EVENT_RETRY_LIMIT_REACHED,
EVENT_OSCILLATION_DETECTED,
EVENT_SAFETY_STATE_ERROR,
}
)
def should_notify(event_type: str, severity: str) -> bool:
"""Deterministische Spam-Regel: nur signifikante Events melden.
CRITICAL-Severity wird immer gemeldet; ansonsten nur in ALERT_TYPES gelistete
Event-Typen. RETRY_ALLOWED / INFO / gewoehnliche Events -> kein Alert.
"""
if severity == "CRITICAL":
return True
return event_type in ALERT_TYPES
def format_alert(
event_type: str,
severity: str,
*,
mission_id: Any = None,
wp_id: Any = None,
scope_type: Any = None,
scope_id: Any = None,
reason: str = "",
reason_code: str = "",
) -> str:
"""Baue eine deterministische, maschinenlesbare Telegram-Nachricht.
Ohne Secrets (redacted). Gibt einen menschenlesbaren Block zurueck, der mit
einem festen Praefix beginnt. Kein Alert bei nicht-signifikanten Events -> "".
"""
if not should_notify(event_type, severity):
return "" # kein Alert -> keine Nachricht (Anti-Spam)
prefix = PREFIX_CRITICAL if severity == "CRITICAL" else (
PREFIX_DECISION
if event_type in (EVENT_HUMAN_DECISION_REQUIRED, EVENT_ESCALATION_REQUIRED)
else PREFIX_BLOCKER
)
scope = f"scope={scope_type}/{scope_id or '-'}" if scope_type else "scope=n/a"
lines = [
f"{prefix} Red Queen Safety",
f"EVENT={event_type} SEVERITY={severity}",
f"REASON_CODE={reason_code or 'n/a'}",
scope,
f"mission={mission_id or 'n/a'}" + (f" wp={wp_id}" if wp_id else ""),
f"reason={redact_secret(reason) or 'n/a'}",
]
return "\n".join(lines)
def build_payload(
event_type: str,
severity: str,
*,
reason: str = "",
reason_code: str = "",
scope_type: Any = None,
scope_id: Any = None,
mission_id: Any = None,
wp_id: Any = None,
evidence_ref: Any = None,
) -> Dict[str, Any]:
"""Strukturierte Payload fuer einen Telegram-Sender (deterministisch, redacted).
Return-Format ist stabil; ein spaeterer Sender muss nur diesen Dict-Payload
an den konfigurierten Messenger senden.
"""
return {
"notify": should_notify(event_type, severity),
"text": format_alert(
event_type, severity, reason=reason, reason_code=reason_code,
scope_type=scope_type, scope_id=scope_id,
mission_id=mission_id, wp_id=wp_id,
),
"event_type": event_type,
"severity": severity,
"reason_code": reason_code,
"reason": redact_secret(reason),
"scope_type": scope_type,
"scope_id": scope_id,
"mission_id": mission_id,
"wp_id": wp_id,
"evidence_ref": evidence_ref,
}