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

277 lines
10 KiB
Markdown

# 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