diff --git a/reporting/__init__.py b/reporting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reporting/report.py b/reporting/report.py new file mode 100644 index 0000000..7cd4da2 --- /dev/null +++ b/reporting/report.py @@ -0,0 +1,69 @@ +"""ENTRYPOINT #3 — quick CLI summary of holdings, PnL, orders, and last night's theses. +The dashboard covers this visually; this is a terminal-friendly glance. + + uv run python -m reporting.report +""" + +from __future__ import annotations + +import json +import sqlite3 +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from config.settings import get_settings # noqa: E402 + + +def main() -> int: + s = get_settings() + print("=== Overnight Trading Agent — report ===\n") + + if s.db_path.exists(): + con = sqlite3.connect(str(s.db_path)) + con.row_factory = sqlite3.Row + snap = con.execute("SELECT * FROM snapshots ORDER BY ts DESC LIMIT 1").fetchone() + if snap: + print(f"Equity ${snap['equity']:,.2f} | cash ${snap['cash']:,.2f} | " + f"buying power ${snap['buying_power']:,.2f}") + print(f" (as of {snap['ts']})") + positions = con.execute( + "SELECT * FROM positions ORDER BY market_value DESC" + ).fetchall() + if positions: + print("\nHoldings:") + for p in positions: + print(f" {p['symbol']:6} qty {p['qty']:>6} @ {p['avg_entry']:>8} " + f"→ mv {p['market_value']:>10} | uPnL {p['unrealized_pl']:>8}") + orders = con.execute("SELECT * FROM orders ORDER BY submitted_at DESC LIMIT 10").fetchall() + if orders: + print("\nRecent orders:") + for o in orders: + print(f" {o['trade_date']} {o['agent']:12} {o['ticker']:6} x{o['qty']:<4} " + f"{o['status']}") + con.close() + else: + print("No state DB yet — run execution once to populate it.") + + dates = ( + sorted((p.name for p in s.output_dir.iterdir() if p.is_dir()), reverse=True) + if s.output_dir.exists() + else [] + ) + if dates: + d = dates[0] + print(f"\nLatest theses ({d}):") + for f in sorted((s.output_dir / d).glob("*.json")): + if f.name.startswith("_"): + continue + j = json.loads(f.read_text()) + picks = ", ".join(f"{p['ticker']}({p['conviction']:.2f})" for p in j["picks"]) + print(f" {j['agent_name']:35} [{j['status']:8}] {picks or '(no picks)'}") + else: + print("\nNo nightly output yet.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web/dashboard.py b/web/dashboard.py new file mode 100644 index 0000000..51a6d69 --- /dev/null +++ b/web/dashboard.py @@ -0,0 +1,157 @@ +"""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_orders, tab_logs = st.tabs( + ["📊 Portfolio", "🧠 Tonight's Theses", "🧾 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)) + +# ---------------- 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.")