Upgrade mission-control UI with charts and live metrics API without impacting trading loop
This commit is contained in:
64
app.py
64
app.py
@@ -33,6 +33,70 @@ def curate_now():
|
||||
def api_account():
|
||||
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("/")
|
||||
def home(request: Request):
|
||||
db = SessionLocal()
|
||||
|
||||
@@ -4,19 +4,31 @@
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>alpaca-llm-bot-v1</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
:root { --bg:#0b0f17; --card:#111827; --text:#e5e7eb; --muted:#9ca3af; --ok:#10b981; --bad:#ef4444; --acc:#60a5fa; }
|
||||
body{background:var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif;margin:0;padding:24px}
|
||||
:root { --bg:#070b12; --card:#0f172a; --line:#1f2937; --text:#e5e7eb; --muted:#9ca3af; --buy:#22c55e; --sell:#ef4444; --hold:#94a3b8; --acc:#60a5fa; }
|
||||
* { 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}
|
||||
.card{background:var(--card);border:1px solid #1f2937;border-radius:12px;padding:14px}
|
||||
.h{font-size:13px;color:var(--muted)} .v{font-size:24px;font-weight:700}
|
||||
table{width:100%;border-collapse:collapse} th,td{padding:8px;border-bottom:1px solid #1f2937;text-align:left;font-size:13px}
|
||||
.buy{color:var(--ok)} .sell{color:var(--bad)} .hold{color:var(--muted)}
|
||||
.row{display:grid;grid-template-columns:2fr 1fr;gap:12px}
|
||||
.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:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
|
||||
.v{font-size:26px;font-weight:700;margin-top:4px}
|
||||
.layout{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>
|
||||
</head>
|
||||
<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="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>
|
||||
@@ -25,16 +37,35 @@
|
||||
<div class="card"><div class="h">Curated Insights</div><div class="v">{{ stats.insights }}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<h3>Recent Decisions</h3>
|
||||
<div class="layout" style="margin-top:12px">
|
||||
<div class="charts">
|
||||
<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>
|
||||
<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>
|
||||
{% for d in decisions %}
|
||||
<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>{{ '%.2f'|format(d.confidence or 0) }}</td>
|
||||
<td>{{ d.status }}</td>
|
||||
@@ -47,28 +78,76 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Curated Market Briefs</h3>
|
||||
<div class="card" style="max-height:520px;overflow:auto">
|
||||
<h3>Curated Briefs <span class="pill">latest 30</span></h3>
|
||||
<div class="card scroll">
|
||||
{% 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 style="font-size:13px;line-height:1.4">{{ i.summary }}</div>
|
||||
<div style="font-size:13px;line-height:1.45">{{ i.summary }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Recent Trades</h3>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead><tr><th>Time</th><th>Symbol</th><th>Side</th><th>Notional</th><th>Order ID</th></tr></thead>
|
||||
<tbody>
|
||||
{% for t in trades %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
let decisionChart, activityChart;
|
||||
|
||||
function renderLeaderboard(rows){
|
||||
const el = document.getElementById('leaderboard');
|
||||
if(!rows || !rows.length){ el.innerHTML = '<div class="sub">No data yet.</div>'; return; }
|
||||
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('');
|
||||
}
|
||||
|
||||
function upsertCharts(m){
|
||||
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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user