Files
Cerbero-Bite/docs/11-gui-streamlit.md
Adriano da88e7f746 docs: align 05/06/09/11 with implemented GUI Phases A–D
* docs/11-gui-streamlit.md — replaces the original spec with what was
  actually built: implementation status table, real page filenames
  (1_Status, 2_Audit, 3_Equity, 4_History, 5_Position), per-page
  inventory of implemented vs deferred sections, GUI ↔ engine table
  showing arm_kill/disarm_kill via manual_actions and the
  not_supported markers for force_close + approve/reject_proposal,
  consumer signature with cron */1, lock model clarified (no GUI
  lockfile), DoD updated with current state.
* docs/05-data-model.md — manual_actions is no longer "pianificata":
  populated by gui/data_layer.py, drained by the manual_actions job;
  per-kind status table (arm/disarm OK, others not_supported).
* docs/09-development-roadmap.md — Phase 4.5 marked implemented with
  per-task / markers for the deferred items (auto-refresh,
  AppTest, force-close hook).
* docs/06-operational-flow.md — adds Flusso 5b describing the
  manual_actions consumer pattern (enqueue → KillSwitch transition →
  audit log linkage).

360/360 tests still pass.

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

14 KiB
Raw Permalink Blame History

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

Stato implementativo

La dashboard è stata costruita in quattro fasi incrementali:

Fase Contenuto Stato
A Status + Audit (osservazione di base)
B Equity + History (analitica + export CSV)
C Position drilldown con payoff plotly + decision history
D Kill-switch arm/disarm dalla dashboard via coda manual_actions

Per scelta di scope, restano fuori dalla prima iterazione: force-close dalla GUI (richiede un hook handle_force_close nell'orchestrator), approve/reject di una proposta (il bot decide autonomamente, non c'è un flusso di proposta in attesa) e auto-refresh attivo via st_autorefresh. Il consumer di manual_actions riconosce già i kind corrispondenti e li archivia con result="not_supported" finché i flussi non saranno cablati.

Layout cartelle

src/cerbero_bite/gui/
├── __init__.py
├── main.py                              # entry Streamlit, sidebar, home
├── data_layer.py                        # wrapper read-only + write helpers
└── pages/
    ├── 1_📊_Status.py                   # health, kill switch, audit anchor
    ├── 2_🔍_Audit.py                    # log stream + chain integrity
    ├── 3_📈_Equity.py                   # cumulative P&L + drawdown
    ├── 4_📜_History.py                  # closed trades + KPI + CSV
    └── 5_💼_Position.py                 # drilldown + payoff plotly

I componenti riutilizzabili descritti nello spec originale (kill_switch_panel, payoff_chart, ecc.) non sono stati estratti in file separati: ogni pagina è autonoma e tiene la propria UI inline, così l'evoluzione resta locale al singolo file. La promozione a componenti separati è giustificata solo se più pagine condividono lo stesso widget — al momento non è il caso.

Pagine

1. 📊 Status (home)

Stato corrente e controlli sul kill switch.

Sezioni implementate:

  • Engine status banner colorato in base alla health derivata dalla combinazione system_state.kill_switch + età di last_health_check (running/degraded/stopped/killed/unknown).
  • Top metric tiles: posizioni aperte, età ultimo health check, started_at, config_version.
  • Kill switch controls: form arm/disarm con typed confirmation ("yes I am sure") + reason obbligatoria. La submission scrive un'azione in manual_actions; il consumer la applica entro un minuto.
  • Pending manual actions: tabella delle azioni in coda non ancora consumate (visibile solo se la coda è non vuota).
  • Audit anchor: hash chain head persistito in system_state.
  • Open positions table: spread type, contracts, credit, max loss, strikes, status, opened/expiry.

Sezioni non ancora implementate rispetto allo spec originale: capitale con variazioni %, MCP health grid (i probe sono fatti dall'engine e visibili in audit), pending-proposal card. Il refresh automatico è manuale (la pagina si aggiorna alla navigazione o al re-render spontaneo di Streamlit).

2. 🔍 Audit

Live log stream + verifica integrità della hash chain.

Sezioni implementate:

  • Chain integrity verify: bottone che richiama verify_chain e riporta numero di entries verificate o l'errore di mismatch.
  • Filtri: limit (10500) + event filter (auto-popolato dagli event effettivamente presenti nella tail).
  • Event-count strip: Counter dei tipi di evento nella finestra.
  • Tail table: timestamp, event, payload JSON canonico, hash abbreviato — newest-first.

3. 📈 Equity

Curva P&L cumulato e analitica trade chiusi.

Sezioni implementate:

  • KPI strip: closed trades, win rate, total P&L, edge per trade, max drawdown (USD + %).
  • Cumulative P&L (Plotly): riempito a zero, con linea zero di riferimento.
  • Drawdown (Plotly area chart, asse invertito).
  • P&L distribution by close reason: istogrammi Plotly sovrapposti con conteggio trades per reason in metric tiles.
  • Per-month stats: tabella aggregata UTC (mese, n trade, vincitori, win rate, P&L totale, P&L medio).

Window picker: All time, last 30/90 giorni, year-to-date. Banda Monte Carlo, overlay DVOL e linee eventi macro non sono ancora implementati.

4. 📜 History

Storico trade chiusi con filtri ed esportazione.

Sezioni implementate:

  • Window picker: All time, last 7/30/90 giorni, year-to-date.
  • Filtri di dettaglio: multiselect su close_reason, radio vincitori/perdenti/tutti.
  • KPI strip a sei tile: trades, win rate, total P&L, avg win, avg loss, edge per trade.
  • Tabella trade chiusi: proposal_id (short), spread type, asset, contracts, strikes, credit/max_loss, P&L, close_reason, days_held, opened/closed/expiry.
  • CSV export: download diretto via st.download_button.

Confronto Monte Carlo side-by-side non ancora implementato.

5. 💼 Position

Drilldown su una posizione specifica (open o ultime 10 chiuse).

Sezioni implementate:

  • Position selector con label proposal_id · spread_type · short/long · status. Supporta deep-link via query string ?proposal_id=….
  • Header tiles: status, spread, contracts, credit USD; caption con proposal_id pieno + opened/expiry.
  • Distance metrics: short strike OTM%, days-to-expiry, days-held, delta at entry, width % of spot.
  • Legs table (snapshot al momento dell'entry, non live): leg, instrument, strike, side, size, delta. Una caption ricorda che mid e greche live non sono fetchate dalla GUI.
  • Payoff at expiry (Plotly): curva P&L con annotazioni per short strike, long strike, breakeven, entry spot. Tile riassuntivi per max profit, max loss, breakeven. Implementato per bull_put e bear_call; gli iron condor cadono su una curva piatta (placeholder).
  • Decision history: tabella delle righe decisions legate al proposal_id, newest-first, con outputs JSON canonici.

Le greche/mid live e il force-close manuale richiedono che l'engine esponga rispettivamente uno snapshot persistito e l'hook handle_force_close — fuori scope della prima iterazione.

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) tramite gui/data_layer.py
Equity / storico Read da SQLite (positions con status='closed') + audit log
MCP health Read indiretto da system_state.last_health_check (l'engine fa il probe)
Disarm kill switch enqueue_disarm_kill(reason) → riga in manual_actions con kind="disarm_kill"; consumer chiama KillSwitch.disarm (audit KILL_SWITCH_DISARMED, source="manual_gui")
Arm kill switch enqueue_arm_kill(reason) → riga kind="arm_kill"; consumer chiama KillSwitch.arm
Force close Pianificato: kind="force_close". Oggi il consumer marca result="not_supported"; richiede l'hook Orchestrator.handle_force_close
Approve / reject pending proposal Pianificato: kind="approve_proposal" / "reject_proposal". Stesso stato (non implementato lato orchestrator)

La GUI non scrive direttamente su system_state: ogni transizione del kill switch passa dal consumer e dalla classe KillSwitch, così SQLite e audit chain restano sincronizzati come per le transizioni automatiche.

Schema SQLite (vedi 05-data-model.md):

CREATE TABLE manual_actions (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    kind         TEXT NOT NULL,
    proposal_id  TEXT,
    payload_json TEXT,
    created_at   TEXT NOT NULL,
    consumed_at  TEXT,
    consumed_by  TEXT,
    result       TEXT
);
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);

Consumer: runtime/manual_actions_consumer.consume_manual_actions. Registrato come job APScheduler manual_actions con cron */1 * * * * (latenza ≤ 1 minuto, sufficiente per kill-switch). Il consumer drena tutta la coda a ogni tick e per ogni azione setta consumed_at, consumed_by="engine" e result ("ok", "not_supported" o "error: …").

# src/cerbero_bite/runtime/manual_actions_consumer.py — sintesi
async def consume_manual_actions(ctx, *, now=None):
    while (action := ctx.repository.next_unconsumed_action(...)) is not None:
        if action.kind == "arm_kill":
            ctx.kill_switch.arm(reason=payload.get("reason"), source="manual_gui")
        elif action.kind == "disarm_kill":
            ctx.kill_switch.disarm(reason=payload.get("reason"), source="manual_gui")
        else:
            result = "not_supported"
        ctx.repository.mark_action_consumed(...)

Le azioni write non bypassano i risk control: la transizione passa sempre per KillSwitch.arm/disarm, che valida lo stato e logga in audit. La typed confirmation ("yes I am sure") è gating lato GUI prima dell'enqueue.

Lock e concorrenza

  • L'engine tiene data/.lockfile esclusivo via runtime/lockfile.py.
  • La GUI non acquisisce un lock dedicato; più tab Streamlit contemporanee sono possibili (sconsigliate ma non impedite). Il vincolo single-writer su SQLite è preservato perché ogni write passa dalla riga manual_actions (auto-increment) e dal consumer dell'engine.
  • Entrambi possono leggere SQLite (le connessioni sono in modalità short-lived: aperte per chiamata e chiuse subito).
  • 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 (storica)

La Fase 4.5 è stata implementata in quattro round (AD). Lo spec originale stimava ~4 giorni ed è stato consegnato in linea con la stima, con il caveat che streamlit.testing.v1.AppTest non è ancora cablato (le pagine sono validate manualmente via smoke test HTTP) e che force-close + approve/reject restano fuori scope.

Task Giorni stimati Stato
Setup gui/main.py + sidebar nav + autorefresh 0.5 (autorefresh non attivo)
Pagina Status + kill_switch panel 0.5 (MCP health grid non implementata)
Pagina Equity + drawdown + plot mensili 0.5
Pagina Position + payoff plotly + decision history 1.0 (greche live e force-close differiti)
Pagina History + filtri + export CSV 0.5
Pagina Audit + verify chain 0.5 (search e export gz differiti)
manual_actions consumer + APScheduler 0.5 (arm/disarm; force_close = not_supported)
Test integration (Streamlit AppTest) 0.5
Totale stimato ~4 giorni

Definition of Done — stato attuale:

  • cerbero-bite gui lancia la dashboard su 127.0.0.1:8765
  • Tutte le 5 pagine raggiungibili e popolate dai dati di runtime
  • Disarm da GUI loggato in audit chain (source="manual_gui") ed effettivo entro ~1 minuto (cron */1)
  • Force-close da GUI: l'enqueue è possibile, ma l'orchestrator non ha ancora handle_force_close; il consumer marca result="not_supported"
  • Test integration con streamlit.testing.v1.AppTest: non scritti

Le voci aperte sono follow-up isolati e non bloccano l'uso quotidiano della dashboard come tableau d'observation.