63d1aa4262
* Localizzazione italiana di tutte le pagine (Stato, Audit, Equity,
Storico, Posizione) e della home; date relative ("5s fa", "12m fa").
* Logo Cerbero (cane a tre teste) in src/cerbero_bite/gui/assets/
cerbero_logo.png — sostituisce l'emoji 🐺 (lupo, semanticamente
errata) sia come favicon (`page_icon`) sia in sidebar e header.
* Caricamento automatico di `.env` dal CWD all'avvio della CLI (skip
sotto pytest tramite PYTEST_CURRENT_TEST), evitando di doversi
esportare manualmente le 4 URL MCP. Aggiunto python-dotenv come
dipendenza, `.env.example` committato come template, `.env` resta
ignorato da git.
* Pagina Stato: nuovo pannello "Saldi exchange" che fa fetch live
via gateway MCP (Deribit USDC + USDT, Hyperliquid USDC + opzionale
USDT spot) con cache TTL 60s e bottone refresh; tile riassuntivi
totale USD / EUR / cambio.
* Pagina Stato: nuovo pannello "Forza ciclo" con tre bottoni
(entry/monitor/health) che accodano azioni `run_cycle` nella tabella
manual_actions; il consumer dell'engine — quando in esecuzione —
dispatcha al `Orchestrator.run_*` corrispondente.
* manual_actions: nuovo `kind="run_cycle"` nello schema
ManualAction; consumer accetta dict di cycle_runners che
l'orchestrator popola in install_scheduler. 3 nuovi test (dispatch
entry, ciclo sconosciuto, fallback senza runner).
* gui/live_data.py — modulo dedicato al fetch MCP dalla GUI
(relax controllato della regola "no MCP from GUI" solo per i saldi,
non per i dati di trading).
363/363 tests pass; ruff clean; mypy strict src clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
149 lines
4.6 KiB
Python
149 lines
4.6 KiB
Python
"""Streamlit entry point for the Cerbero Bite dashboard.
|
|
|
|
Launch with::
|
|
|
|
cerbero-bite gui
|
|
|
|
or directly::
|
|
|
|
uv run streamlit run src/cerbero_bite/gui/main.py \
|
|
--server.address 127.0.0.1 \
|
|
--server.port 8765 \
|
|
--server.headless true
|
|
|
|
The dashboard is **read-mostly**: it reads SQLite + the audit log and
|
|
never imports ``runtime/`` modules. Each Streamlit page is in
|
|
``gui/pages/`` and Streamlit auto-discovers them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import streamlit as st
|
|
|
|
from cerbero_bite.gui.data_layer import (
|
|
DEFAULT_AUDIT_PATH,
|
|
DEFAULT_DB_PATH,
|
|
humanize_age,
|
|
humanize_dt,
|
|
load_engine_snapshot,
|
|
)
|
|
|
|
PAGE_TITLE = "Cerbero Bite — Cruscotto"
|
|
PAGE_ICON = str(Path(__file__).parent / "assets" / "cerbero_logo.png")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Path resolution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _resolve_paths() -> tuple[Path, Path]:
|
|
"""Read DB / audit paths from env (settable by ``cerbero-bite gui``)."""
|
|
db_path = Path(os.environ.get("CERBERO_BITE_GUI_DB", DEFAULT_DB_PATH))
|
|
audit_path = Path(os.environ.get("CERBERO_BITE_GUI_AUDIT", DEFAULT_AUDIT_PATH))
|
|
return db_path, audit_path
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sidebar
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
_HEALTH_BADGES: dict[str, tuple[str, str]] = {
|
|
"running": ("🟢", "ATTIVO"),
|
|
"degraded": ("🟡", "DEGRADATO"),
|
|
"killed": ("🔴", "KILL SWITCH"),
|
|
"stopped": ("⚫", "FERMO"),
|
|
"unknown": ("⚪", "SCONOSCIUTO"),
|
|
}
|
|
|
|
|
|
def _render_sidebar(db_path: Path, audit_path: Path) -> None:
|
|
snap = load_engine_snapshot(db_path=db_path)
|
|
icon, label = _HEALTH_BADGES.get(snap.health, ("⚪", "SCONOSCIUTO"))
|
|
|
|
logo_path = Path(__file__).parent / "assets" / "cerbero_logo.png"
|
|
if logo_path.is_file():
|
|
st.sidebar.image(str(logo_path), use_container_width=True)
|
|
st.sidebar.markdown(f"### {icon} {label}")
|
|
if snap.kill_switch_armed:
|
|
st.sidebar.error(
|
|
f"**Kill switch armato**\n\n"
|
|
f"motivo: {snap.kill_reason or '—'}\n\n"
|
|
f"da: {humanize_dt(snap.kill_at)}"
|
|
)
|
|
|
|
st.sidebar.metric(
|
|
"Ultimo health check",
|
|
humanize_age(snap.last_health_check_age_s),
|
|
)
|
|
st.sidebar.metric("Posizioni aperte", snap.open_positions)
|
|
st.sidebar.caption(f"config: `{snap.config_version or '—'}`")
|
|
|
|
st.sidebar.divider()
|
|
st.sidebar.caption("Sola lettura • solo localhost")
|
|
st.sidebar.caption(f"db: `{db_path}`")
|
|
st.sidebar.caption(f"audit: `{audit_path}`")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Home page
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def main() -> None:
|
|
st.set_page_config(
|
|
page_title=PAGE_TITLE,
|
|
page_icon=PAGE_ICON,
|
|
layout="wide",
|
|
initial_sidebar_state="expanded",
|
|
)
|
|
|
|
db_path, audit_path = _resolve_paths()
|
|
_render_sidebar(db_path, audit_path)
|
|
|
|
logo_path = Path(__file__).parent / "assets" / "cerbero_logo.png"
|
|
header_cols = st.columns([1, 6])
|
|
if logo_path.is_file():
|
|
header_cols[0].image(str(logo_path), use_container_width=True)
|
|
header_cols[1].title("Cerbero Bite")
|
|
st.caption(
|
|
"Motore rule-based per credit spread su ETH — cruscotto in sola lettura"
|
|
)
|
|
|
|
st.markdown(
|
|
"""
|
|
Usa la barra laterale per navigare:
|
|
|
|
- **Stato** — salute del motore, kill switch, posizioni aperte, ancora audit
|
|
- **Audit** — streaming del registro audit + verifica integrità della catena
|
|
- **Equity** — P&L cumulato, drawdown, distribuzione per chiusura, statistiche mensili
|
|
- **Storico** — trade chiusi con filtri, KPI, esportazione CSV
|
|
- **Posizione** — drilldown sulla singola posizione con grafico payoff
|
|
|
|
Il cruscotto legge `data/state.sqlite` e `data/audit.log` direttamente;
|
|
non interroga mai i servizi MCP né il broker. L'unico canale di
|
|
scrittura è la coda `manual_actions` per arm/disarm del kill switch.
|
|
"""
|
|
)
|
|
|
|
snap = load_engine_snapshot(db_path=db_path)
|
|
cols = st.columns(4)
|
|
cols[0].metric("Salute motore", _HEALTH_BADGES[snap.health][1])
|
|
cols[1].metric(
|
|
"Kill switch",
|
|
"ARMATO" if snap.kill_switch_armed else "DISARMATO",
|
|
)
|
|
cols[2].metric("Posizioni aperte", snap.open_positions)
|
|
cols[3].metric(
|
|
"Ultimo health check",
|
|
humanize_age(snap.last_health_check_age_s),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|