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>
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user