- 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>
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:
- Append evento al log file (fsync).
- Begin transaction SQLite.
- Update tabelle.
- Commit.
- 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.