Files
Cerbero-Bite/docs/05-data-model.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

11 KiB

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 fill, snapshot DVOL.
  • Il log è la verità per ciò che è successo. Ogni evento (entry, 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 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 (proposta, in attesa di fill, aperta, in chiusura, chiusa, cancellata).

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,            -- ETH-15MAY26-2475-P
    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         → riga creata, place_combo_order in volo
awaiting_fill    → ordine accettato da Deribit, in attesa di fill
open             → fill confermato, monitoring attivo
closing          → close combo inviata, in attesa di fill di chiusura
closed           → close fill confermato, P&L calcolato
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 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.

CREATE TABLE instructions (
    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 della response Deribit
                                                 -- (combo_instrument, order_id, state, ...)
    sent_at          TEXT NOT NULL,
    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
);

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 è HOLD o no_entry). Serve per audit e analisi delle scelte.

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, broker_error, ...
    notes            TEXT
);

CREATE INDEX idx_decisions_timestamp ON decisions(timestamp);
CREATE INDEX idx_decisions_proposal ON decisions(proposal_id);

dvol_history

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.

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). Schema previsto in vista della Fase 4.5; al momento la GUI non è implementata e la tabella resta vuota.

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 (quando esisterà) applicherà gli stessi check di safety.system_healthy() prima di eseguire.

system_state

Singleton di stato globale (kill switch, last health check, anchor audit chain).

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,
    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 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

{
  "ts": "2026-04-27T14:00:01.234Z",
  "event": "entry placed",
  "level": "info",
  "logger": "cerbero_bite.runtime.entry",
  "cycle": "entry",
  "cycle_id": "9d2a8a6a-7f58-4f10-8b33-...",
  "proposal_id": "uuid-...",
  "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 completato (boot OK)
RECOVERY_DONE Fine del recovery loop
HEALTH_OK / HEALTH_DEGRADED Health check periodico
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

  • 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 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. (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.

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 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. Lo job è registrato nello scheduler dell'orchestrator (backup_cron = "0 * * * *"); è disponibile anche come CLI python scripts/backup.py per backup ad-hoc.