067f74bc89
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>
293 lines
11 KiB
Markdown
293 lines
11 KiB
Markdown
# 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).
|
|
|
|
```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, -- 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.
|
|
|
|
```sql
|
|
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.
|
|
|
|
```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, 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.
|
|
|
|
```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`). 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
|
|
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).
|
|
|
|
```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,
|
|
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
|
|
|
|
```json
|
|
{
|
|
"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.
|