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>
12 KiB
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 |
Sì | runtime/health_check.py |
Severity HIGH |
MCP cerbero-macro / cerbero-portfolio / cerbero-hyperliquid / cerbero-sentiment non risponde per 3 health check consecutivi |
Sì | runtime/health_check.py |
Severity HIGH |
mcp-deribit.environment_info.environment ≠ strategy.execution.environment |
Sì | 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) |
Sì | runtime/orchestrator._verify_audit_anchor |
Severity CRITICAL al boot |
| Stato SQLite incoerente con il broker (recovery non risolutivo) | Sì | runtime/recovery.py |
Severity CRITICAL al boot |
place_combo_order di chiusura respinto dal broker |
Sì | runtime/monitor_cycle.py |
Severity CRITICAL; la posizione torna in open per ritentare |
place_combo_order di apertura respinto dal broker |
Sì | 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 |
Sì | 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:
- Il processo separato
scripts/dead_man.sh(cron in user crontab, indipendente dall'engine) rileva il silenzio cercando l'ultimoHEALTH_OKnel JSONL del giorno. - Invia un alert al canale Telegram di backup (variabile
DEAD_MAN_ALERT_CMDo filedata/log/dead-man-alert.txt). - Marca SQLite con
system_state.kill_switch=1direttamente via sqlite3 CLI. - 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_hashdi ogni linea uguale all'hashdella precedente;hashricalcolato 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:
- Chaos test MCP: simula timeout/errori su ogni MCP, verifica
che il comportamento documentato in
04-mcp-integration.mdsia rispettato (retry, fallback, kill switch). - State corruption test: corrompi una riga
positionse verifica che ilrecover_statelo rilevi. - Hash chain test: modifica una linea audit e verifica che
audit verifyfallisca; tronca il file e verifica che il check anchor al boot armi il kill switch. - Replay test: rigioca una giornata storica via
cerbero-bite replay(Fase 5/6), confronta le decisioni con un set golden. - 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.