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():
|
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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user