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>
This commit is contained in:
+119
-68
@@ -6,21 +6,30 @@ 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.
|
||||
proposte in attesa di fill, snapshot DVOL.
|
||||
- **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.
|
||||
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 `state/migrations/0001_init.sql`,
|
||||
applicati in sequenza. No ORM heavy: si usa `sqlalchemy.core` o
|
||||
addirittura `sqlite3` nativo con piccole utility.
|
||||
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 (aperta, in chiusura, chiusa).
|
||||
Posizione Cerberus Bite (proposta, in attesa di fill, aperta, in
|
||||
chiusura, chiusa, cancellata).
|
||||
|
||||
```sql
|
||||
CREATE TABLE positions (
|
||||
@@ -30,7 +39,7 @@ CREATE TABLE positions (
|
||||
expiry TEXT NOT NULL, -- ISO8601 UTC
|
||||
short_strike NUMERIC NOT NULL,
|
||||
long_strike NUMERIC NOT NULL,
|
||||
short_instrument TEXT 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,
|
||||
@@ -61,27 +70,36 @@ 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
|
||||
proposed → riga creata, place_combo_order in volo
|
||||
awaiting_fill → ordine accettato da Deribit, in attesa di fill
|
||||
open → fill confermato, monitoring attivo
|
||||
closing → istruzione close inviata, in attesa fill
|
||||
closing → close combo inviata, in attesa di fill di chiusura
|
||||
closed → close fill confermato, P&L calcolato
|
||||
cancelled → proposta rifiutata da Adriano o no fill
|
||||
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 `push_user_instruction` inviata a Cerbero core.
|
||||
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 dato a Cerbero core
|
||||
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 dell'istruzione completa
|
||||
payload_json TEXT NOT NULL, -- JSON della response Deribit
|
||||
-- (combo_instrument, order_id, state, ...)
|
||||
sent_at TEXT NOT NULL,
|
||||
acknowledged_at TEXT,
|
||||
filled_at TEXT,
|
||||
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
|
||||
@@ -90,6 +108,12 @@ CREATE TABLE instructions (
|
||||
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 è
|
||||
@@ -103,7 +127,7 @@ CREATE TABLE decisions (
|
||||
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
|
||||
action_taken TEXT, -- HOLD, no_entry, propose_open, broker_error, ...
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
@@ -113,7 +137,9 @@ CREATE INDEX idx_decisions_proposal ON decisions(proposal_id);
|
||||
|
||||
### `dvol_history`
|
||||
|
||||
Snapshot DVOL ad ogni evaluation (utile per analisi di lungo periodo).
|
||||
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 (
|
||||
@@ -125,13 +151,15 @@ CREATE TABLE dvol_history (
|
||||
|
||||
### `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.
|
||||
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
|
||||
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,
|
||||
@@ -142,12 +170,14 @@ CREATE TABLE manual_actions (
|
||||
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.
|
||||
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).
|
||||
Singleton di stato globale (kill switch, last health check, anchor
|
||||
audit chain).
|
||||
|
||||
```sql
|
||||
CREATE TABLE system_state (
|
||||
@@ -158,54 +188,66 @@ CREATE TABLE system_state (
|
||||
last_health_check TEXT NOT NULL,
|
||||
last_kelly_calib TEXT,
|
||||
config_version TEXT NOT NULL,
|
||||
started_at 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.
|
||||
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_PROPOSED",
|
||||
"level": "INFO",
|
||||
"module": "runtime.orchestrator",
|
||||
"event": "entry placed",
|
||||
"level": "info",
|
||||
"logger": "cerbero_bite.runtime.entry",
|
||||
"cycle": "entry",
|
||||
"cycle_id": "9d2a8a6a-7f58-4f10-8b33-...",
|
||||
"proposal_id": "uuid-...",
|
||||
"data": { ... },
|
||||
"decision_id": 12345
|
||||
"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 |
|
||||
| `ENGINE_STOP` | Stop pulito |
|
||||
| `ENGINE_START` | Avvio engine completato (boot OK) |
|
||||
| `RECOVERY_DONE` | Fine del recovery loop |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
@@ -216,26 +258,35 @@ Ogni linea è un evento JSON.
|
||||
|
||||
## 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:
|
||||
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. Append evento al log file (fsync).
|
||||
2. Begin transaction SQLite.
|
||||
3. Update tabelle.
|
||||
4. Commit.
|
||||
5. Update cache RAM.
|
||||
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.
|
||||
|
||||
Se il sistema crasha tra (1) e (5), al restart un riconciliatore confronta
|
||||
log vs SQLite e ripristina la consistenza.
|
||||
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 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).
|
||||
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`.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user