"""ENTRYPOINT #4 — local Streamlit dashboard. Read-only over the state DB (positions, snapshots, orders, theses), the nightly output JSON, and the log files. Everything the system did, in one place. uv run streamlit run web/dashboard.py """ from __future__ import annotations import json import sqlite3 import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import pandas as pd # noqa: E402 import streamlit as st # noqa: E402 from agents.schemas import AgentOutput # noqa: E402 from config.settings import get_settings # noqa: E402 S = get_settings() st.set_page_config(page_title="Overnight Trading Agent", layout="wide") def _con() -> sqlite3.Connection | None: if not S.db_path.exists(): return None con = sqlite3.connect(str(S.db_path)) con.row_factory = sqlite3.Row return con def _query_df(sql: str, params: tuple = ()) -> pd.DataFrame: con = _con() if con is None: return pd.DataFrame() try: return pd.read_sql_query(sql, con, params=params) except Exception: return pd.DataFrame() finally: con.close() def _trade_dates() -> list[str]: if not S.output_dir.exists(): return [] return sorted((p.name for p in S.output_dir.iterdir() if p.is_dir()), reverse=True) # ---------------- header ---------------- st.title("🌙 Overnight Trading Agent") # latest snapshot per account (one book per persona) _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 """ latest = _query_df(_LATEST) c1, c2, c3, c4 = st.columns(4) if not latest.empty: c1.metric("Total Equity", f"${latest['equity'].sum():,.0f}") c2.metric("Total Cash", f"${latest['cash'].sum():,.0f}") c3.metric("Accounts", str(len(latest))) pos = _query_df("SELECT COALESCE(SUM(unrealized_pl),0) AS pl FROM positions") c4.metric("Unrealized PnL", f"${pos.iloc[0]['pl']:,.0f}" if not pos.empty else "$0") else: c1.info("No account snapshot yet — run execution once Alpaca keys are set.") tab_port, tab_theses, tab_reports, tab_orders, tab_logs = st.tabs( ["📊 Portfolio", "🧠 Tonight's Theses", "📝 Reports", "🧾 Orders", "📜 Logs"] ) # ---------------- portfolio ---------------- with tab_port: if latest.empty: st.info("No positions recorded yet — run execution once Alpaca keys are set.") else: for _, acc in latest.iterrows(): st.subheader(f"{acc['account']} · ${acc['equity']:,.0f} equity · ${acc['cash']:,.0f} cash") p = _query_df( "SELECT symbol, qty, avg_entry, market_value, unrealized_pl FROM positions " "WHERE account = ? ORDER BY market_value DESC", (acc["account"],), ) if p.empty: st.caption("all cash — no open positions") else: st.dataframe(p, use_container_width=True, hide_index=True) equity = _query_df("SELECT ts, account, equity FROM snapshots ORDER BY ts") if len(equity) > 1: st.subheader("Equity curves") equity["ts"] = pd.to_datetime(equity["ts"]) st.line_chart(equity.pivot_table(index="ts", columns="account", values="equity")) # ---------------- theses ---------------- with tab_theses: dates = _trade_dates() if not dates: st.info("No nightly output yet. Run: `uv run python -m orchestration.run_nightly --compressed`") else: date = st.selectbox("Trade date", dates) day_dir = S.output_dir / date files = sorted(f for f in day_dir.glob("*.json") if not f.name.startswith("_")) cols = st.columns(max(len(files), 1)) for col, f in zip(cols, files): try: out = AgentOutput.model_validate_json(f.read_text()) except Exception as e: col.error(f"{f.name}: {e}") continue with col: st.markdown(f"### {out.agent_name}") st.caption(f"status **{out.status.value}** · {len(out.picks)} picks · " f"{out.steps_used} steps · {out.wall_seconds:.0f}s") for p in out.picks: with st.expander(f"{p.ticker} · conv {p.conviction:.2f} · {p.time_horizon.value}"): st.write(p.thesis) st.caption( f"weight {p.requested_weight_pct:.0%} · stop {p.stop_loss_pct} · " f"take {p.take_profit_pct} · {p.direction.value}" ) if p.catalysts: st.caption("catalysts: " + ", ".join(p.catalysts)) if out.reasoning_trace: with st.expander("🔍 reasoning trace"): st.code("\n".join(out.reasoning_trace), language="text") if out.errors: st.warning("errors: " + "; ".join(out.errors)) # ---------------- reports ---------------- with tab_reports: rdir = S.reports_dir rdates = ( sorted((p.name for p in rdir.iterdir() if p.is_dir()), reverse=True) if rdir.exists() else [] ) if not rdates: st.info("No reports yet — generated during the nightly run (state/reports/).") else: rdate = st.selectbox("Report date", rdates, key="rdate") ddir = rdir / rdate # daily.md first, then each agent mds = sorted(ddir.glob("*.md"), key=lambda p: (p.name != "daily.md", p.name)) for md in mds: st.markdown(md.read_text()) pdf = md.with_suffix(".pdf") if pdf.exists(): st.download_button( f"⬇ Download {pdf.name}", pdf.read_bytes(), file_name=pdf.name, mime="application/pdf", key=f"dl_{md.stem}", ) for png in sorted(ddir.glob(f"{md.stem}_*.png")): st.image(str(png)) st.divider() # ---------------- orders ---------------- with tab_orders: orders = _query_df( "SELECT trade_date, agent, ticker, side, qty, est_price, stop_price, take_price, " "status, alpaca_order_id, submitted_at FROM orders ORDER BY submitted_at DESC LIMIT 200" ) if orders.empty: st.info("No orders placed yet.") else: st.dataframe(orders, use_container_width=True, hide_index=True) # ---------------- logs ---------------- with tab_logs: log_files = sorted(S.log_dir.glob("*.log"), reverse=True) if S.log_dir.exists() else [] if not log_files: st.info("No logs yet.") else: choice = st.selectbox("Log file", [f.name for f in log_files]) n = st.slider("Lines", 20, 1000, 200, 20) text = (S.log_dir / choice).read_text().splitlines()[-n:] st.code("\n".join(text), language="text") st.caption("Read-only. Refresh the page to update. Alpaca is the source of truth for live positions.")