Files
Cerbero-Bite/docs/11-gui-streamlit.md
T
Adriano 881bc8a1bf Phase 0: project skeleton
- pyproject.toml with uv, deps for runtime + gui + backtest + dev
- ruff/mypy strict config, pre-commit hooks for ruff/mypy/pytest
- src/cerbero_bite/ layout with empty modules ready for Phase 1+
- structlog JSONL logger with daily rotation
- click CLI with placeholder subcommands (status, start, kill-switch,
  gui, replay, config hash, audit verify)
- 6 smoke tests passing, mypy --strict clean, ruff clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:10:30 +02:00

10 KiB

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

cerbero-bite gui              # alias per `streamlit run src/cerbero_bite/gui/main.py`

oppure manualmente:

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):

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:

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