# 06 — Flussi operativi Tre flussi principali, tutti orchestrati dallo scheduler interno APScheduler. ## Flusso 1 — Avvio engine ``` 1. Carica strategy.yaml + strategy.local.yaml (override) 2. Validazione schema (pydantic) — fail veloce 3. Acquisisci lock file (data/.lockfile) 4. Apri SQLite, esegui migrations 5. Health check MCP server (versioni + ping) 6. Riconciliatore stato: a. legge positions con status in {awaiting_fill, open, closing} b. per ognuna chiama cerbero-memory.is_acknowledged + Deribit get_positions c. allinea SQLite alla realtà del broker (sempre la realtà vince) d. log delle discrepanze come WARNING 7. Health check sistema OK? → arma scheduler KO? → kill switch + alert + idle 8. Log ENGINE_START ``` L'avvio è progettato per essere **safe**: se qualcosa non torna, il sistema si rifiuta di operare. Mai partire con uno stato dubbio. ## Flusso 2 — Settimanale (entry) Trigger: cron `0 14 * * MON` (lunedì 14:00 UTC). ``` START ├── safety.system_healthy()? → no → log + skip ├── state.has_open_position()? → yes → log + skip ├── snapshot dati di mercato (parallel): │ spot, dvol, funding_perp, funding_cross, macro_calendar, │ holdings_pct, capital ├── entry_validator.validate_entry → fail → log ENTRY_REJECTED + reasons ├── entry_validator.compute_bias → None → log ENTRY_REJECTED ("no_bias") ├── deribit.get_options_chain (DTE 14-21) ├── combo_builder.select_strikes → None → log ENTRY_REJECTED ("no_strike") ├── deribit.get_orderbook per le 2 gambe ├── liquidity_gate.check → fail → log ENTRY_REJECTED ("illiquid") ├── sizing_engine.compute_contracts → 0 → log ENTRY_REJECTED ("undersize") ├── combo_builder.build → ComboProposal ├── greeks_aggregator.aggregate ├── reporting.pre_trade(proposal) → testo Telegram ├── telegram.request_confirmation(timeout=60min) │ ├── confermato: │ │ ├── state.create_position (status="proposed") │ │ ├── memory.push_user_instruction │ │ ├── state.update(status="awaiting_fill") │ │ └── log ENTRY_CONFIRMED │ ├── rifiutato: │ │ └── log ENTRY_REJECTED_BY_USER │ └── timeout: │ └── log ENTRY_TIMEOUT └── END ``` ### Tempo previsto end-to-end - Snapshot dati: 3-5 secondi - Algoritmi puri: < 0.5 secondi - Conferma utente: variabile (max 60 min) Il critical path automatico è **sotto 10 secondi**. La latenza umana non rallenta i prezzi: se Adriano risponde dopo 30 minuti, l'engine prima di inviare l'istruzione **rivaluta** spot, mid e dvol e abortisce l'apertura se le condizioni sono cambiate (slippage > 8% o dvol fuori banda o macro nuova). ## Flusso 3 — Monitoring (12h) Trigger: cron `0 2,14 * * *` (02:00 e 14:00 UTC, ogni giorno). ``` START ├── safety.system_healthy()? → no → kill switch (no auto-close ciechi) ├── per ogni position con status="open": │ ├── snapshot: │ │ spot, dvol, mark_combo, delta_short, return_4h │ ├── exit_decision.evaluate │ ├── action == HOLD: │ │ └── log EXIT_EVALUATED("hold") │ ├── action == CLOSE_*: │ │ ├── reporting.exit_proposal(position, decision) │ │ ├── telegram.request_confirmation(timeout=30min) │ │ │ ├── confermato: │ │ │ │ ├── memory.push_user_instruction (close) │ │ │ │ ├── state.update(status="closing") │ │ │ │ └── log EXIT_CONFIRMED │ │ │ ├── rifiutato: │ │ │ │ └── log EXIT_DEFERRED (resterà in HOLD fino a prossimo ciclo) │ │ │ └── timeout: │ │ │ └── escalation: per CLOSE_STOP/CLOSE_VOL/CLOSE_DELTA │ │ │ → invia comunque l'istruzione (urgenza prevale) │ │ │ per altri motivi → log EXIT_TIMEOUT, attende prossimo ciclo │ └── continue └── END ``` ### Politica di escalation I trigger di uscita non sono uguali. Alcuni hanno **urgenza alta** che giustifica l'esecuzione anche senza conferma utente entro la finestra: | Trigger | Urgenza | Comportamento su timeout | |---|---|---| | `CLOSE_PROFIT` | Bassa | Attendi prossimo ciclo (può migliorare) | | `CLOSE_STOP` | **Alta** | Esegui chiusura senza conferma esplicita | | `CLOSE_VOL` | **Alta** | Esegui chiusura senza conferma | | `CLOSE_DELTA` | **Alta** | Esegui chiusura senza conferma | | `CLOSE_TIME` | Media | Attendi 1 ciclo extra; al secondo timeout esegui | | `CLOSE_AVERSE` | Media | Attendi prossimo ciclo | Su escalation hard, Telegram riceve un messaggio **post-fact**: "ho chiuso la posizione X per stop loss perché non ho ricevuto risposta in 30 min e l'urgenza non consentiva attesa". ## Flusso 4 — Mensile (Kelly recalibration) Trigger: cron `0 12 1 * *` (primo di ogni mese alle 12:00 UTC). ``` 1. Carica trades chiusi negli ultimi 365 giorni 2. kelly_recalibration.recalibrate 3. Genera report mensile: - performance vs simulazione MC - win rate, avg win/loss, edge - kelly suggerito vs corrente - violazioni regole (deve essere 0) 4. telegram.send (informativo, no conferma) 5. brain_bridge.write_note( path="strategies/cerberus-bite/monthly-report-YYYY-MM.md", content=report ) 6. log KELLY_RECALIBRATED ``` L'aggiornamento di `strategy.yaml` resta **manuale**: Adriano legge il report, discute con Milito, e committa il nuovo file di config (con giustificazione nel commit message). Nessun auto-update. ## Flusso 5 — Health check periodico Trigger: ogni 5 minuti. ``` 1. ping ogni MCP usato → ms latency 2. SQLite read-write probe (transazione fittizia) 3. lock file ancora valido 4. ultima fill di Cerbero core ricevuta entro tempo atteso 5. state.system_state.kill_switch == 0 Se qualsiasi passo fallisce: - log HEALTH_DEGRADED - se 3 fallimenti consecutivi → kill_switch + alert - se 5 fallimenti consecutivi → restart automatico (1 sola volta al giorno) ``` ## Flusso 6 — Recovery dopo crash Quando l'engine riparte: 1. Riapre SQLite + log della giornata. 2. Per ogni posizione `awaiting_fill`: - Chiede a `cerbero-memory.get_status(instruction_id)` - Se filled → aggiorna a `open` - Se cancelled → aggiorna a `cancelled` - Se ancora pending → mantiene stato e riprende il monitoraggio 3. Per ogni posizione `closing`: - Stessa logica 4. Per ogni posizione `open`: - Verifica che esista realmente sul broker (Deribit `get_positions`) - Se non esiste → flag `state_inconsistent` + alert + kill switch 5. Riconciliazione completata → ENGINE_START. Il sistema **mai** prende decisioni di trading durante recovery: solo allinea lo stato. Il primo decision loop avviene al prossimo trigger naturale. ## Diagramma di stato di una posizione ``` ┌─ rejected_by_user ──→ cancelled │ (entry) ├─ timeout ──────────→ cancelled proposed ─────────────► │ ├─ confirmed ▼ awaiting_fill │ ├─ no_fill ──────────→ cancelled ├─ filled ───────────► open │ │ │ (exit_decision != HOLD) │ │ │ ▼ │ closing │ │ │ ├─ no_fill (escalated) → manual_intervention │ └─ filled ──────────→ closed ``` ## Cron summary | Cron | Trigger | Frequenza | |---|---|---| | `0 14 * * MON` | Entry evaluation | Settimanale | | `0 2,14 * * *` | Position monitoring | 2× giorno | | `0 12 1 * *` | Kelly recalibration | Mensile | | `*/5 * * * *` | Health check | 5 min | | `0 0 * * *` | Backup SQLite + rotation log | Giornaliero | | `0 8 * * *` | Daily digest Telegram | Giornaliero | Tutti gli orari in UTC.