# 11 — GUI Streamlit GUI desktop locale per osservazione e azioni manuali. Implementazione con **Streamlit**: un singolo processo Python, niente HTML/JS custom, niente bundler, niente porte esposte all'esterno. ## Filosofia - **Read-mostly**: la dashboard è soprattutto per *guardare* cosa fa il sistema. Le azioni interattive sono poche e ben delimitate (arm/disarm, force-close di emergenza, conferma manuale di una proposta in coda). - **Stesso state, due frontend**: Streamlit non duplica logica. Legge da `state/repository.py` e dai log, esattamente come fa il decision loop. Non parla mai direttamente al broker. - **Localhost only**: `streamlit run --server.address 127.0.0.1`. Mai bind su `0.0.0.0`. Niente autenticazione, niente HTTPS: la protezione è "macchina di Adriano, browser locale". - **Single-user, single-tab**: Streamlit non è progettato per concorrenza. Una sola sessione attiva alla volta. ## Stack tecnico | Componente | Scelta | |---|---| | Framework | `streamlit` ≥ 1.40 | | Charts | `streamlit` builtin (`st.line_chart`, `st.bar_chart`) + `plotly` per payoff diagram | | Tabelle | `streamlit.dataframe` con filtri | | Auto-refresh | `st_autorefresh` (component) ogni 5-10 sec sulle pagine live | | Config interna | letta da `strategy.yaml` via `config.loader` (sola lettura) | | Sicurezza | localhost-only, lock file condiviso con engine | | Process model | Processo separato da quello dell'engine, comunicazione **solo via SQLite e log** | ## Avvio ```bash cerbero-bite gui # alias per `streamlit run src/cerbero_bite/gui/main.py` ``` oppure manualmente: ```bash uv run streamlit run src/cerbero_bite/gui/main.py \ --server.address 127.0.0.1 \ --server.port 8765 \ --server.headless true \ --browser.gatherUsageStats false ``` ## Layout cartelle ``` src/cerbero_bite/gui/ ├── __init__.py ├── main.py # entry point streamlit, sidebar nav ├── pages/ │ ├── 1_📊_status.py │ ├── 2_📈_equity.py │ ├── 3_💼_position.py │ ├── 4_📜_history.py │ └── 5_🔍_audit.py ├── components/ │ ├── kill_switch_panel.py │ ├── mcp_health_grid.py │ ├── pending_proposal_card.py │ ├── payoff_chart.py │ └── greeks_panel.py └── data_layer.py # wrapper read-only verso state.repository ``` ## Pagine ### 1. 📊 Status (home) Vista a colpo d'occhio dello stato corrente. Sezioni: - **Engine status**: badge verde/giallo/rosso (running/degraded/killed), uptime, ultimo health check, kill_switch state, kill_reason se armato. - **Capitale**: equity corrente da `cerbero-portfolio` (cache ultimo valore noto + timestamp), variazione % vs giorno prima, vs settimana, vs mese. - **Posizione attiva**: card con riepilogo (proposal_id, expiry, credit, P&L unrealized stimato, days_to_expiry) o "nessuna posizione aperta". - **MCP health grid**: 8 box, uno per server, con latenza ms e semaforo. - **Pending action**: se l'engine ha una proposta in attesa di conferma e il timeout Telegram è scaduto, qui appare una card con `Approve`/`Reject`. Effetto: la decisione viene scritta in coda e il decision orchestrator la legge al prossimo health-check. - **Big buttons**: `🟢 Disarm` / `🔴 Arm Kill Switch` (con conferma typed `"yes I am sure"`). Auto-refresh: 5 secondi. ### 2. 📈 Equity Grafico storia capitale e analitica. Sezioni: - **Equity curve** (line chart): capitale nel tempo dall'inizio del tracking. Risoluzione giornaliera. Sovrapposizione opzionale: - banda Monte Carlo P5/P50/P95 (statica, dal documento) - DVOL nel tempo (asse Y secondario) - eventi macro (vertical lines sui giorni FOMC/CPI) - **Drawdown rolling** (sotto curve): area chart del DD% corrente. - **P&L distribution** (histogram): trade chiusi raggruppati per outcome (profit_take, stop_loss, vol_stop, time_stop, ecc.). - **Tabella mensile**: per ogni mese — n trade, win rate, P&L, max DD. Filtri: range temporale, asset (solo ETH per ora). Auto-refresh: 30 secondi (cambia raramente). ### 3. 💼 Position Drill-down sulla posizione attualmente aperta (se esiste). Sezioni: - **Header**: proposal_id, opened_at, expiry, days_left, status. - **Legs table**: instrument, side, size, mid corrente, delta, theta, vega — refresh periodico via `clients.deribit`. - **Greche aggregate**: delta/theta/vega netti. - **Payoff diagram** (plotly): P&L vs spot ETH a scadenza, con breakeven, max profit, max loss, spot corrente come marker. - **Decision history**: tabella con tutte le `decisions` di tipo `exit_check` per questa posizione, in ordine cronologico, con outcome HOLD / CLOSE_*. - **Distance metrics**: short strike a `X% OTM`, delta corrente, distanza in sigma. - **Force close** (collapsibile): typed confirmation + reason field. Su submit: scrive in coda azione `manual_close`, l'engine la consuma al prossimo monitor cycle. Auto-refresh: 10 secondi. ### 4. 📜 History Storico trade chiusi. Sezioni: - **Filtri**: range temporale, outcome (multiselect), P&L > 0 / < 0 / tutti. - **Tabella trade chiusi** (`st.dataframe` sortable): proposal_id, opened_at, closed_at, expiry, n_contracts, credit_usd, debit_paid_usd, pnl_usd, outcome, days_held. - **KPI strip**: n trade, win rate, avg win, avg loss, edge per trade, edge cumulato. - **Confronto Monte Carlo**: side-by-side delle metriche reali vs attese da simulazione, con delta in %. - **Export CSV**: bottone download per uso fiscale. Auto-refresh: manuale (button). ### 5. 🔍 Audit Log e audit chain. Sezioni: - **Live log stream**: ultimi 100 eventi, filtro per `level` e `event`. Auto-refresh 5 sec. - **Audit chain status**: bottone `Verify`. Mostra "✅ chain integra fino a 14.382 eventi" o "❌ tampering rilevato a evento N". - **Search**: ricerca testuale negli ultimi 30 giorni di log. - **Stats engine**: numero kill switch armati nell'ultimo mese, MCP failure count per server, average decision loop latency. - **Export log**: download `.jsonl.gz` per analisi forensica. Auto-refresh: manuale. ## Comunicazione GUI ↔ Engine La GUI **non importa** moduli `runtime/` né chiama direttamente i client MCP. Tutto passa via: | Azione GUI | Effetto | |---|---| | Visualizzazione stato | Read da `state/repository.py` (SQLite) | | Equity / storico | Read da SQLite + `data/log/*.jsonl` | | MCP health | Read da `state.system_state.last_health_check` (l'engine fa il check) | | **Disarm kill switch** | Write su `system_state` con `kill_switch=0`; l'engine al prossimo health check rileva e log `KILL_SWITCH_DISARMED` | | **Arm kill switch** | Write su `system_state` con `kill_switch=1, kill_reason="manual via GUI"` | | **Force close** | Insert riga in tabella `manual_actions` (nuova) con `kind="force_close", proposal_id=...`; l'engine al prossimo monitor cycle la consuma | | **Approve pending proposal** | Insert riga in `manual_actions` con `kind="approve_proposal", proposal_id=...` | **Nuova tabella SQLite** (`05-data-model.md` da estendere): ```sql CREATE TABLE manual_actions ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, -- approve_proposal, reject_proposal, force_close, etc. proposal_id TEXT, payload_json TEXT, 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); ``` L'engine include un nuovo job APScheduler `every 30s`: ```python async def consume_manual_actions(): actions = state.fetch_unconsumed_manual_actions() for a in actions: if a.kind == "force_close": await orchestrator.handle_force_close(a.proposal_id, a.payload) elif a.kind == "approve_proposal": await orchestrator.handle_proposal_approved(a.proposal_id) # etc. state.mark_action_consumed(a.id, result="ok") ``` Le azioni write **non bypassano** i risk control: una `force_close` deve comunque passare dal `safety.system_healthy()` e da una conferma typed nella GUI prima di essere scritta in coda. ## Lock e concorrenza - L'engine tiene `data/.lockfile` esclusivo. - La GUI tiene `data/.gui-lockfile` esclusivo (impedisce due tab/Streamlit aperti). - Entrambi possono leggere SQLite (modalità WAL). - Le `manual_actions` sono il **canale di scrittura** condiviso, con primary key auto-increment e flag `consumed_at` per consumo idempotente. ## Sicurezza - Bind solo `127.0.0.1`. Mai `0.0.0.0`. - Streamlit avviato con `--server.headless true` per evitare apertura automatica del browser via tunnel. - Nessuna autenticazione HTTP: la barriera è il fatto che la macchina è personale di Adriano. Se il sistema fosse mai esposto, va aggiunto reverse proxy con basic auth — ma non è il caso. - Le azioni write richiedono **typed confirmation** (`"yes I am sure"`, identico al flusso CLI). Mai `st.button` senza challenge. - CSRF: non rilevante in Streamlit (no form HTML, tutto via session state). ## Cosa la GUI **non** fa Per chiarezza: - Non interroga direttamente Deribit o altri exchange. - Non chiama mai `cerbero-memory.push_user_instruction`. - Non muta `strategy.yaml` (modifiche restano da CLI con audit chain). - Non riavvia l'engine (start/stop sono CLI). - Non sostituisce Telegram come canale di conferma di apertura. Telegram resta il canale primario; la GUI è canale di **fallback** per quando Adriano è davanti al laptop e non al telefono. ## Stima di sforzo Inserita come **Fase 4.5** nella roadmap, tra Orchestrator e Reporting: | Task | Giorni | |---|---| | Setup `gui/main.py` + sidebar nav + autorefresh | 0.5 | | Pagina Status + MCP health grid + kill_switch panel | 0.5 | | Pagina Equity + drawdown + plot mensili | 0.5 | | Pagina Position + payoff plotly + decision history | 1.0 | | Pagina History + filtri + export CSV | 0.5 | | Pagina Audit + search log + verify chain | 0.5 | | `manual_actions` table + consumer job APScheduler | 0.5 | | Test integration (Streamlit AppTest framework) | 0.5 | | **Totale** | **~4 giorni** | Definition of Done: - `cerbero-bite gui` lancia la dashboard - Tutte le 5 pagine raggiungibili e popolate (anche con dati fake) - Disarm da GUI loggato in audit chain ed effettivo entro 30 sec - Force-close da GUI consumato dall'engine entro 30 sec - Test integration con `streamlit.testing.v1.AppTest` per ogni pagina