Files

185 lines
7.5 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>