# 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.

> **Nota — la tolleranza "3 fallimenti" vale solo per i probe del
> health check.** Tutti i tick schedulati (entry, monitor, snapshot,
> …) girano dentro `orchestrator._safe`, che escala **qualsiasi**
> eccezione non gestita a `alert_manager.critical` → kill switch
> armato al **primo** errore, anche transitorio. È il percorso che ha
> fermato il bot il 31/05/2026 (singolo `McpTimeoutError` nel tick
> entry). Vedi la tabella trigger in `07-risk-controls.md`.

## 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.
