Add REST API with dashboard and configurable port (server.host/port, TM_PORT, --port)

This commit is contained in:
Tobias Zimmermann
2026-08-22 12:26:17 +02:00
parent 7959dd71ff
commit 98a332da79
11 changed files with 570 additions and 3 deletions
+184
View File
@@ -0,0 +1,184 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TradeMind</title>
<style>
:root {
--bg: #0e1116; --panel: #161b23; --border: #232b36;
--text: #d7dee8; --muted: #8b97a7;
--green: #2ecc71; --red: #e74c3c; --blue: #4da3ff;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text);
font: 15px/1.5 system-ui, "Segoe UI", Roboto, sans-serif; }
header { display: flex; align-items: center; gap: 12px; padding: 14px 20px;
border-bottom: 1px solid var(--border); background: var(--panel); }
header h1 { font-size: 17px; margin: 0; }
header .sym { color: var(--blue); font-weight: 600; }
header .ver { color: var(--muted); font-size: 12px; }
main { padding: 20px; max-width: 1200px; margin: 0 auto; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px; }
.card .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .4px; }
.card .value { font-size: 22px; font-weight: 650; margin-top: 4px; }
.pos { color: var(--green); } .neg { color: var(--red); }
section { margin-top: 24px; }
h2 { font-size: 15px; color: var(--muted); margin: 0 0 10px; text-transform: uppercase; letter-spacing: .5px; }
table { width: 100%; border-collapse: collapse; background: var(--panel);
border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 13.5px; }
th { color: var(--muted); font-weight: 500; }
tr:last-child td { border-bottom: none; }
badge, .badge { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 12px; }
.buy { background: rgba(46,204,113,.15); color: var(--green); }
.sell { background: rgba(231,76,60,.15); color: var(--red); }
.hold { background: rgba(139,151,167,.15); color: var(--muted); }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
button { background: var(--blue); color: #08111e; border: 0; border-radius: 8px;
padding: 9px 16px; font-weight: 600; cursor: pointer; }
button:disabled { opacity: .5; cursor: default; }
canvas { width: 100%; height: 260px; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; }
.hint { color: var(--muted); font-size: 13px; }
footer { color: var(--muted); font-size: 12px; text-align: center; padding: 18px; }
</style>
</head>
<body>
<header>
<h1>TradeMind <span class="sym" id="symbol"></span></h1>
<span class="ver" id="version"></span>
<span class="hint" id="updated"></span>
</header>
<main>
<section>
<div class="row">
<button id="runBtn" onclick="runPaper()">Run paper simulation</button>
<span class="hint" id="statusMsg"></span>
</div>
<div class="grid" id="cards"></div>
</section>
<section>
<h2>Equity curve</h2>
<canvas id="chart" width="1200" height="260"></canvas>
<p class="hint" id="chartHint">No data yet run a paper simulation.</p>
</section>
<section>
<h2>Last signals</h2>
<table>
<thead><tr><th>Time (UTC)</th><th>Action</th><th>Price</th><th>Score</th><th>Reason</th></tr></thead>
<tbody id="signals"></tbody>
</table>
</section>
</main>
<footer>Paper mode only this dashboard never places live orders.</footer>
<script>
"use strict";
const $ = (id) => document.getElementById(id);
function fmt(v, d = 2) {
if (v === null || v === undefined || isNaN(v)) return "";
return Number(v).toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
}
function badge(action) {
if (action === 1) return '<span class="badge buy">BUY</span>';
if (action === -1) return '<span class="badge sell">SELL</span>';
return '<span class="badge hold">HOLD</span>';
}
function card(label, value, cls = "") {
return `<div class="card"><div class="label">${label}</div><div class="value ${cls}">${value}</div></div>`;
}
async function jget(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(url + " -> " + r.status);
return r.json();
}
async function loadStatus() {
const s = await jget("/api/status");
$("symbol").textContent = s.symbol || "";
$("version").textContent = "v" + s.version;
const cards = [
card("Equity", fmt((s.last_run && s.last_run.summary && s.last_run.summary.final_equity) || s.trading.initial_balance)),
card("Total return", ((s.last_run?.summary?.total_return_pct)?.toFixed(2) ?? "") + " %",
(s.last_run?.summary?.total_return_pct ?? 0) >= 0 ? "pos" : "neg"),
card("Trades", s.last_run?.summary?.num_trades ?? ""),
card("Win rate", ((s.last_run?.summary?.win_rate ?? 0) * 100).toFixed(1) + " %"),
card("Sharpe", fmt(s.last_run?.summary?.sharpe ?? 0, 3)),
card("Max drawdown", ((s.last_run?.summary?.max_drawdown_pct ?? 0)).toFixed(2) + " %", "neg"),
];
$("cards").innerHTML = cards.join("");
if (s.last_run) $("chartHint").style.display = "none";
if (s.last_run) drawEquity(s.last_run.equity_curve || []);
}
async function loadSignals() {
const d = await jget("/api/signals?limit=25");
$("updated").textContent = "data: " + (d.broker || "") + " · " + new Date().toLocaleTimeString();
$("signals").innerHTML = d.signals
.map((x) => `<tr><td>${x.time || ""}</td><td>${badge(x.action)}</td><td>${fmt(x.price, 2)}</td><td>${x.score}</td><td>${x.reason}</td></tr>`)
.join("") || '<tr><td colspan="5" class="hint">no data</td></tr>';
}
async function runPaper() {
const btn = $("runBtn");
btn.disabled = true;
$("statusMsg").textContent = "running…";
try {
const r = await fetch("/api/paper", { method: "POST" });
if (!r.ok) throw new Error("HTTP " + r.status);
const d = await r.json();
$("statusMsg").innerHTML = `done: return <b class="${d.summary.total_return_pct >= 0 ? "pos" : "neg"}">${d.summary.total_return_pct.toFixed(2)} %</b> (${d.summary.num_trades} trades), data: ${d.broker}`;
$("chartHint").style.display = "none";
drawEquity(d.equity_curve || []);
loadStatus(); loadSignals();
} catch (e) {
$("statusMsg").textContent = "error: " + e.message;
} finally {
btn.disabled = false;
}
}
function drawEquity(curve) {
const c = $("chart");
const ctx = c.getContext("2d");
const W = c.width, H = c.height, P = 14;
ctx.clearRect(0, 0, W, H);
if (!curve || curve.length < 2) return;
const min = Math.min(...curve), max = Math.max(...curve);
const span = (max - min) || 1;
const x = (i) => P + (i / (curve.length - 1)) * (W - 2 * P);
const y = (v) => H - P - ((v - min) / span) * (H - 2 * P);
// grid
ctx.strokeStyle = "#232b36"; ctx.lineWidth = 1;
for (let g = 0; g <= 4; g++) {
const gy = P + (g / 4) * (H - 2 * P);
ctx.beginPath(); ctx.moveTo(P, gy); ctx.lineTo(W - P, gy); ctx.stroke();
}
// line
ctx.strokeStyle = curve[curve.length - 1] >= curve[0] ? "#2ecc71" : "#e74c3c";
ctx.lineWidth = 2; ctx.beginPath();
curve.forEach((v, i) => (i ? ctx.lineTo(x(i), y(v)) : ctx.moveTo(x(i), y(v))));
ctx.stroke();
// labels
ctx.fillStyle = "#8b97a7"; ctx.font = "12px sans-serif";
ctx.fillText(fmt(max), P, P - 3);
ctx.fillText(fmt(min, 0), P, H - 4);
}
async function loadAll() {
try {
await Promise.all([loadStatus(), loadSignals()]);
} catch (e) {
$("statusMsg").textContent = "api error: " + e.message;
}
}
loadAll();
setInterval(loadSignals, 60_000);
</script>
</body>
</html>