feat(web): Streamlit dashboard (per-account books, theses, orders, logs) + CLI report
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
This commit is contained in:
1 parent
709870b1d5
commit
05a8457067
3 files changed
+226
No files matched your search
Whitespace-only changes.
@@ -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())
|
||||||
@@ -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.")
|
||||||
Reference in new issue
Block a user