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:
@@ -38,40 +38,41 @@ def _position_label(p: PositionRecord) -> str:
|
||||
|
||||
def _render_header(position: PositionRecord) -> None:
|
||||
cols = st.columns(4)
|
||||
cols[0].metric("status", position.status)
|
||||
cols[0].metric("stato", position.status)
|
||||
cols[1].metric("spread", position.spread_type)
|
||||
cols[2].metric("contracts", position.n_contracts)
|
||||
cols[3].metric("credit (USD)", f"${float(position.credit_usd):+.2f}")
|
||||
cols[2].metric("contratti", position.n_contracts)
|
||||
cols[3].metric("credito (USD)", f"${float(position.credit_usd):+.2f}")
|
||||
st.caption(
|
||||
f"`{position.proposal_id}` · opened {humanize_dt(position.opened_at)} · "
|
||||
f"expiry {humanize_dt(position.expiry)}"
|
||||
f"`{position.proposal_id}` · aperta il "
|
||||
f"{humanize_dt(position.opened_at)} · scadenza "
|
||||
f"{humanize_dt(position.expiry)}"
|
||||
)
|
||||
|
||||
|
||||
def _render_legs(position: PositionRecord) -> None:
|
||||
st.subheader("Legs (entry snapshot)")
|
||||
st.subheader("Gambe (snapshot all'entrata)")
|
||||
rows = [
|
||||
{
|
||||
"leg": "short",
|
||||
"instrument": position.short_instrument,
|
||||
"gamba": "short",
|
||||
"strumento": position.short_instrument,
|
||||
"strike": float(position.short_strike),
|
||||
"side": "SELL",
|
||||
"lato": "VENDI",
|
||||
"size": position.n_contracts,
|
||||
"delta_at_entry": float(position.delta_at_entry),
|
||||
"delta all'entrata": float(position.delta_at_entry),
|
||||
},
|
||||
{
|
||||
"leg": "long",
|
||||
"instrument": position.long_instrument,
|
||||
"gamba": "long",
|
||||
"strumento": position.long_instrument,
|
||||
"strike": float(position.long_strike),
|
||||
"side": "BUY",
|
||||
"lato": "COMPRA",
|
||||
"size": position.n_contracts,
|
||||
"delta_at_entry": "—", # only short delta is persisted
|
||||
"delta all'entrata": "—",
|
||||
},
|
||||
]
|
||||
st.dataframe(rows, use_container_width=True, hide_index=True)
|
||||
st.caption(
|
||||
"Live mid/greeks are not pulled from MCP by the GUI. "
|
||||
"Refresh shown by the engine via the Audit page."
|
||||
"Mid e greche live non vengono richiesti agli MCP dal cruscotto. "
|
||||
"Il refresh è demandato al motore: visibile nella pagina Audit."
|
||||
)
|
||||
|
||||
|
||||
@@ -79,25 +80,25 @@ def _render_distance(position: PositionRecord) -> None:
|
||||
metrics = compute_distance_metrics(position)
|
||||
cols = st.columns(5)
|
||||
cols[0].metric(
|
||||
"Short strike OTM",
|
||||
"Short OTM %",
|
||||
f"{metrics.short_strike_otm_pct:.1%}"
|
||||
if metrics.short_strike_otm_pct is not None
|
||||
else "—",
|
||||
)
|
||||
cols[1].metric(
|
||||
"Days to expiry",
|
||||
"Giorni a scadenza",
|
||||
metrics.days_to_expiry if metrics.days_to_expiry is not None else "—",
|
||||
)
|
||||
cols[2].metric(
|
||||
"Days held",
|
||||
"Giorni in tenuta",
|
||||
metrics.days_held if metrics.days_held is not None else "—",
|
||||
)
|
||||
cols[3].metric("Δ at entry", f"{metrics.delta_at_entry:+.3f}")
|
||||
cols[4].metric("Width % of spot", f"{metrics.width_pct_of_spot:.1%}")
|
||||
cols[3].metric("Δ all'entrata", f"{metrics.delta_at_entry:+.3f}")
|
||||
cols[4].metric("Larghezza % spot", f"{metrics.width_pct_of_spot:.1%}")
|
||||
|
||||
|
||||
def _render_payoff(position: PositionRecord) -> None:
|
||||
st.subheader("Payoff at expiry")
|
||||
st.subheader("Payoff a scadenza")
|
||||
curve = compute_payoff_curve(position)
|
||||
|
||||
fig = go.Figure()
|
||||
@@ -107,7 +108,7 @@ def _render_payoff(position: PositionRecord) -> None:
|
||||
y=curve.pnl_grid_usd,
|
||||
mode="lines",
|
||||
line={"color": "#3498db", "width": 2.5},
|
||||
name="P&L at expiry",
|
||||
name="P&L a scadenza",
|
||||
fill="tozeroy",
|
||||
fillcolor="rgba(52,152,219,0.10)",
|
||||
)
|
||||
@@ -143,21 +144,21 @@ def _render_payoff(position: PositionRecord) -> None:
|
||||
line_dash="solid",
|
||||
line_color="#7f8c8d",
|
||||
opacity=0.4,
|
||||
annotation_text=f"entry spot {curve.spot_at_entry:.0f}",
|
||||
annotation_text=f"spot all'entrata {curve.spot_at_entry:.0f}",
|
||||
annotation_position="bottom",
|
||||
)
|
||||
fig.update_layout(
|
||||
height=380,
|
||||
margin={"l": 10, "r": 10, "t": 30, "b": 10},
|
||||
xaxis_title="ETH spot at expiry (USD)",
|
||||
xaxis_title="ETH spot a scadenza (USD)",
|
||||
yaxis_title="P&L (USD)",
|
||||
legend={"orientation": "h", "y": 1.1},
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
cols = st.columns(3)
|
||||
cols[0].metric("Max profit", f"${curve.max_profit_usd:+.2f}")
|
||||
cols[1].metric("Max loss", f"${curve.max_loss_usd:+.2f}")
|
||||
cols[0].metric("Profitto massimo", f"${curve.max_profit_usd:+.2f}")
|
||||
cols[1].metric("Perdita massima", f"${curve.max_loss_usd:+.2f}")
|
||||
cols[2].metric(
|
||||
"Breakeven",
|
||||
f"{curve.breakeven:.2f}" if curve.breakeven is not None else "—",
|
||||
@@ -165,10 +166,10 @@ def _render_payoff(position: PositionRecord) -> None:
|
||||
|
||||
|
||||
def _render_decisions(position: PositionRecord) -> None:
|
||||
st.subheader("Decision history")
|
||||
st.subheader("Storico decisioni")
|
||||
decisions = load_decisions_for_position(position.proposal_id)
|
||||
if not decisions:
|
||||
st.info("No decisions recorded for this position yet.")
|
||||
st.info("Nessuna decisione registrata per questa posizione.")
|
||||
return
|
||||
|
||||
rows = []
|
||||
@@ -180,46 +181,44 @@ def _render_decisions(position: PositionRecord) -> None:
|
||||
rows.append(
|
||||
{
|
||||
"timestamp": humanize_dt(d.timestamp),
|
||||
"decision_type": d.decision_type,
|
||||
"action": d.action_taken or "—",
|
||||
"notes": d.notes or "",
|
||||
"outputs": json.dumps(outputs, sort_keys=True)
|
||||
if outputs
|
||||
else "",
|
||||
"tipo decisione": d.decision_type,
|
||||
"azione": d.action_taken or "—",
|
||||
"note": d.notes or "",
|
||||
"output": json.dumps(outputs, sort_keys=True) if outputs else "",
|
||||
}
|
||||
)
|
||||
st.dataframe(rows, use_container_width=True, hide_index=True)
|
||||
|
||||
|
||||
def render() -> None:
|
||||
st.title("💼 Position")
|
||||
st.title("💼 Posizione")
|
||||
st.caption(
|
||||
"Drilldown on the trade: legs, payoff at expiry, decision history. "
|
||||
"All data is read from SQLite — no live MCP calls."
|
||||
"Drilldown sul trade: gambe, payoff a scadenza, storico decisioni. "
|
||||
"Tutti i dati arrivano da SQLite — nessuna chiamata MCP live."
|
||||
)
|
||||
|
||||
db_path = _resolve_db()
|
||||
|
||||
open_pos = load_open_positions(db_path=db_path)
|
||||
closed_recent = load_closed_positions(db_path=db_path)[-10:] # last 10
|
||||
closed_recent = load_closed_positions(db_path=db_path)[-10:]
|
||||
candidates: list[PositionRecord] = list(open_pos) + list(reversed(closed_recent))
|
||||
|
||||
if not candidates:
|
||||
st.info(
|
||||
"No positions to display. The page will populate once the "
|
||||
"engine opens its first trade."
|
||||
"Nessuna posizione da mostrare. La pagina si popolerà non "
|
||||
"appena il motore aprirà il primo trade."
|
||||
)
|
||||
return
|
||||
|
||||
labels = {_position_label(p): p for p in candidates}
|
||||
pick = st.selectbox(
|
||||
"Position",
|
||||
"Posizione",
|
||||
options=list(labels.keys()),
|
||||
index=0,
|
||||
)
|
||||
position = labels[pick]
|
||||
|
||||
# Allow deep-linking via ?proposal_id=...
|
||||
# Deep-link via ?proposal_id=…
|
||||
qp = st.query_params.get("proposal_id")
|
||||
if qp:
|
||||
try:
|
||||
@@ -228,7 +227,7 @@ def render() -> None:
|
||||
if override is not None:
|
||||
position = override
|
||||
except ValueError:
|
||||
st.warning(f"Invalid proposal_id query parameter: {qp}")
|
||||
st.warning(f"Parametro proposal_id non valido: {qp}")
|
||||
|
||||
st.divider()
|
||||
_render_header(position)
|
||||
|
||||
Reference in New Issue
Block a user