Upgrade mission-control UI with charts and live metrics API without impacting trading loop

This commit is contained in:
2026-02-26 17:57:42 -08:00
parent 94aba00967
commit dbf6bf4e73
2 changed files with 171 additions and 28 deletions

64
app.py
View File

@@ -33,6 +33,70 @@ def curate_now():
def api_account(): def api_account():
return JSONResponse(account_snapshot()) return JSONResponse(account_snapshot())
@app.get("/api/metrics")
def api_metrics(hours: int = 72):
db = SessionLocal()
try:
since = datetime.utcnow() - timedelta(hours=hours)
decs = db.query(BotDecision).filter(BotDecision.ts >= since).order_by(BotDecision.ts.asc()).all()
trades = db.query(TradeExecution).filter(TradeExecution.ts >= since).order_by(TradeExecution.ts.asc()).all()
# hour buckets
buckets = {}
for d in decs:
k = d.ts.strftime("%m-%d %H:00")
buckets.setdefault(k, {"buy": 0, "sell": 0, "hold": 0, "executed": 0, "failed": 0})
if d.action in ("buy", "sell", "hold"):
buckets[k][d.action] += 1
if d.status == "executed":
buckets[k]["executed"] += 1
if d.status == "failed":
buckets[k]["failed"] += 1
labels = list(buckets.keys())
buy = [buckets[k]["buy"] for k in labels]
sell = [buckets[k]["sell"] for k in labels]
hold = [buckets[k]["hold"] for k in labels]
executed = [buckets[k]["executed"] for k in labels]
failed = [buckets[k]["failed"] for k in labels]
# cumulative notional (proxy activity curve)
t_labels, t_values = [], []
c = 0.0
for t in trades:
c += float(t.notional or 0)
t_labels.append(t.ts.strftime("%m-%d %H:%M"))
t_values.append(round(c, 2))
# symbol leaderboard
lb = {}
for d in decs:
row = lb.setdefault(d.symbol, {"symbol": d.symbol, "decisions": 0, "executed": 0})
row["decisions"] += 1
if d.status == "executed":
row["executed"] += 1
leaderboard = sorted(lb.values(), key=lambda x: x["executed"], reverse=True)
return {
"ok": True,
"hours": hours,
"decisionSeries": {
"labels": labels,
"buy": buy,
"sell": sell,
"hold": hold,
"executed": executed,
"failed": failed,
},
"activitySeries": {
"labels": t_labels,
"cumulativeNotional": t_values,
},
"leaderboard": leaderboard,
}
finally:
db.close()
@app.get("/") @app.get("/")
def home(request: Request): def home(request: Request):
db = SessionLocal() db = SessionLocal()

View File

@@ -4,19 +4,31 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>alpaca-llm-bot-v1</title> <title>alpaca-llm-bot-v1</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <style>
:root { --bg:#0b0f17; --card:#111827; --text:#e5e7eb; --muted:#9ca3af; --ok:#10b981; --bad:#ef4444; --acc:#60a5fa; } :root { --bg:#070b12; --card:#0f172a; --line:#1f2937; --text:#e5e7eb; --muted:#9ca3af; --buy:#22c55e; --sell:#ef4444; --hold:#94a3b8; --acc:#60a5fa; }
body{background:var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif;margin:0;padding:24px} * { box-sizing: border-box; }
body{background:radial-gradient(1200px 800px at 20% -10%, #1a2340 0%, var(--bg) 40%);color:var(--text);font-family:Inter,system-ui,sans-serif;margin:0;padding:20px}
h2,h3{margin:8px 0 14px}
.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px} .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
.card{background:var(--card);border:1px solid #1f2937;border-radius:12px;padding:14px} .card{background:linear-gradient(180deg,#101b33 0%,var(--card) 100%);border:1px solid var(--line);border-radius:14px;padding:14px;box-shadow:0 8px 30px rgba(0,0,0,.25)}
.h{font-size:13px;color:var(--muted)} .v{font-size:24px;font-weight:700} .h{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
table{width:100%;border-collapse:collapse} th,td{padding:8px;border-bottom:1px solid #1f2937;text-align:left;font-size:13px} .v{font-size:26px;font-weight:700;margin-top:4px}
.buy{color:var(--ok)} .sell{color:var(--bad)} .hold{color:var(--muted)} .layout{display:grid;grid-template-columns:2fr 1fr;gap:12px}
.row{display:grid;grid-template-columns:2fr 1fr;gap:12px} .charts{display:grid;grid-template-columns:1fr;gap:12px}
.chart-wrap{height:270px}
table{width:100%;border-collapse:collapse}
th,td{padding:8px;border-bottom:1px solid var(--line);text-align:left;font-size:13px}
.buy{color:var(--buy)} .sell{color:var(--sell)} .hold{color:var(--hold)}
.pill{display:inline-block;padding:3px 8px;border:1px solid var(--line);border-radius:999px;font-size:12px;color:var(--muted)}
.scroll{max-height:430px;overflow:auto}
</style> </style>
</head> </head>
<body> <body>
<h2>alpaca-llm-bot-v1</h2> <h2>alpaca-llm-bot-v1 · Mission Control</h2>
<div class="sub">Autonomous trading telemetry · updates every 60s · non-blocking UI reads</div>
<div class="grid"> <div class="grid">
<div class="card"><div class="h">Decisions (24h)</div><div class="v">{{ stats.decisions }}</div></div> <div class="card"><div class="h">Decisions (24h)</div><div class="v">{{ stats.decisions }}</div></div>
<div class="card"><div class="h">Trades (24h)</div><div class="v">{{ stats.trades }}</div></div> <div class="card"><div class="h">Trades (24h)</div><div class="v">{{ stats.trades }}</div></div>
@@ -25,16 +37,35 @@
<div class="card"><div class="h">Curated Insights</div><div class="v">{{ stats.insights }}</div></div> <div class="card"><div class="h">Curated Insights</div><div class="v">{{ stats.insights }}</div></div>
</div> </div>
<div class="row"> <div class="layout" style="margin-top:12px">
<div> <div class="charts">
<h3>Recent Decisions</h3>
<div class="card"> <div class="card">
<div class="h">Decision Flow (last 72h)</div>
<div class="chart-wrap"><canvas id="decisionChart"></canvas></div>
</div>
<div class="card">
<div class="h">Cumulative Notional Activity</div>
<div class="chart-wrap"><canvas id="activityChart"></canvas></div>
</div>
</div>
<div class="card">
<div class="h">Symbol Leaderboard</div>
<div id="leaderboard" class="scroll" style="margin-top:8px"></div>
</div>
</div>
<div style="margin-top:12px" class="layout">
<div>
<h3>Recent Decisions <span class="pill">latest 120</span></h3>
<div class="card scroll">
<table> <table>
<thead><tr><th>Time</th><th>Symbol</th><th>Action</th><th>Confidence</th><th>Status</th><th>Reason</th></tr></thead> <thead><tr><th>Time</th><th>Symbol</th><th>Action</th><th>Conf</th><th>Status</th><th>Reason</th></tr></thead>
<tbody> <tbody>
{% for d in decisions %} {% for d in decisions %}
<tr> <tr>
<td>{{ d.ts }}</td><td>{{ d.symbol }}</td> <td>{{ d.ts }}</td>
<td>{{ d.symbol }}</td>
<td class="{{ d.action }}">{{ d.action }}</td> <td class="{{ d.action }}">{{ d.action }}</td>
<td>{{ '%.2f'|format(d.confidence or 0) }}</td> <td>{{ '%.2f'|format(d.confidence or 0) }}</td>
<td>{{ d.status }}</td> <td>{{ d.status }}</td>
@@ -47,28 +78,76 @@
</div> </div>
<div> <div>
<h3>Curated Market Briefs</h3> <h3>Curated Briefs <span class="pill">latest 30</span></h3>
<div class="card" style="max-height:520px;overflow:auto"> <div class="card scroll">
{% for i in insights %} {% for i in insights %}
<div style="margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #1f2937"> <div style="margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid var(--line)">
<div><b>{{ i.symbol }}</b> · <span class="h">{{ i.ts }}</span></div> <div><b>{{ i.symbol }}</b> · <span class="h">{{ i.ts }}</span></div>
<div style="font-size:13px;line-height:1.4">{{ i.summary }}</div> <div style="font-size:13px;line-height:1.45">{{ i.summary }}</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
<h3>Recent Trades</h3> <script>
<div class="card"> let decisionChart, activityChart;
<table>
<thead><tr><th>Time</th><th>Symbol</th><th>Side</th><th>Notional</th><th>Order ID</th></tr></thead> function renderLeaderboard(rows){
<tbody> const el = document.getElementById('leaderboard');
{% for t in trades %} if(!rows || !rows.length){ el.innerHTML = '<div class="sub">No data yet.</div>'; return; }
<tr><td>{{ t.ts }}</td><td>{{ t.symbol }}</td><td class="{{ t.side }}">{{ t.side }}</td><td>${{ '%.2f'|format(t.notional or 0) }}</td><td>{{ t.alpaca_order_id }}</td></tr> el.innerHTML = rows.map((r,i)=>`<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #1f2937"><span>#${i+1} <b>${r.symbol}</b></span><span class="sub">exec ${r.executed} / dec ${r.decisions}</span></div>`).join('');
{% endfor %} }
</tbody>
</table> function upsertCharts(m){
</div> const d = m.decisionSeries;
const a = m.activitySeries;
if(!decisionChart){
decisionChart = new Chart(document.getElementById('decisionChart'), {
type: 'line',
data: { labels: d.labels, datasets: [
{label:'buy', data:d.buy, borderColor:'#22c55e', tension:.25},
{label:'sell', data:d.sell, borderColor:'#ef4444', tension:.25},
{label:'hold', data:d.hold, borderColor:'#94a3b8', tension:.25},
{label:'executed', data:d.executed, borderColor:'#60a5fa', tension:.25},
]},
options: { responsive:true, maintainAspectRatio:false, plugins:{legend:{labels:{color:'#e5e7eb'}}}, scales:{x:{ticks:{color:'#9ca3af'}},y:{ticks:{color:'#9ca3af'}}} }
});
} else {
decisionChart.data.labels = d.labels;
decisionChart.data.datasets[0].data = d.buy;
decisionChart.data.datasets[1].data = d.sell;
decisionChart.data.datasets[2].data = d.hold;
decisionChart.data.datasets[3].data = d.executed;
decisionChart.update();
}
if(!activityChart){
activityChart = new Chart(document.getElementById('activityChart'), {
type:'bar',
data:{ labels:a.labels, datasets:[{label:'cum notional', data:a.cumulativeNotional, backgroundColor:'#60a5fa66', borderColor:'#60a5fa'}] },
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{labels:{color:'#e5e7eb'}}}, scales:{x:{ticks:{color:'#9ca3af'}},y:{ticks:{color:'#9ca3af'}}} }
});
} else {
activityChart.data.labels = a.labels;
activityChart.data.datasets[0].data = a.cumulativeNotional;
activityChart.update();
}
}
async function refreshMetrics(){
try{
const r = await fetch('/api/metrics?hours=72');
const m = await r.json();
if(!m.ok) return;
upsertCharts(m);
renderLeaderboard(m.leaderboard);
}catch(e){ }
}
refreshMetrics();
setInterval(refreshMetrics, 60000);
</script>
</body> </body>
</html> </html>