feat(gui): traduzione italiana, logo Cerbero, saldi live e Forza ciclo
* 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>
This commit is contained in:
@@ -25,12 +25,15 @@ def _resolve_db() -> Path:
|
||||
|
||||
|
||||
def _date_window(label: str) -> tuple[datetime | None, datetime | None]:
|
||||
"""UI control for picking the analytics window."""
|
||||
"""Selettore della finestra temporale per l'analitica."""
|
||||
options = {
|
||||
"All time": (None, None),
|
||||
"Last 30 days": (datetime.now(UTC) - timedelta(days=30), None),
|
||||
"Last 90 days": (datetime.now(UTC) - timedelta(days=90), None),
|
||||
"Year to date": (datetime(datetime.now(UTC).year, 1, 1, tzinfo=UTC), None),
|
||||
"Tutto lo storico": (None, None),
|
||||
"Ultimi 30 giorni": (datetime.now(UTC) - timedelta(days=30), None),
|
||||
"Ultimi 90 giorni": (datetime.now(UTC) - timedelta(days=90), None),
|
||||
"Da inizio anno": (
|
||||
datetime(datetime.now(UTC).year, 1, 1, tzinfo=UTC),
|
||||
None,
|
||||
),
|
||||
}
|
||||
pick = st.selectbox(label, list(options.keys()), index=0)
|
||||
return options[pick]
|
||||
@@ -39,28 +42,29 @@ def _date_window(label: str) -> tuple[datetime | None, datetime | None]:
|
||||
def render() -> None:
|
||||
st.title("📈 Equity")
|
||||
st.caption(
|
||||
"Cumulative realised P&L, drawdown, and per-trade distribution. "
|
||||
"Computed from closed positions in `data/state.sqlite`."
|
||||
"P&L realizzato cumulato, drawdown e distribuzione per trade. "
|
||||
"Calcolato dalle posizioni chiuse in `data/state.sqlite`."
|
||||
)
|
||||
|
||||
start, end = _date_window("Window")
|
||||
start, end = _date_window("Finestra")
|
||||
|
||||
db_path = _resolve_db()
|
||||
positions = load_closed_positions(db_path=db_path, start=start, end=end)
|
||||
|
||||
if not positions:
|
||||
st.info(
|
||||
"No closed positions in the selected window yet. "
|
||||
"The equity curve will populate as soon as the engine closes its first trade."
|
||||
"Nessuna posizione chiusa nella finestra selezionata. "
|
||||
"La curva equity si popolerà non appena il motore chiuderà "
|
||||
"il primo trade."
|
||||
)
|
||||
return
|
||||
|
||||
# KPI strip
|
||||
# Striscia KPI
|
||||
kpis = compute_kpis(positions)
|
||||
cols = st.columns(5)
|
||||
cols[0].metric("Closed trades", kpis.n_trades)
|
||||
cols[0].metric("Trade chiusi", kpis.n_trades)
|
||||
cols[1].metric("Win rate", f"{kpis.win_rate:.0%}")
|
||||
cols[2].metric("Total P&L", f"${float(kpis.total_pnl_usd):+.2f}")
|
||||
cols[2].metric("P&L totale", f"${float(kpis.total_pnl_usd):+.2f}")
|
||||
cols[3].metric("Edge / trade", f"${float(kpis.edge_per_trade_usd):+.2f}")
|
||||
cols[4].metric(
|
||||
"Max drawdown",
|
||||
@@ -82,14 +86,14 @@ def render() -> None:
|
||||
}
|
||||
)
|
||||
|
||||
st.subheader("Cumulative P&L (USD)")
|
||||
st.subheader("P&L cumulato (USD)")
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df["timestamp"],
|
||||
y=df["cumulative_pnl_usd"],
|
||||
mode="lines+markers",
|
||||
name="cumulative P&L",
|
||||
name="P&L cumulato",
|
||||
line={"color": "#2ecc71", "width": 2},
|
||||
)
|
||||
)
|
||||
@@ -122,18 +126,18 @@ def render() -> None:
|
||||
)
|
||||
st.plotly_chart(dd_fig, use_container_width=True)
|
||||
|
||||
# PnL distribution
|
||||
st.subheader("P&L distribution by close reason")
|
||||
# Distribuzione P&L
|
||||
st.subheader("Distribuzione P&L per motivo di chiusura")
|
||||
by_reason: dict[str, list[float]] = {}
|
||||
for pos in positions:
|
||||
if pos.pnl_usd is None:
|
||||
continue
|
||||
by_reason.setdefault(pos.close_reason or "(unknown)", []).append(
|
||||
by_reason.setdefault(pos.close_reason or "(sconosciuto)", []).append(
|
||||
float(pos.pnl_usd)
|
||||
)
|
||||
|
||||
counts = Counter(
|
||||
(pos.close_reason or "(unknown)") for pos in positions
|
||||
(pos.close_reason or "(sconosciuto)") for pos in positions
|
||||
)
|
||||
cols = st.columns(min(len(counts), 6) or 1)
|
||||
for col, (reason, count) in zip(cols, counts.most_common(6), strict=False):
|
||||
@@ -141,28 +145,30 @@ def render() -> None:
|
||||
|
||||
hist_fig = go.Figure()
|
||||
for reason, pnls in by_reason.items():
|
||||
hist_fig.add_trace(go.Histogram(x=pnls, name=reason, opacity=0.6, nbinsx=30))
|
||||
hist_fig.add_trace(
|
||||
go.Histogram(x=pnls, name=reason, opacity=0.6, nbinsx=30)
|
||||
)
|
||||
hist_fig.update_layout(
|
||||
barmode="overlay",
|
||||
height=320,
|
||||
margin={"l": 10, "r": 10, "t": 30, "b": 10},
|
||||
xaxis_title="P&L (USD)",
|
||||
yaxis_title="trades",
|
||||
yaxis_title="numero trade",
|
||||
legend={"orientation": "h", "y": 1.1},
|
||||
)
|
||||
st.plotly_chart(hist_fig, use_container_width=True)
|
||||
|
||||
# Monthly table
|
||||
st.subheader("Per-month stats")
|
||||
# Tabella mensile
|
||||
st.subheader("Statistiche mensili")
|
||||
months = compute_monthly_stats(positions)
|
||||
rows = [
|
||||
{
|
||||
"month": m.year_month,
|
||||
"trades": m.n_trades,
|
||||
"wins": m.n_wins,
|
||||
"win_rate": f"{m.win_rate:.0%}",
|
||||
"mese": m.year_month,
|
||||
"trade": m.n_trades,
|
||||
"vittorie": m.n_wins,
|
||||
"win rate": f"{m.win_rate:.0%}",
|
||||
"P&L (USD)": f"{float(m.pnl_usd):+.2f}",
|
||||
"avg / trade": f"{float(m.avg_pnl_usd):+.2f}",
|
||||
"media / trade": f"{float(m.avg_pnl_usd):+.2f}",
|
||||
}
|
||||
for m in months
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user