Add REST API with dashboard and configurable port (server.host/port, TM_PORT, --port)
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user