"""Equity page — cumulative PnL, drawdown, distributions.""" from __future__ import annotations import os from collections import Counter from datetime import UTC, datetime, timedelta from pathlib import Path import pandas as pd import plotly.graph_objects as go import streamlit as st from cerbero_bite.gui.data_layer import ( DEFAULT_DB_PATH, compute_equity_curve, compute_kpis, compute_monthly_stats, load_closed_positions, ) def _resolve_db() -> Path: return Path(os.environ.get("CERBERO_BITE_GUI_DB", DEFAULT_DB_PATH)) def _date_window(label: str) -> tuple[datetime | None, datetime | None]: """UI control for picking the analytics window.""" 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), } pick = st.selectbox(label, list(options.keys()), index=0) return options[pick] 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`." ) start, end = _date_window("Window") 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." ) return # KPI strip kpis = compute_kpis(positions) cols = st.columns(5) cols[0].metric("Closed trades", 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[3].metric("Edge / trade", f"${float(kpis.edge_per_trade_usd):+.2f}") cols[4].metric( "Max drawdown", f"${float(kpis.max_drawdown_usd):.2f}", delta=f"{kpis.max_drawdown_pct:.1%}", delta_color="inverse", ) st.divider() # Equity curve + drawdown curve = compute_equity_curve(positions) df = pd.DataFrame( { "timestamp": [p.timestamp for p in curve], "cumulative_pnl_usd": [float(p.cumulative_pnl_usd) for p in curve], "drawdown_usd": [float(p.drawdown_usd) for p in curve], "realized_pnl_usd": [float(p.realized_pnl_usd) for p in curve], } ) st.subheader("Cumulative P&L (USD)") fig = go.Figure() fig.add_trace( go.Scatter( x=df["timestamp"], y=df["cumulative_pnl_usd"], mode="lines+markers", name="cumulative P&L", line={"color": "#2ecc71", "width": 2}, ) ) fig.add_hline(y=0, line_dash="dot", line_color="grey", opacity=0.5) fig.update_layout( height=320, margin={"l": 10, "r": 10, "t": 30, "b": 10}, xaxis_title=None, yaxis_title="USD", ) st.plotly_chart(fig, use_container_width=True) st.subheader("Drawdown (USD)") dd_fig = go.Figure() dd_fig.add_trace( go.Scatter( x=df["timestamp"], y=-df["drawdown_usd"], mode="lines", fill="tozeroy", name="drawdown", line={"color": "#e74c3c", "width": 1.5}, ) ) dd_fig.update_layout( height=220, margin={"l": 10, "r": 10, "t": 30, "b": 10}, xaxis_title=None, yaxis_title="USD", ) st.plotly_chart(dd_fig, use_container_width=True) # PnL distribution st.subheader("P&L distribution by close reason") 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( float(pos.pnl_usd) ) counts = Counter( (pos.close_reason or "(unknown)") 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): col.metric(reason, count) 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.update_layout( barmode="overlay", height=320, margin={"l": 10, "r": 10, "t": 30, "b": 10}, xaxis_title="P&L (USD)", yaxis_title="trades", legend={"orientation": "h", "y": 1.1}, ) st.plotly_chart(hist_fig, use_container_width=True) # Monthly table st.subheader("Per-month stats") 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%}", "P&L (USD)": f"{float(m.pnl_usd):+.2f}", "avg / trade": f"{float(m.avg_pnl_usd):+.2f}", } for m in months ] st.dataframe(rows, use_container_width=True, hide_index=True) render()