185 lines
7.5 KiB
HTML
185 lines
7.5 KiB
HTML
<!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>
|