# 06 — Flussi operativi I quattro flussi principali del rule engine, tutti orchestrati dallo scheduler interno APScheduler. Aggiornati al modello di esercizio **autonomo notify-only** introdotto in Fase 3: Cerbero Bite consulta i server MCP della suite per leggere il mercato e per inviare l'ordine, non chiede mai conferma a Adriano e usa Telegram esclusivamente per notificare quanto fatto. ## Flusso 1 — Avvio engine ``` 1. Carica strategy.yaml + strategy.local.yaml (override) 2. Validazione schema + verifica config_hash 3. Acquisisci lock file (data/.lockfile) 4. Apri SQLite, esegui migrations, init_system_state 5. Health check MCP (environment_info, ping read tools) - cerbero-deribit.environment_info → confronta con strategy.execution.environment; mismatch → kill switch 6. Riconciliatore stato (vedi flusso 6) 7. Health check sistema OK? → arma scheduler KO? → kill switch + alert + idle 8. Audit 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 o un ambiente diverso da quello atteso. ## Flusso 2 — Daily (entry) Trigger: cron `0 14 * * *` (ogni giorno 14:00 UTC). Crypto è 24/7: la cadenza di candidatura non è gateata sulla settimana — sono i gate quantitativi a decidere se entrare o saltare il giorno. ``` START ├── safety.system_healthy()? → no → log + skip ├── repository.has_open_position()? → yes → log + skip ├── snapshot dati di mercato (parallel): │ spot_eth, dvol, funding_perp, │ funding_cross, macro_calendar, │ eth_holdings_pct, capital_eur ├── entry_validator.validate_entry → fail → log ENTRY_REJECTED + reasons ├── entry_validator.compute_bias → None → log ENTRY_REJECTED ("no_bias") ├── deribit.options_chain (DTE 14-21) ├── combo_builder.select_strikes → None → log ENTRY_REJECTED ("no_strike") ├── deribit.orderbook_depth_top3 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 ├── repository.create_position(status="proposed") ├── deribit.place_combo_order │ ├── ordine accettato (state=open|filled): │ │ ├── repository.update_position_status(awaiting_fill|open) │ │ ├── repository.create_instruction │ │ ├── audit ENTRY_PLACED │ │ └── telegram.notify_position_opened (post-fact) │ └── ordine rigettato: │ ├── repository.update_position_status(cancelled) │ ├── audit ENTRY_REJECTED_BY_BROKER │ └── telegram.notify_alert (priority=high) └── END ``` Nessuna conferma manuale: la decisione di apertura è il risultato deterministico delle regole, non una proposta soggetta ad approvazione. Il messaggio Telegram è informativo. ### Tempo previsto end-to-end - Snapshot dati: 3-5 secondi. - Algoritmi puri: < 0.5 secondi. - `place_combo_order` su testnet: 1-3 secondi. Critical path completo sotto 10 secondi nominali. ## 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) ├── salva snapshot in dvol_history (per il calcolo di return_4h) ├── per ogni position con status="open": │ ├── snapshot: │ │ spot, dvol, mark_combo (= mid_short - mid_long), │ │ delta_short, return_4h (vs dvol_history 4h fa) │ ├── exit_decision.evaluate │ ├── action == HOLD: │ │ └── log EXIT_EVALUATED("hold") │ ├── action == CLOSE_*: │ │ ├── repository.update_position_status("closing") │ │ ├── deribit.place_combo_order (direzione inversa) │ │ │ ├── filled: │ │ │ │ ├── repository.update_position_status("closed") │ │ │ │ ├── audit EXIT_FILLED │ │ │ │ └── telegram.notify_position_closed │ │ │ └── rejected: │ │ │ ├── repository.update_position_status("open") │ │ │ └── telegram.notify_alert (priority=critical, │ │ │ source="exit_failed") │ └── continue └── END ``` Non c'è un ramo "richiesta conferma utente". Ogni `CLOSE_*` viene eseguito immediatamente; il messaggio Telegram è post-fact e descrive azione + motivo (formato `notify_position_closed`). In caso di fallimento del `place_combo_order` di chiusura (broker respinge, latenza > soglia, ecc.) la posizione viene rimessa in `open` e generato un alert `critical`: sarà il prossimo ciclo a ritentare. ## 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 dal repository 2. kelly_recalibration.recalibrate 3. Genera report mensile testuale (markdown) 4. telegram.notify (priority=normal, tag="kelly") 5. salva report come allegato al log JSONL del giorno 6. audit KELLY_RECALIBRATED ``` L'aggiornamento di `strategy.yaml` resta **manuale**: Adriano legge il report e committa il nuovo file (con giustificazione nel commit message). Nessun auto-update del kelly_fraction. ## Flusso 5 — Health check periodico Trigger: ogni 5 minuti. ``` 1. Per ogni MCP utilizzato: probe lightweight - deribit.environment_info - macro.get_macro_calendar(days=1) - sentiment.get_cross_exchange_funding (no asset filter) - hyperliquid.get_funding_rate("ETH") - portfolio: skip (componente in-process, copertura indiretta dai probe deribit/hyperliquid/macro) - telegram: skip (notify-only, no probe non invasivo) 2. SQLite read-write probe (transazione fittizia) 3. Lock file ancora valido 4. environment_info.environment == strategy.execution.environment 5. Audit HEALTH_OK / HEALTH_DEGRADED 6. Conta fallimenti consecutivi: - 3 fallimenti → kill_switch + alert HIGH - 5 fallimenti → audit + alert CRITICAL (riavvio è demandato a Docker) ``` Il dead-man (`scripts/dead_man.sh`) sorveglia che `HEALTH_OK` venga scritto: silenzio > 15 min → kill switch via SQLite e alert. ## Flusso 5b — Manual actions consumer Trigger: cron `*/1 * * * *` (job APScheduler `manual_actions`). ``` 1. Mentre la coda ha righe non consumate: - leggi `next_unconsumed_action` (oldest-first) - dispatch per kind: arm_kill → KillSwitch.arm(reason, source="manual_gui") disarm_kill → KillSwitch.disarm(reason, source="manual_gui") force_close / approve_proposal / reject_proposal → result="not_supported" - mark_action_consumed con consumed_by="engine" e result 2. Latenza tipica end-to-end (enqueue da GUI → effetto): ≤ 60 sec. ``` Il consumer è il **canale unico** di scrittura dalla GUI verso il runtime: ogni transizione del kill switch passa dalla classe `KillSwitch` per mantenere SQLite e audit chain in lock-step. Vedi `runtime/manual_actions_consumer.py` e `docs/11-gui-streamlit.md`. ## Flusso 6 — Recovery dopo crash All'avvio o dopo un riavvio del container: 1. Apre SQLite + log JSONL della giornata. 2. Per ogni `awaiting_fill`/`closing`: - chiama `deribit.get_positions` per verificare l'esistenza sul broker - se trovata → aggiorna a `open` (fill confermato) - se non trovata e l'ordine risulta cancellato → aggiorna a `cancelled` - se nessuna delle due → flag `state_inconsistent`, kill switch, alert CRITICAL 3. Per ogni `open`: - verifica corrispondenza posizione broker vs DB (size, strike, expiry); discrepanze → kill switch 4. Riconciliazione completata → `audit ENGINE_START`. Il sistema **mai** prende decisioni di trading durante il recovery: solo allinea lo stato. Il primo decision loop avviene al prossimo trigger naturale. ## Diagramma di stato di una posizione ``` proposed │ ├─ broker_reject ────→ cancelled ├─ submitted ────────► awaiting_fill │ ├─ no_fill ───────────→ cancelled ├─ filled ────────────► open │ │ │ (exit_decision != HOLD) │ │ │ ▼ │ closing │ │ │ ├─ no_fill → open │ │ (riprovato al prossimo ciclo) │ └─ filled → closed ``` ## Cron summary | Cron | Trigger | Frequenza | |---|---|---| | `0 14 * * *` | Entry evaluation | Giornaliera | | `0 2,14 * * *` | Position monitoring | 2× giorno | | `0 12 1 * *` | Kelly recalibration | Mensile | | `*/5 * * * *` | Health check | 5 min | | `*/15 * * * *` | Market snapshot (calibrazione soglie) | 15 min | | `0 0 * * *` | Backup SQLite + rotation log | Giornaliero | | `0 8 * * *` | Daily digest Telegram | Giornaliero | Tutti gli orari in UTC. ## Modalità operativa (interruttori `RuntimeFlags`) Il bot riconosce due interruttori indipendenti, letti da `.env` al boot tramite `cerbero_bite.config.runtime_flags.load_runtime_flags()`: | Variabile d'ambiente | Default | Cosa abilita | |---|---|---| | `CERBERO_BITE_ENABLE_DATA_ANALYSIS` | `true` | Job `market_snapshot` ogni 15 min: raccolta dati MCP, scrittura tabella `market_snapshots`, calibrazione soglie. | | `CERBERO_BITE_ENABLE_STRATEGY` | `false` | Job `entry` (daily 14:00 UTC) e `monitor` (2× giorno): valutazione regole §2-§9 di `01-strategy-rules.md` e proposta/esecuzione ordini. | I job di infrastruttura (`health`, `backup`, `manual_actions`) sono **sempre attivi**, indipendentemente dai flag, perché tengono in vita il kill switch e la persistenza. ### Profilo "solo analisi dati" (default) Configurazione standard del periodo di soak post-deploy: ```env CERBERO_BITE_ENABLE_DATA_ANALYSIS=true CERBERO_BITE_ENABLE_STRATEGY=false ``` Effetto: il bot raccoglie snapshot di mercato, alimenta `market_snapshots`, ma **non** invia entry né chiude posizioni autonomamente. I metodi `run_entry`/`run_monitor` restano richiamabili manualmente da CLI (`cerbero-bite dry-run --cycle entry|monitor`) e tramite `manual_actions` per testing e validazione. ### Profilo "trading attivo" ```env CERBERO_BITE_ENABLE_DATA_ANALYSIS=true CERBERO_BITE_ENABLE_STRATEGY=true ``` Effetto: tutti i job canonici vengono installati nello scheduler. Lo switch va fatto solo dopo che la qualità dei dati raccolti è stata validata e Adriano dà esplicito consenso al passaggio. ### Disattivazione completa dell'analisi dati Caso eccezionale (manutenzione, problema MCP): ```env CERBERO_BITE_ENABLE_DATA_ANALYSIS=false CERBERO_BITE_ENABLE_STRATEGY=false ``` Il bot resta vivo per health check e ricezione di manual actions, ma non interroga MCP per dati di mercato e non opera. Il kill switch resta operativo.