Files
Cerbero-Bite/docs/07-risk-controls.md
T
Adriano 067f74bc89 docs: align 02/05/07 with autonomous notify-only architecture
Conclude il doc drift residuo dei tre documenti che ancora
descrivevano il modello di esercizio pre-Fase 4 (memory/brain-bridge,
push_user_instruction, conferma manuale). Aggiornati per riflettere
l'engine autonomo notify-only attuale, con tutti gli ultimi
hardening integrati.

docs/02-architecture.md:
- Diagramma a blocchi: rimosso cerbero-memory ↔ Cerbero core,
  aggiunto annotation sull'audit chain con anchor SQLite.
- Tabella stack: httpx pooling al posto dell'SDK mcp, hash chain
  con anchor in system_state.
- Layout cartelle: aggiunte runtime/lockfile.py,
  runtime/orchestrator.py, runtime/recovery.py, scripts/dead_man.sh,
  state/migrations/0002_audit_anchor.sql.
- Sequenze entry/monitor riscritte all'auto-execute via
  place_combo_order, niente attesa conferma utente.
- Nuova sezione "Lifecycle del container" con boot order, scheduler,
  SIGTERM clean shutdown, lock release.
- Failure modes aggiornati: environment mismatch, audit anchor
  mismatch, lock occupato.

docs/05-data-model.md:
- Filosofia estesa con la regola dell'audit chain e l'anchor.
- Schema instructions: payload_json riferito ai response Deribit
  (combo_instrument, order_id, state) invece di
  push_user_instruction.
- Aggiunta migration 0002_audit_anchor.sql con last_audit_hash.
- Schema log JSONL: campi cycle e cycle_id propagati da
  structlog.contextvars.
- Sezione "Audit log" descrive il formato concretamente in uso
  (separatori | con prev_hash/hash) ed elenco eventi reali
  (ENGINE_START, RECOVERY_DONE, ENTRY_PLACED, HOLD, EXIT_FILLED,
  KILL_SWITCH_*, ALERT, KELLY_RECALIBRATED).
- Sezione backup riferita allo job APScheduler ora schedulato
  (0 * * * *).

docs/07-risk-controls.md:
- Nuova tabella trigger automatici allineata al codice (column
  "Implementato" punta ai moduli runtime/safety reali).
- Sezione "Single-instance lock" introdotta (fcntl.flock,
  EngineLock, caveat multi-host).
- Sezione "Anti-truncation" che descrive il flusso anchor: callback
  on_append → SQLite → check al boot.
- "Cap di rischio" estesa con i due nuovi filter dealer-gamma e
  liquidation-heatmap (§2.8).
- Sezione "Versionamento config" cita execution.environment,
  execution.eur_to_usd, dealer_gamma_min, dealer_gamma_filter_enabled,
  liquidation_filter_enabled.
- Escalation tree concretizzata sull'AlertManager con i metodi
  reali (low/medium/high/critical).

Test: 335 pass, 1 skip (sqlite3 CLI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:04:30 +02:00

12 KiB
Raw Blame History

07 — Risk Controls

Controlli di sicurezza trasversali che non sono parte della strategia ma proteggono il sistema da bug, dati corrotti, fallimenti infrastrutturali o decisioni umane fuori posto.

Filosofia

  • Default deny: in caso di dubbio, il sistema non fa nulla.
  • Disarm manuale: ogni kill switch viene disarmato esplicitamente da Adriano via CLI, mai automaticamente.
  • Visibilità: ogni evento di sicurezza viene loggato e notificato immediatamente.
  • No silent close: una posizione viene chiusa solo a seguito di decisione esplicita dei trigger di strategia, mai per "evento sospetto".

Kill switch

Stato

system_state.kill_switch ∈ {0, 1}. Quando = 1, l'engine:

  • continua i flussi di sola lettura (health check, monitoring di stato, log)
  • non invia istruzioni di apertura
  • non invia istruzioni di chiusura automatiche (il monitor cycle salta quando il kill switch è armato)
  • continua a notificare via Telegram gli alert con la severity appropriata (vedi escalation tree)

Trigger automatici

Causa Auto-arm Implementato Note
MCP cerbero-deribit non risponde per 3 health check consecutivi runtime/health_check.py Severity HIGH
MCP cerbero-macro / cerbero-portfolio / cerbero-hyperliquid / cerbero-sentiment non risponde per 3 health check consecutivi runtime/health_check.py Severity HIGH
mcp-deribit.environment_info.environmentstrategy.execution.environment runtime/orchestrator.boot + health check Severity CRITICAL al boot, HIGH a runtime
Mismatch tra il tail del file data/audit.log e system_state.last_audit_hash (truncation o tampering) runtime/orchestrator._verify_audit_anchor Severity CRITICAL al boot
Stato SQLite incoerente con il broker (recovery non risolutivo) runtime/recovery.py Severity CRITICAL al boot
place_combo_order di chiusura respinto dal broker runtime/monitor_cycle.py Severity CRITICAL; la posizione torna in open per ritentare
place_combo_order di apertura respinto dal broker runtime/entry_cycle.py Severity HIGH; la posizione viene marcata cancelled
Hash chain audit non verifica (audit verify fallisce) Manuale per ora; CLI audit verify segnala l'anomalia con exit 2 cli.py audit verify + safety/audit_log.verify_chain Severity CRITICAL quando integrata nel boot
Comando manuale via cerbero-bite kill-switch arm cli.py kill_switch_arm Severity HIGH (operator-initiated)

Disarm

cerbero-bite kill-switch disarm --reason "<motivo>" \
    --db data/state.sqlite \
    --audit data/audit.log

L'operazione è transazionale: SQLite system_state.kill_switch = 0 + una linea KILL_SWITCH_DISARMED nella audit chain con il motivo. Il disarm non riavvia automaticamente lo scheduler; è il prossimo tick naturale (entry settimanale o monitor 12h) a far ripartire la decisione.

Cap di rischio (oltre alle regole di strategia)

Questi cap sono ridondanti rispetto a quelli del §5 della strategia, ma sono applicati in modo difensivo come ultima linea:

Misura Limite Comportamento se superato
Notional combo singolo 200 EUR Sizing engine reject (undersize)
Engagement totale aperto 1.000 EUR Sizing engine reject
Posizioni concorrenti CB 1 (default per la strategia ETH) Entry cycle reject (has_open_position)
Trade aperti per giorno 6 (intera Cerbero suite) Non implementato — richiede integrazione cross-suite, lasciato a Cerbero core
Distance short strike < 15 % spot OTM Combo builder reject (no_strike)
Credit / width < 0.30 Combo builder reject (no_strike)
Slippage ≥ 8 % credito Hard Liquidity gate reject (illiquid)
DVOL fuori [35, 90] Hard Entry validator reject (dvol)
Funding ETH-PERP ` · > 80%` annualizzato
ETH holdings > 30 % portfolio Hard Entry validator reject (holdings)
Macro evento high entro DTE Hard Entry validator reject (macro)
Dealer net gamma < dealer_gamma_min Soft (gate disabilitabile) Entry validator reject (dealer short-gamma regime) — vedi 01-strategy-rules.md §2.8
liquidation_squeeze_risk == "high" Soft (gate disabilitabile) Entry validator reject (imminent liquidation squeeze risk)

I primi sei cap sono applicati direttamente dai moduli core/; gli altri tre filtri quant-grade (DVOL, holdings, dealer-gamma, liquidation) sono applicati da entry_validator.validate_entry con soglie esplicite in strategy.yaml.

Single-instance lock

Cerbero Bite acquisisce data/.lockfile con fcntl.flock(LOCK_EX | LOCK_NB) all'avvio dell'engine (runtime/lockfile.EngineLock). Un secondo container che provasse a partire sullo stesso file di stato fallirebbe immediatamente con LockError, prima di toccare SQLite o i client MCP. Il lock viene rilasciato in modo automatico dal kernel quando il processo termina, anche su crash, quindi non rimane mai "appeso".

Caveat: flock è efficace solo all'interno dello stesso host (filesystem locale del container o bind mount). Per uno scenario distribuito multi-host servirebbe un lock service esterno (es. Redis SET NX); al momento Cerbero Bite gira in un singolo container e il lock locale è sufficiente.

Dead-man switch

Se l'engine non scrive un evento HEALTH_OK per 15 minuti consecutivi:

  1. Il processo separato scripts/dead_man.sh (cron in user crontab, indipendente dall'engine) rileva il silenzio cercando l'ultimo HEALTH_OK nel JSONL del giorno.
  2. Invia un alert al canale Telegram di backup (variabile DEAD_MAN_ALERT_CMD o file data/log/dead-man-alert.txt).
  3. Marca SQLite con system_state.kill_switch=1 direttamente via sqlite3 CLI.
  4. Adriano interviene manualmente.

Lo script è scritto in shell minimale (no dipendenze Python) per sopravvivere a corruzioni dell'env Python. La presenza del binario sqlite3 è opzionale: in sua assenza il dead-man genera comunque l'alert ma salta lo step di arming SQLite.

Audit log immutabile

Oltre al log JSONL standard, ogni decisione di trading produce una linea append-only in data/audit.log con il digest SHA-256 della linea precedente (chain di hash, stile blockchain semplificato). Il file viene flush + os.fsync a ogni append.

Esempio:

2026-04-27T14:00:01+00:00|ENTRY_PLACED|{"proposal_id":"...","spread_type":"bull_put"}|prev_hash=abc123...|hash=def456...

Verificabile retroattivamente con cerbero-bite audit verify. La verifica controlla:

  • parsing della struttura (<ts>|<event>|<json>|prev_hash=...|hash=...);
  • consistenza del JSON payload (oggetto, non lista o scalare);
  • prev_hash di ogni linea uguale all'hash della precedente;
  • hash ricalcolato uguale a quello memorizzato.

Una discrepanza è trattata come tampering e produce exit 2 dal comando CLI; in regime servirà che lo stesso check, integrato nel ciclo di health, armi il kill switch CRITICAL.

Anti-truncation

La chain così com'è descritta resta valida anche se il file viene troncato alla fine: i restanti record verificano l'uno con l'altro. Per coprire questo caso Cerbero Bite mantiene un anchor: ogni AuditLog.append invoca un callback registrato in runtime/dependencies.build_runtime che persiste l'hash appena scritto in system_state.last_audit_hash. Al boot Orchestrator._verify_audit_anchor confronta il valore persistito con il tail del file: in caso di mismatch (truncation, sostituzione, file mancante) viene armato il kill switch CRITICAL prima che qualsiasi ciclo trading parta.

Dry-run mode

Il comando cerbero-bite dry-run --cycle entry|monitor|health esegue un singolo ciclo senza avviare lo scheduler. Il ciclo usa lo stesso codice di produzione (snapshot reali, place_combo_order reale sul testnet), quindi non è "lettura sola" — è un'esecuzione one-shot. Per testare flussi senza toccare il broker si usa il Cerbero_mcp con DERIBIT_TESTNET=true (default), così mcp-deribit.environment_info riporta testnet e gli ordini vanno sul paper book.

enforce_hash è disattivato in dry-run per agevolare il debug; il comando start invece carica strategy.yaml con enforce_hash=True, quindi mismatch dell'hash producono exit 1 prima che l'engine tocchi qualsiasi stato.

Versionamento config

Ogni strategy.yaml ha:

config_version: "1.0.0"
config_hash: "<sha256 del file con il valore di config_hash sostituito da vuoto>"
last_review: "2026-04-26"
last_reviewer: "Adriano"

All'avvio di cerbero-bite start l'engine verifica che config_hash corrisponda al contenuto del file (il calcolo esclude il valore stesso del campo config_hash, vedi config/loader.compute_config_hash). Mismatch → exit 1 prima del boot. La verifica protegge da modifiche silenziose alla config, accidentali o malevole.

Nuovi campi proposti dalla migration di Fase 4 hardening:

execution:
  environment: "testnet"            # testnet|mainnet — kill switch su mismatch broker
  eur_to_usd: "1.075"               # FX di sizing, override-able via CLI flag

entry:
  dealer_gamma_min: "0"             # filtro §2.8
  dealer_gamma_filter_enabled: true
  liquidation_filter_enabled: true

Ogni cambio richiede una nuova versione di config_version, ricalcolo dell'hash via cerbero-bite config hash e commit con giustificazione testuale nel messaggio.

Escalation tree

Evento anomalo
    │
    ├── Severity LOW (es. 1 health check fallito)
    │     └── Append in audit chain (event=ALERT severity=low),
    │         continua
    │
    ├── Severity MEDIUM (es. snapshot dato mancante non bloccante)
    │     ├── Append in audit chain
    │     └── Telegram notify (priority=high), continua
    │
    ├── Severity HIGH (es. 3 health check consecutivi falliti,
    │                     entry rejected dal broker)
    │     ├── Append in audit chain
    │     ├── Telegram notify_alert (priority=high)
    │     ├── Kill switch ARM (idempotente)
    │     └── Adriano interviene per disarmare
    │
    └── Severity CRITICAL (es. mismatch environment al boot,
                              hash chain rotta, close fallito su monitor)
          ├── Append in audit chain
          ├── Telegram notify_system_error (priority=critical)
          ├── Kill switch ARM (idempotente)
          └── Engine resta in idle finché Adriano non disarma

L'implementazione vive in runtime/alert_manager.AlertManager; ciascun modulo runtime accede al manager tramite RuntimeContext.alert_manager e chiama am.low(...) / am.medium(...) / am.high(...) / am.critical(...) con source (modulo emittente) e message (descrizione human-friendly).

Test di resilienza obbligatori

Prima del go-live e ad ogni release minor:

  1. Chaos test MCP: simula timeout/errori su ogni MCP, verifica che il comportamento documentato in 04-mcp-integration.md sia rispettato (retry, fallback, kill switch).
  2. State corruption test: corrompi una riga positions e verifica che il recover_state lo rilevi.
  3. Hash chain test: modifica una linea audit e verifica che audit verify fallisca; tronca il file e verifica che il check anchor al boot armi il kill switch.
  4. Replay test: rigioca una giornata storica via cerbero-bite replay (Fase 5/6), confronta le decisioni con un set golden.
  5. Cap saturation test: simula posizioni concorrenti, verifica che il sizing engine rifiuti.

I risultati sono documentati in tests/golden/results-YYYY-MM-DD.md.

Cosa NON è un risk control

Per chiarezza, queste cose non sono cap né kill switch — sono parte della strategia, gestite altrove:

  • Profit take 50 %: regola di strategia.
  • Stop loss 1.5×: regola di strategia.
  • Vol stop +10 DVOL: regola di strategia.
  • Time stop 7 DTE: regola di strategia.

I risk control proteggono il sistema. La strategia protegge il capitale. Sono livelli diversi.