Files
Cerbero-Bite/docs/05-data-model.md
T
Adriano 881bc8a1bf Phase 0: project skeleton
- pyproject.toml with uv, deps for runtime + gui + backtest + dev
- ruff/mypy strict config, pre-commit hooks for ruff/mypy/pytest
- src/cerbero_bite/ layout with empty modules ready for Phase 1+
- structlog JSONL logger with daily rotation
- click CLI with placeholder subcommands (status, start, kill-switch,
  gui, replay, config hash, audit verify)
- 6 smoke tests passing, mypy --strict clean, ruff clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:10:30 +02:00

8.3 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 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).

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.

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.

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).

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.

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).

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

{
  "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.