- 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>
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.pye 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 su0.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
decisionsdi tipoexit_checkper 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.dataframesortable): 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
leveleevent. 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.gzper 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/.lockfileesclusivo. - La GUI tiene
data/.gui-lockfileesclusivo (impedisce due tab/Streamlit aperti). - Entrambi possono leggere SQLite (modalità WAL).
- Le
manual_actionssono il canale di scrittura condiviso, con primary key auto-increment e flagconsumed_atper consumo idempotente.
Sicurezza
- Bind solo
127.0.0.1. Mai0.0.0.0. - Streamlit avviato con
--server.headless trueper 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). Maist.buttonsenza 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 guilancia 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.AppTestper ogni pagina