"""ENTRYPOINT #4 โ€” local Streamlit dashboard. Read-only over the trading state (positions, snapshots, orders, theses, reports, logs) plus one read-WRITE surface: the Agents tab, which manages the agent roster in state/agents.json (add/edit/disable agents, per-agent research budgets) โ€” the nightly run reads that registry. uv run streamlit run web/dashboard.py """ from __future__ import annotations import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import streamlit as st # noqa: E402 st.set_page_config(page_title="Overnight Trading Agent", layout="wide") from web.views import agents_admin, logs, orders, portfolio, reports, theses # noqa: E402 from web.views.common import live_overview, query_df # noqa: E402 # ---------------- sidebar ---------------- _REFRESH = {"Off": 0, "30s": 30, "60s": 60, "5 min": 300} refresh_choice = st.sidebar.selectbox("Auto-refresh", list(_REFRESH), index=0) st.sidebar.caption("Alpaca is the source of truth for live positions.") # ---------------- header ---------------- st.title("๐ŸŒ™ Overnight Trading Agent") # latest snapshot per account (one book per persona), plus the prior day's close for deltas _LATEST = """ SELECT s.account, s.equity, s.cash, s.buying_power FROM snapshots s JOIN (SELECT account, MAX(ts) mt FROM snapshots GROUP BY account) m ON s.account = m.account AND s.ts = m.mt """ _PREV = """ SELECT s.account, s.equity FROM snapshots s JOIN ( SELECT account, MAX(ts) mt FROM snapshots WHERE DATE(ts) < (SELECT DATE(MAX(ts)) FROM snapshots) GROUP BY account ) m ON s.account = m.account AND s.ts = m.mt """ # LIVE-first: pull accounts + positions straight from Alpaca (60s cache); the DB snapshot # (written at the last morning reconcile) is only the offline fallback. Deltas always # compare against the previous day's DB snapshot. live_acc, live_pos = live_overview() if not live_acc.empty: latest, source = live_acc, "live" else: latest, source = query_df(_LATEST), "snapshot" prev = query_df(_PREV) prev_eq = dict(zip(prev["account"], prev["equity"])) if not prev.empty else {} if latest.empty: st.info("No account data yet โ€” set Alpaca keys in .env (live) or run execution once (snapshot).") else: st.caption("๐ŸŸข live from Alpaca (refreshes โ‰ค60s)" if source == "live" else "๐ŸŸก offline โ€” showing last reconciled snapshot") total_eq = latest["equity"].sum() total_prev = sum(prev_eq.get(a, e) for a, e in zip(latest["account"], latest["equity"])) if source == "live": upl = live_pos["unrealized_pl"].sum() if not live_pos.empty else 0.0 else: pos = query_df("SELECT COALESCE(SUM(unrealized_pl),0) AS pl FROM positions") upl = pos.iloc[0]["pl"] if not pos.empty else 0.0 cols = st.columns(2 + len(latest)) cols[0].metric( "Total Equity", f"${total_eq:,.0f}", delta=f"${total_eq - total_prev:+,.0f}" if prev_eq else None, ) cols[1].metric("Unrealized PnL", f"${upl:,.0f}") for col, (_, acc) in zip(cols[2:], latest.iterrows()): p = prev_eq.get(acc["account"]) col.metric( acc["account"], f"${acc['equity']:,.0f}", delta=f"${acc['equity'] - p:+,.0f}" if p is not None else None, ) tab_port, tab_theses, tab_reports, tab_orders, tab_agents, tab_logs = st.tabs( ["๐Ÿ“Š Portfolio", "๐Ÿง  Tonight's Theses", "๐Ÿ“ Reports", "๐Ÿงพ Orders", "๐Ÿค– Agents", "๐Ÿ“œ Logs"] ) with tab_port: portfolio.render(latest, live_pos if source == "live" else None) with tab_theses: theses.render() with tab_reports: reports.render() with tab_orders: orders.render() with tab_agents: agents_admin.render() with tab_logs: logs.render() # ---------------- auto-refresh ---------------- _interval = _REFRESH[refresh_choice] if _interval: time.sleep(_interval) st.rerun()