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:
2026-04-29 00:04:30 +02:00
parent f4faef6fd1
commit 067f74bc89
3 changed files with 482 additions and 306 deletions
+119 -68
View File
@@ -6,21 +6,30 @@ file `.jsonl`. Niente database remoti, niente broker esterni.
## Filosofia
- **Lo stato è la verità per ciò che è in volo.** Posizioni aperte,
proposte in attesa di conferma, ack di Cerbero core.
proposte in attesa di fill, snapshot DVOL.
- **Il log è la verità per ciò che è successo.** Ogni evento (entry,
exit, errore, decisione di hold) viene scritto in modo immutabile
prima di toccare lo stato.
- Lo stato si può sempre **ricostruire dal log** in caso di corruzione.
exit, health, decisione di hold, alert) viene scritto in modo
immutabile prima di toccare lo stato.
- **L'audit chain è la verità per ciò che era stato registrato.**
Hash chain SHA-256 su `data/audit.log`, ancorata in
`system_state.last_audit_hash`: una manomissione del file viene
rilevata al boot successivo dall'orchestrator.
- Lo stato si può sempre **ricostruire dal broker** in caso di
corruzione tramite `runtime/recovery.py`.
## Schema SQLite
Migrazione gestita con file SQL semplici sotto `state/migrations/0001_init.sql`,
applicati in sequenza. No ORM heavy: si usa `sqlalchemy.core` o
addirittura `sqlite3` nativo con piccole utility.
Migrazione gestita con file SQL semplici sotto
`src/cerbero_bite/state/migrations/`, applicati in sequenza dal runner
in `state/db.py` che incrementa `PRAGMA user_version`. Forward-only.
Tutti i pragmi runtime (`foreign_keys = ON`, `journal_mode = WAL`,
`synchronous = NORMAL`) vengono applicati a ogni connessione aperta da
`state.db.connect`.
### `positions`
Posizione Cerberus Bite (aperta, in chiusura, chiusa).
Posizione Cerberus Bite (proposta, in attesa di fill, aperta, in
chiusura, chiusa, cancellata).
```sql
CREATE TABLE positions (
@@ -30,7 +39,7 @@ CREATE TABLE positions (
expiry TEXT NOT NULL, -- ISO8601 UTC
short_strike NUMERIC NOT NULL,
long_strike NUMERIC NOT NULL,
short_instrument TEXT NOT NULL,
short_instrument TEXT NOT NULL, -- ETH-15MAY26-2475-P
long_instrument TEXT NOT NULL,
n_contracts INTEGER NOT NULL,
spread_width_usd NUMERIC NOT NULL,
@@ -61,27 +70,36 @@ CREATE INDEX idx_positions_closed_at ON positions(closed_at);
Stati validi:
```
proposed → proposta inviata a Adriano, in attesa di conferma
awaiting_fill → istruzione push_user_instruction inviata, in attesa fill
proposed → riga creata, place_combo_order in volo
awaiting_fill → ordine accettato da Deribit, in attesa di fill
open → fill confermato, monitoring attivo
closing → istruzione close inviata, in attesa fill
closing → close combo inviata, in attesa di fill di chiusura
closed → close fill confermato, P&L calcolato
cancelled → proposta rifiutata da Adriano o no fill
cancelled → ordine respinto dal broker o nessun fill nella finestra
```
I `Decimal` documentati come `NUMERIC` vengono persistiti come `TEXT`
con `str(Decimal)`: la conversione avviene nel layer `Repository` per
preservare la precisione e non scivolare mai su `float`.
### `instructions`
Tracciamento di ogni `push_user_instruction` inviata a Cerbero core.
Tracciamento di ogni ordine inviato a Deribit tramite
`mcp-deribit.place_combo_order` o `cancel_order`. La tabella esiste
per audit forense: a fronte di una posizione chiusa serve poter
ricostruire la sequenza di ordini broker che hanno portato al fill.
```sql
CREATE TABLE instructions (
instruction_id TEXT PRIMARY KEY, -- UUID dato a Cerbero core
instruction_id TEXT PRIMARY KEY, -- UUID Bite-side
proposal_id TEXT NOT NULL REFERENCES positions(proposal_id),
kind TEXT NOT NULL, -- open_combo, close_combo
payload_json TEXT NOT NULL, -- JSON dell'istruzione completa
payload_json TEXT NOT NULL, -- JSON della response Deribit
-- (combo_instrument, order_id, state, ...)
sent_at TEXT NOT NULL,
acknowledged_at TEXT,
filled_at TEXT,
acknowledged_at TEXT, -- riservato a un futuro hook ack
filled_at TEXT, -- compilato dal recovery o da un
-- hook che monitora i fill
cancelled_at TEXT,
actual_fill_eth NUMERIC, -- prezzo medio fill effettivo
actual_fees_eth NUMERIC
@@ -90,6 +108,12 @@ CREATE TABLE instructions (
CREATE INDEX idx_instructions_proposal ON instructions(proposal_id);
```
Cerbero Bite invia direttamente l'ordine al broker: il payload qui
salvato è la response di `mcp-deribit.place_combo_order` (che a sua
volta wrappa `private/create_combo` + `private/buy|sell`). Niente
relazioni con un eventuale `cerbero-memory`: quel servizio non è
parte del decision loop.
### `decisions`
Storia delle valutazioni del decision loop (anche quando l'esito è
@@ -103,7 +127,7 @@ CREATE TABLE decisions (
timestamp TEXT NOT NULL,
inputs_json TEXT NOT NULL, -- snapshot input al modulo core
outputs_json TEXT NOT NULL, -- output del modulo core
action_taken TEXT, -- HOLD, no_entry, propose_open, propose_close
action_taken TEXT, -- HOLD, no_entry, propose_open, broker_error, ...
notes TEXT
);
@@ -113,7 +137,9 @@ CREATE INDEX idx_decisions_proposal ON decisions(proposal_id);
### `dvol_history`
Snapshot DVOL ad ogni evaluation (utile per analisi di lungo periodo).
Snapshot DVOL + ETH spot ad ogni evaluation. Utile per il calcolo di
`return_4h` durante il monitor (vedi `runtime/monitor_cycle.py
_fetch_return_4h`) e per analisi post-mortem.
```sql
CREATE TABLE dvol_history (
@@ -125,13 +151,15 @@ CREATE TABLE dvol_history (
### `manual_actions`
Coda di azioni manuali generate dalla GUI Streamlit (vedi `11-gui-streamlit.md`).
L'engine consuma le righe non processate via job APScheduler ogni 30s.
Coda di azioni manuali generate dalla GUI Streamlit (vedi
`11-gui-streamlit.md`). Schema previsto in vista della Fase 4.5; al
momento la GUI non è implementata e la tabella resta vuota.
```sql
CREATE TABLE manual_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- approve_proposal, reject_proposal, force_close, arm_kill, disarm_kill
kind TEXT NOT NULL, -- approve_proposal, reject_proposal,
-- force_close, arm_kill, disarm_kill
proposal_id TEXT, -- NULL se l'azione non è legata a una proposta
payload_json TEXT, -- JSON con motivo, conferma typed, ecc.
created_at TEXT NOT NULL,
@@ -142,12 +170,14 @@ CREATE TABLE manual_actions (
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);
```
Le `manual_actions` non bypassano i risk control: il consumer applica
gli stessi check di `safety.system_healthy()` prima di eseguire.
Le `manual_actions` non bypassano i risk control: il consumer
(quando esisterà) applicherà gli stessi check di
`safety.system_healthy()` prima di eseguire.
### `system_state`
Singleton di stato globale (kill switch, last health check).
Singleton di stato globale (kill switch, last health check, anchor
audit chain).
```sql
CREATE TABLE system_state (
@@ -158,54 +188,66 @@ CREATE TABLE system_state (
last_health_check TEXT NOT NULL,
last_kelly_calib TEXT,
config_version TEXT NOT NULL,
started_at TEXT NOT NULL
started_at TEXT NOT NULL,
last_audit_hash TEXT -- aggiunto dalla migration 0002
);
```
Il campo `last_audit_hash` viene aggiornato dal callback
`AuditLog.on_append` registrato in `runtime/dependencies.build_runtime`.
Al boot l'orchestrator confronta questo valore con il tail del file
`audit.log`: discrepanza → kill switch CRITICAL, vedi
`07-risk-controls.md`.
## Log file
Sotto `data/log/` un file per giorno: `cerbero-bite-YYYY-MM-DD.jsonl`.
Ogni linea è un evento JSON.
Ogni linea è un evento JSON arricchito automaticamente da
`structlog.contextvars` con `cycle` e `cycle_id` quando il record è
emesso dentro un ciclo (entry/monitor/health), in modo da poter
filtrare tutti i log relativi a una specifica esecuzione.
### Schema evento
```json
{
"ts": "2026-04-27T14:00:01.234Z",
"event": "ENTRY_PROPOSED",
"level": "INFO",
"module": "runtime.orchestrator",
"event": "entry placed",
"level": "info",
"logger": "cerbero_bite.runtime.entry",
"cycle": "entry",
"cycle_id": "9d2a8a6a-7f58-4f10-8b33-...",
"proposal_id": "uuid-...",
"data": { ... },
"decision_id": 12345
"data": { ... }
}
```
### Audit log
Il file `data/audit.log` è separato dai log JSONL ed è alimentato
esclusivamente da `safety.audit_log.AuditLog`. Ogni riga ha la forma:
```
<iso-ts>|<event>|<json-payload>|prev_hash=<hex>|hash=<hex>
```
con `hash = SHA256("<iso-ts>|<event>|<json-payload>|<prev_hash>")`.
La chain è ancorata in `system_state.last_audit_hash`. Il file è
fsync'd ad ogni append.
### Eventi previsti
| Event | Quando |
|---|---|
| `ENGINE_START` | Avvio engine |
| `ENGINE_STOP` | Stop pulito |
| `ENGINE_START` | Avvio engine completato (boot OK) |
| `RECOVERY_DONE` | Fine del recovery loop |
| `HEALTH_OK` / `HEALTH_DEGRADED` | Health check periodico |
| `MCP_CALL` | Ogni chiamata MCP (livello DEBUG) |
| `MCP_FAIL` | Fallimento MCP (livello WARN/ERROR) |
| `ENTRY_EVALUATED` | Ciclo settimanale completato |
| `ENTRY_REJECTED` | Filtri non passati |
| `ENTRY_PROPOSED` | Proposta inviata a Telegram |
| `ENTRY_CONFIRMED` | Adriano conferma |
| `ENTRY_REJECTED_BY_USER` | Adriano nega |
| `ENTRY_TIMEOUT` | Adriano non risponde nei 60 min |
| `INSTRUCTION_SENT` | push_user_instruction completato |
| `INSTRUCTION_ACK` | Cerbero core ack |
| `FILL_CONFIRMED` | Posizione effettivamente aperta |
| `EXIT_EVALUATED` | Ciclo monitor completato (anche HOLD) |
| `EXIT_PROPOSED` | Proposta chiusura |
| `EXIT_CONFIRMED` | Adriano conferma chiusura |
| `EXIT_FILLED` | Fill chiusura confermato |
| `KILL_SWITCH_ARMED` | Disarm manuale richiesto |
| `KILL_SWITCH_DISARMED` | Manuale via CLI |
| `KELLY_RECALIBRATED` | Output report mensile |
| `ENTRY_PLACED` | Combo aperto e fill (o awaiting_fill) confermato |
| `HOLD` | Monitor ha valutato una posizione e nessun trigger ha fired |
| `EXIT_FILLED` | Close confermato dal broker |
| `KILL_SWITCH_ARMED` / `KILL_SWITCH_DISARMED` | Transizioni manuali o automatiche |
| `ALERT` | Routing severity-based dell'alert manager |
| `KELLY_RECALIBRATED` | Output del job mensile (Fase 5) |
### Rotation e ritenzione
@@ -216,26 +258,35 @@ Ogni linea è un evento JSON.
## Stato in memoria
L'engine mantiene una cache delle posizioni aperte in RAM, ricaricata
da SQLite all'avvio e refreshed dopo ogni transizione di stato.
Ogni mutazione segue il pattern:
L'engine non mantiene una cache delle posizioni aperte: ogni ciclo
legge fresco da SQLite. Lo scheduler APScheduler è in-process e i job
ricevono il `RuntimeContext` (con `Repository`, `AuditLog`,
`KillSwitch`, alert manager, client MCP) dalla closure di
`Orchestrator.install_scheduler`. Ogni mutazione dei `positions`
segue il pattern:
1. Append evento al log file (fsync).
2. Begin transaction SQLite.
3. Update tabelle.
4. Commit.
5. Update cache RAM.
1. (Opzionale) Audit append per gli eventi tracciati nella chain.
2. `with transaction(conn):``repository.update_position_status(...)`
o `repository.create_*(...)`.
3. Notifica Telegram post-fact, dove pertinente.
Se il sistema crasha tra (1) e (5), al restart un riconciliatore confronta
log vs SQLite e ripristina la consistenza.
Il connessione SQLite è aperta e chiusa dentro la singola operazione,
mai mantenuta long-lived: WAL e auto-commit garantiscono che letture
da altri processi (es. CLI `state inspect`) non vedano stati parziali.
## Migrations
Lo schema viene tracciato con un counter `pragma user_version`. La
prima volta `0001_init.sql` viene applicato e versione → 1. Aggiunte
future incrementano. Nessun rollback supportato (migrations forward-only).
Lo schema viene tracciato con il counter `PRAGMA user_version`. La
prima volta `0001_init.sql` viene applicato e versione → 1; alla
seconda esecuzione (o su DB già a versione 1) `0002_audit_anchor.sql`
viene applicato e versione → 2. `state.db.run_migrations` è
idempotente. Nessun rollback supportato (migrations forward-only).
## Backup
Backup orari di `state.sqlite` in `data/backups/state-YYYYMMDD-HH.sqlite`,
ritenuti per 30 giorni. Operazione idempotente con SQLite `VACUUM INTO`.
Backup orari di `state.sqlite` in
`data/backups/state-YYYYMMDD-HH.sqlite`, ritenuti per 30 giorni.
Operazione idempotente con SQLite `VACUUM INTO`. Lo job è registrato
nello scheduler dell'orchestrator (`backup_cron = "0 * * * *"`); è
disponibile anche come CLI `python scripts/backup.py` per backup
ad-hoc.