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>
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user