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>
This commit is contained in:
+166
-85
@@ -8,7 +8,7 @@ infrastrutturali o decisioni umane fuori posto.
|
||||
|
||||
- **Default deny**: in caso di dubbio, il sistema non fa nulla.
|
||||
- **Disarm manuale**: ogni kill switch viene disarmato esplicitamente
|
||||
da Adriano via CLI o comando Telegram, mai automaticamente.
|
||||
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
|
||||
@@ -19,45 +19,43 @@ infrastrutturali o decisioni umane fuori posto.
|
||||
|
||||
### Stato
|
||||
|
||||
`system_state.kill_switch ∈ {0, 1}`. Quando = 1, l'engine:
|
||||
`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** (richiede
|
||||
intervento manuale via CLI con flag `--force-close-position`)
|
||||
- continua a notificare i trigger di uscita ad Adriano via Telegram
|
||||
con escalation manuale
|
||||
- **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 | Note |
|
||||
|---|---|---|
|
||||
| MCP `cerbero-deribit` versione mismatch | Sì | Non procede senza schema atteso |
|
||||
| MCP `cerbero-macro` non risponde per >30 min | Sì | No entry possibile |
|
||||
| MCP `cerbero-memory` non risponde per >5 min | Sì | No esecuzione possibile |
|
||||
| Stato SQLite incoerente con broker | Sì | Diff tra DB e Deribit positions |
|
||||
| 3 health check consecutivi falliti | Sì | Engine instabile |
|
||||
| Perdita giornaliera > 3% equity | Sì | Hard prohibition Cerbero v4 |
|
||||
| Numero posizioni concorrenti > cap | Sì | Bug del sizing engine |
|
||||
| Push instruction respinta da Cerbero core | Sì | Dopo 2 retry |
|
||||
| Schema config invalido al reload | Sì | Mai partire con config rotta |
|
||||
| Comando Adriano `/kill` su Telegram | Sì | Trigger volontario |
|
||||
| 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
|
||||
|
||||
```bash
|
||||
cerbero-bite kill-switch --disarm --reason "<motivo>"
|
||||
cerbero-bite kill-switch disarm --reason "<motivo>" \
|
||||
--db data/state.sqlite \
|
||||
--audit data/audit.log
|
||||
```
|
||||
|
||||
oppure via Telegram:
|
||||
|
||||
```
|
||||
/disarm <motivo>
|
||||
```
|
||||
|
||||
In entrambi i casi il sistema chiede una conferma esplicita ("yes I am
|
||||
sure").
|
||||
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)
|
||||
|
||||
@@ -66,61 +64,113 @@ ma sono applicati in modo difensivo come **ultima linea**:
|
||||
|
||||
| Misura | Limite | Comportamento se superato |
|
||||
|---|---|---|
|
||||
| Notional combo singolo | 200 EUR | Sizing engine reject |
|
||||
| Notional combo singolo | 200 EUR | Sizing engine reject (`undersize`) |
|
||||
| Engagement totale aperto | 1.000 EUR | Sizing engine reject |
|
||||
| Posizioni concorrenti CB | 4 (default 1 per strategia) | Reject pre-push |
|
||||
| Trade aperti per giorno | 6 (intera Cerbero suite) | Lookup `cerbero-memory.daily_trade_count` |
|
||||
| Perdita giornaliera | 3% equity | Kill switch |
|
||||
| Distance short strike | < 15% spot OTM | Combo builder reject |
|
||||
| Credit / width | < 0.30 | Combo builder 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 | Hard | Entry validator reject (`funding`) |
|
||||
| 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`) |
|
||||
|
||||
Doppia verifica: il sizing_engine applica il cap, **e** il watchdog
|
||||
runtime ricontrolla il payload prima di chiamare `push_user_instruction`.
|
||||
Se le due misure divergono → kill switch + alert.
|
||||
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:
|
||||
Se l'engine non scrive un evento `HEALTH_OK` per **15 minuti**
|
||||
consecutivi:
|
||||
|
||||
1. Un processo separato `data/dead-man.sh` (cron in user crontab,
|
||||
indipendente dall'engine) rileva il silenzio.
|
||||
2. Invia alert su canale Telegram di backup.
|
||||
3. Marca SQLite con `system_state.kill_switch=1`.
|
||||
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.
|
||||
|
||||
Il dead-man è scritto in shell minimale (no dipendenze Python) per
|
||||
sopravvivere a corruzioni dell'env Python.
|
||||
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).
|
||||
linea precedente (chain di hash, stile blockchain semplificato). Il
|
||||
file viene `flush + os.fsync` a ogni append.
|
||||
|
||||
Esempio:
|
||||
|
||||
```
|
||||
2026-04-27T14:00:01Z|ENTRY_PROPOSED|{...full payload...}|prev_hash=abc123...|hash=def456...
|
||||
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`. Una
|
||||
discrepanza dell'hash chain è trattata come tampering e arma il kill
|
||||
switch.
|
||||
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
|
||||
|
||||
Tutti i flussi possono essere eseguiti in modalità `--dry-run`:
|
||||
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.
|
||||
|
||||
- Tutti i tool MCP **read-only** vengono chiamati normalmente.
|
||||
- Tool MCP **write** (`push_user_instruction`, `kb_write`, `send`) vengono
|
||||
loggati ma **non** chiamati.
|
||||
- `state.create_position` viene scritto in tabella `dry_positions`
|
||||
(schema identico a `positions` ma separata).
|
||||
|
||||
Usato per:
|
||||
- Testing in produzione prima di andare live.
|
||||
- Replay di giornate storiche per validazione.
|
||||
- Test di nuove versioni di config o algoritmi.
|
||||
`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
|
||||
|
||||
@@ -128,14 +178,33 @@ Ogni `strategy.yaml` ha:
|
||||
|
||||
```yaml
|
||||
config_version: "1.0.0"
|
||||
config_hash: "<sha256 del file senza questa riga>"
|
||||
config_hash: "<sha256 del file con il valore di config_hash sostituito da vuoto>"
|
||||
last_review: "2026-04-26"
|
||||
last_reviewer: "Adriano"
|
||||
```
|
||||
|
||||
All'avvio l'engine verifica che `config_hash` corrisponda al contenuto.
|
||||
Mismatch → kill switch (qualcuno ha modificato la config senza
|
||||
aggiornare l'hash, possibile tampering o errore umano).
|
||||
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:
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
@@ -143,39 +212,51 @@ aggiornare l'hash, possibile tampering o errore umano).
|
||||
Evento anomalo
|
||||
│
|
||||
├── Severity LOW (es. 1 health check fallito)
|
||||
│ └── Log WARNING, continua
|
||||
│ └── Append in audit chain (event=ALERT severity=low),
|
||||
│ continua
|
||||
│
|
||||
├── Severity MEDIUM (es. MCP timeout occasionale)
|
||||
│ ├── Log WARNING + Telegram digest giornaliero
|
||||
│ └── Continua, retry next cycle
|
||||
├── 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)
|
||||
│ ├── Kill switch ARM
|
||||
│ ├── Telegram alert immediato
|
||||
│ └── Adriano interviene
|
||||
├── 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. stato incoerente, hash chain rotto)
|
||||
├── Kill switch ARM
|
||||
├── Telegram + canale backup BotPapà
|
||||
├── Engine si mette in idle (no decisioni, solo monitoring)
|
||||
└── Richiede intervento umano per disarmo
|
||||
└── 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.
|
||||
2. **State corruption test**: corrompi una riga `positions` e verifica
|
||||
che il riconciliatore lo rilevi.
|
||||
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.
|
||||
4. **Replay test**: rigioca una giornata storica in dry-run, confronta
|
||||
le decisioni con un set golden.
|
||||
5. **Cap saturation test**: simula 4 posizioni concorrenti, verifica
|
||||
che il quinto trade venga rifiutato.
|
||||
`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`.
|
||||
|
||||
@@ -184,7 +265,7 @@ I risultati sono documentati in `tests/golden/results-YYYY-MM-DD.md`.
|
||||
Per chiarezza, queste cose **non** sono cap né kill switch — sono
|
||||
parte della strategia, gestite altrove:
|
||||
|
||||
- Profit take 50%: regola di strategia.
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user