Phase 4: orchestrator + cycles auto-execute
Componente runtime/ che cabla core+clients+state+safety in un engine autonomo notify-only: nessuna conferma manuale, ordini combo piazzati direttamente quando le regole passano. 311 test pass, copertura totale 94%, runtime/ 90%, mypy strict pulito, ruff clean. Moduli: - runtime/alert_manager.py: escalation tree LOW/MEDIUM/HIGH/CRITICAL → audit + Telegram + kill switch. - runtime/dependencies.py: build_runtime() costruisce RuntimeContext con tutti i client MCP, repository, audit log, kill switch, alert manager. - runtime/entry_cycle.py: flusso settimanale (snapshot parallelo spot/dvol/funding/macro/holdings/equity → validate_entry → compute_bias → options_chain → select_strikes → liquidity_gate → sizing_engine → combo_builder.build → place_combo_order → notify_position_opened). - runtime/monitor_cycle.py: loop 12h con dvol_history per il return_4h, exit_decision.evaluate, close auto-execute. - runtime/health_check.py: probe parallelo MCP + SQLite + environment match; 3 strikes consecutivi → kill switch HIGH. - runtime/recovery.py: riconciliazione SQLite vs broker all'avvio; mismatch → kill switch CRITICAL. - runtime/scheduler.py: AsyncIOScheduler builder con cron entry (lun 14:00), monitor (02/14), health (5min). - runtime/orchestrator.py: façade boot() + run_entry/monitor/health + install_scheduler + run_forever, con env check vs strategy. CLI: - start: avvia engine bloccante (asyncio.run + scheduler). - dry-run --cycle entry|monitor|health: esegue un singolo ciclo per debug/test in produzione. - stop: documenta lo shutdown via SIGTERM al container. Documentazione: - docs/06-operational-flow.md riscritto per il modello notify-only auto-execute (no conferma manuale, no memory, no brain-bridge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""Severity-based alert dispatcher (``docs/07-risk-controls.md``).
|
||||
|
||||
Routes anomalies to the right notification channel and, for HIGH and
|
||||
CRITICAL events, arms the kill switch.
|
||||
|
||||
* ``LOW`` — append to audit log only.
|
||||
* ``MEDIUM`` — audit + ``telegram.notify`` (priority="high").
|
||||
* ``HIGH`` — audit + ``telegram.notify_alert`` (priority="high")
|
||||
+ arm kill switch.
|
||||
* ``CRITICAL``— audit + ``telegram.notify_system_error``
|
||||
+ arm kill switch (already armed → idempotent).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from enum import StrEnum
|
||||
|
||||
from cerbero_bite.clients.telegram import TelegramClient
|
||||
from cerbero_bite.safety.audit_log import AuditLog
|
||||
from cerbero_bite.safety.kill_switch import KillSwitch
|
||||
|
||||
__all__ = ["AlertManager", "Severity"]
|
||||
|
||||
|
||||
_log = logging.getLogger("cerbero_bite.runtime.alert_manager")
|
||||
|
||||
|
||||
class Severity(StrEnum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class AlertManager:
|
||||
"""Dispatcher used by every runtime module to surface anomalies."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
telegram: TelegramClient,
|
||||
audit_log: AuditLog,
|
||||
kill_switch: KillSwitch,
|
||||
) -> None:
|
||||
self._telegram = telegram
|
||||
self._audit = audit_log
|
||||
self._kill = kill_switch
|
||||
|
||||
async def emit(
|
||||
self,
|
||||
severity: Severity,
|
||||
*,
|
||||
source: str,
|
||||
message: str,
|
||||
component: str | None = None,
|
||||
) -> None:
|
||||
"""Emit an alert at the given severity level."""
|
||||
self._audit.append(
|
||||
event="ALERT",
|
||||
payload={
|
||||
"severity": severity.value,
|
||||
"source": source,
|
||||
"message": message,
|
||||
"component": component,
|
||||
},
|
||||
)
|
||||
|
||||
if severity == Severity.LOW:
|
||||
_log.info("alert.low source=%s message=%s", source, message)
|
||||
return
|
||||
|
||||
if severity == Severity.MEDIUM:
|
||||
await self._telegram.notify(
|
||||
f"[{source}] {message}", priority="high", tag=source
|
||||
)
|
||||
return
|
||||
|
||||
if severity == Severity.HIGH:
|
||||
await self._telegram.notify_alert(
|
||||
source=source, message=message, priority="high"
|
||||
)
|
||||
self._kill.arm(reason=message, source=source)
|
||||
return
|
||||
|
||||
# CRITICAL
|
||||
await self._telegram.notify_system_error(
|
||||
message=message, component=component, priority="critical"
|
||||
)
|
||||
self._kill.arm(reason=message, source=source)
|
||||
|
||||
async def low(self, *, source: str, message: str) -> None:
|
||||
await self.emit(Severity.LOW, source=source, message=message)
|
||||
|
||||
async def medium(self, *, source: str, message: str) -> None:
|
||||
await self.emit(Severity.MEDIUM, source=source, message=message)
|
||||
|
||||
async def high(self, *, source: str, message: str) -> None:
|
||||
await self.emit(Severity.HIGH, source=source, message=message)
|
||||
|
||||
async def critical(
|
||||
self, *, source: str, message: str, component: str | None = None
|
||||
) -> None:
|
||||
await self.emit(
|
||||
Severity.CRITICAL, source=source, message=message, component=component
|
||||
)
|
||||
Reference in New Issue
Block a user