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:
2026-04-28 00:03:45 +02:00
parent 466e63dc19
commit 42b0fbe1ab
20 changed files with 3715 additions and 131 deletions
+106
View File
@@ -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
)