Introduces a dashboard-managed agent registry (`state/agents.json`) used by nightly research and market-open execution, including support for custom/disabled agents and per-agent time budgets. Expands personas to four agents (adds Maverick), shifts pick targets to include mandatory options, and updates prompts/finalization nudges to enforce option research without hard-failing runs. Strengthens execution safety by adding option-specific guardrails (conviction floor, premium cap, reserved option slot through trade/day/buying-power caps), validates OCC option symbols in schemas, and normalizes invalid option direction cases. The dashboard was refactored into modular views, adds an Agents admin tab, and improves live portfolio visibility with Alpaca-first data and snapshot fallback.
106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
"""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()
|