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

326 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```
## 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`):
```sql
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: …"`).
```python
# 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.