# 05 — Data Model Persistenza locale su SQLite (`data/state.sqlite`) + log append-only su 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. - **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. ## 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. ### `positions` Posizione Cerberus Bite (aperta, in chiusura, chiusa). ```sql CREATE TABLE positions ( proposal_id TEXT PRIMARY KEY, -- UUID v4 spread_type TEXT NOT NULL, -- bull_put, bear_call, iron_condor asset TEXT NOT NULL DEFAULT 'ETH', expiry TEXT NOT NULL, -- ISO8601 UTC short_strike NUMERIC NOT NULL, long_strike NUMERIC NOT NULL, short_instrument TEXT NOT NULL, long_instrument TEXT NOT NULL, n_contracts INTEGER NOT NULL, spread_width_usd NUMERIC NOT NULL, spread_width_pct NUMERIC NOT NULL, credit_eth NUMERIC NOT NULL, -- ricevuto effettivo (post fill) credit_usd NUMERIC NOT NULL, max_loss_usd NUMERIC NOT NULL, spot_at_entry NUMERIC NOT NULL, dvol_at_entry NUMERIC NOT NULL, delta_at_entry NUMERIC NOT NULL, eth_price_at_entry NUMERIC NOT NULL, proposed_at TEXT NOT NULL, opened_at TEXT, -- NULL se non ancora confermato fill closed_at TEXT, -- NULL se aperta close_reason TEXT, -- ENUM exit action debit_paid_eth NUMERIC, -- pagato a chiusura pnl_eth NUMERIC, pnl_usd NUMERIC, status TEXT NOT NULL, -- proposed, awaiting_fill, open, closing, closed, cancelled created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE INDEX idx_positions_status ON positions(status); 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 open → fill confermato, monitoring attivo closing → istruzione close inviata, in attesa fill closed → close fill confermato, P&L calcolato cancelled → proposta rifiutata da Adriano o no fill ``` ### `instructions` Tracciamento di ogni `push_user_instruction` inviata a Cerbero core. ```sql CREATE TABLE instructions ( instruction_id TEXT PRIMARY KEY, -- UUID dato a Cerbero core 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 sent_at TEXT NOT NULL, acknowledged_at TEXT, filled_at TEXT, cancelled_at TEXT, actual_fill_eth NUMERIC, -- prezzo medio fill effettivo actual_fees_eth NUMERIC ); CREATE INDEX idx_instructions_proposal ON instructions(proposal_id); ``` ### `decisions` Storia delle valutazioni del decision loop (anche quando l'esito è HOLD o no_entry). Serve per audit e analisi delle scelte. ```sql CREATE TABLE decisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, decision_type TEXT NOT NULL, -- entry_check, exit_check, kelly_recalib proposal_id TEXT, -- NULL per entry_check senza proposta 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 notes TEXT ); CREATE INDEX idx_decisions_timestamp ON decisions(timestamp); CREATE INDEX idx_decisions_proposal ON decisions(proposal_id); ``` ### `dvol_history` Snapshot DVOL ad ogni evaluation (utile per analisi di lungo periodo). ```sql CREATE TABLE dvol_history ( timestamp TEXT PRIMARY KEY, dvol NUMERIC NOT NULL, eth_spot NUMERIC NOT NULL ); ``` ### `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. ```sql CREATE TABLE manual_actions ( id INTEGER PRIMARY KEY AUTOINCREMENT, 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, consumed_at TEXT, -- NULL = ancora da processare consumed_by TEXT, result TEXT ); 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. ### `system_state` Singleton di stato globale (kill switch, last health check). ```sql CREATE TABLE system_state ( id INTEGER PRIMARY KEY CHECK (id = 1), kill_switch INTEGER NOT NULL DEFAULT 0, kill_reason TEXT, kill_at TEXT, last_health_check TEXT NOT NULL, last_kelly_calib TEXT, config_version TEXT NOT NULL, started_at TEXT NOT NULL ); ``` ## Log file Sotto `data/log/` un file per giorno: `cerbero-bite-YYYY-MM-DD.jsonl`. Ogni linea è un evento JSON. ### Schema evento ```json { "ts": "2026-04-27T14:00:01.234Z", "event": "ENTRY_PROPOSED", "level": "INFO", "module": "runtime.orchestrator", "proposal_id": "uuid-...", "data": { ... }, "decision_id": 12345 } ``` ### Eventi previsti | Event | Quando | |---|---| | `ENGINE_START` | Avvio engine | | `ENGINE_STOP` | Stop pulito | | `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 | ### Rotation e ritenzione - File giornaliero, gzip dopo 30 giorni. - Ritenzione 365 giorni; oltre, archiviazione manuale. - Mai cancellati i log relativi a posizioni con `pnl != 0`: archivio permanente per fini fiscali. ## 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: 1. Append evento al log file (fsync). 2. Begin transaction SQLite. 3. Update tabelle. 4. Commit. 5. Update cache RAM. Se il sistema crasha tra (1) e (5), al restart un riconciliatore confronta log vs SQLite e ripristina la consistenza. ## 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). ## Backup Backup orari di `state.sqlite` in `data/backups/state-YYYYMMDD-HH.sqlite`, ritenuti per 30 giorni. Operazione idempotente con SQLite `VACUUM INTO`.