Files
trade-kuns/public/index.html
Claude 29846e82a7 feat: Live-Paper-Engine — 5-min-Loop, API, Dashboard, Dockerfile
processCycle spiegelt Runner-Semantik exakt (Paritätstest gegen runBacktest),
Restart-Recovery über Cursor, DecisionLog mit Outcome-Backfill,
Bun.serve-API + statisches Dashboard, Deploy-Ziel trading.kuns.dev.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 06:11:44 +00:00

167 lines
9.1 KiB
HTML
Raw 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="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>trade-kuns — Paper Trading</title>
<style>
:root {
--bg: #0d1117; --panel: #161b22; --border: #21262d; --text: #e6edf3;
--muted: #8b949e; --green: #3fb950; --red: #f85149; --accent: #58a6ff;
--mono: ui-monospace, 'JetBrains Mono', 'Fira Code', monospace;
}
* { box-sizing: border-box; margin: 0; }
body { background: var(--bg); color: var(--text); font: 14px/1.5 system-ui, sans-serif; padding: 24px; max-width: 1200px; margin: 0 auto; }
header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
h1 { font-size: 20px; font-weight: 600; }
h1 small { color: var(--muted); font-weight: 400; font-size: 13px; }
#status { margin-left: auto; font-size: 12px; color: var(--muted); }
#status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; }
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 20px; }
.kpi { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }
.kpi .label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.kpi .value { font-size: 20px; font-weight: 600; font-family: var(--mono); margin-top: 2px; }
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 20px; }
.panel h2 { font-size: 14px; font-weight: 600; margin-bottom: 10px; color: var(--muted); }
canvas { width: 100%; height: 220px; display: block; }
table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12.5px; }
th { text-align: right; color: var(--muted); font-weight: 500; padding: 6px 8px; border-bottom: 1px solid var(--border); }
td { text-align: right; padding: 6px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; }
th:first-child, td:first-child { text-align: left; }
tr:last-child td { border-bottom: none; }
.pos { color: var(--green); } .neg { color: var(--red); }
.empty { color: var(--muted); padding: 12px 8px; font-style: italic; }
.tag { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 11px; background: var(--border); color: var(--muted); }
.tag.long { background: rgba(63,185,80,.15); color: var(--green); }
@media (max-width: 700px) { body { padding: 12px; } td, th { padding: 5px 4px; } }
</style>
</head>
<body>
<header>
<h1>trade-kuns <small>Donchian-Trendfolge · Paper · BTC ETH SOL XRP</small></h1>
<div id="status"><span class="dot" style="background:var(--muted)"></span>lade…</div>
</header>
<div class="kpis" id="kpis"></div>
<div class="panel"><h2>Equity-Kurve (4h)</h2><canvas id="chart" height="220"></canvas></div>
<div class="panel"><h2>Offene Positionen</h2><div id="positions"></div></div>
<div class="panel"><h2>Abgeschlossene Trades</h2><div id="trades"></div></div>
<div class="panel"><h2>Letzte Entscheidungen (4h-Bars)</h2><div id="decisions"></div></div>
<script>
const fmt = (n, d = 2) => n == null || Number.isNaN(n) ? '' : n.toLocaleString('de-DE', { minimumFractionDigits: d, maximumFractionDigits: d });
const fmtTs = (ts) => ts == null ? '' : new Date(ts).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
const cls = (n) => n > 0 ? 'pos' : n < 0 ? 'neg' : '';
const sign = (n, d = 2) => (n > 0 ? '+' : '') + fmt(n, d);
function kpi(label, value, klass = '') {
return `<div class="kpi"><div class="label">${label}</div><div class="value ${klass}">${value}</div></div>`;
}
function table(headers, rows, emptyMsg) {
if (!rows.length) return `<div class="empty">${emptyMsg}</div>`;
return `<table><thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead><tbody>${rows.join('')}</tbody></table>`;
}
function drawChart(curve, startCapital) {
const cv = document.getElementById('chart');
const dpr = window.devicePixelRatio || 1;
const w = cv.clientWidth, h = 220;
cv.width = w * dpr; cv.height = h * dpr;
const ctx = cv.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, w, h);
if (curve.length < 2) {
ctx.fillStyle = '#8b949e'; ctx.font = '13px system-ui';
ctx.fillText('Noch keine Equity-Historie — der erste 4h-Bucket kommt.', 10, h / 2);
return;
}
const pad = { l: 54, r: 8, t: 10, b: 22 };
const xs = curve.map(p => p.ts), ys = curve.map(p => p.equity);
const xmin = xs[0], xmax = xs[xs.length - 1];
let ymin = Math.min(...ys, startCapital), ymax = Math.max(...ys, startCapital);
const yspan = (ymax - ymin) || 1; ymin -= yspan * .08; ymax += yspan * .08;
const X = t => pad.l + (t - xmin) / (xmax - xmin) * (w - pad.l - pad.r);
const Y = v => h - pad.b - (v - ymin) / (ymax - ymin) * (h - pad.t - pad.b);
ctx.strokeStyle = '#21262d'; ctx.fillStyle = '#8b949e'; ctx.font = '11px system-ui';
for (let i = 0; i <= 4; i++) {
const v = ymin + (ymax - ymin) * i / 4, y = Y(v);
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(w - pad.r, y); ctx.stroke();
ctx.fillText(fmt(v, 0), 6, y + 4);
}
ctx.strokeStyle = '#8b949e'; ctx.setLineDash([4, 4]);
ctx.beginPath(); ctx.moveTo(pad.l, Y(startCapital)); ctx.lineTo(w - pad.r, Y(startCapital)); ctx.stroke();
ctx.setLineDash([]);
const last = ys[ys.length - 1];
ctx.strokeStyle = last >= startCapital ? '#3fb950' : '#f85149'; ctx.lineWidth = 1.8;
ctx.beginPath();
curve.forEach((p, i) => i ? ctx.lineTo(X(p.ts), Y(p.equity)) : ctx.moveTo(X(p.ts), Y(p.equity)));
ctx.stroke();
ctx.fillStyle = '#8b949e';
ctx.fillText(fmtTs(xmin), pad.l, h - 6);
const endLabel = fmtTs(xmax);
ctx.fillText(endLabel, w - pad.r - ctx.measureText(endLabel).width, h - 6);
}
async function refresh() {
try {
const [pf, stats, trades, decisions] = await Promise.all([
fetch('/api/portfolio').then(r => r.json()),
fetch('/api/stats').then(r => r.json()),
fetch('/api/trades?limit=30').then(r => r.json()),
fetch('/api/decisions?limit=12').then(r => r.json()),
]);
const pnl = pf.equity - pf.startCapital;
const pnlPct = pf.startCapital ? pnl / pf.startCapital * 100 : 0;
document.getElementById('kpis').innerHTML =
kpi('Equity', fmt(pf.equity) + ' $') +
kpi('PnL gesamt', `${sign(pnl)} $ (${sign(pnlPct, 1)} %)`, cls(pnl)) +
kpi('Cash', fmt(pf.cash) + ' $') +
kpi('Trades', stats.trades) +
kpi('Profit Factor', stats.trades ? fmt(stats.profitFactor) : '') +
kpi('Win Rate', stats.trades ? fmt(stats.winRate * 100, 0) + ' %' : '') +
kpi('Max DD', fmt(stats.maxDrawdownPct * 100, 1) + ' %') +
kpi('BTC Buy&Hold', stats.btcBuyHoldPct == null ? '' : sign(stats.btcBuyHoldPct, 1) + ' %', cls(stats.btcBuyHoldPct));
drawChart(stats.equityCurve || [], stats.startCapital);
document.getElementById('positions').innerHTML = table(
['Pair', 'Entry', 'Entry-Preis', 'Letzter', 'Stop', 'Wert $', 'PnL $'],
pf.positions.map(p => `<tr><td><span class="tag long">${p.pair}</span></td><td>${fmtTs(p.entryTs)}</td><td>${fmt(p.entryPrice, 4)}</td><td>${fmt(p.lastPrice, 4)}</td><td>${fmt(p.stop, 4)}</td><td>${fmt(p.value)}</td><td class="${cls(p.unrealizedPnl)}">${sign(p.unrealizedPnl)}</td></tr>`),
'Keine offene Position — warten auf Donchian-Breakout. Das ist Normalbetrieb.');
document.getElementById('trades').innerHTML = table(
['Pair', 'Entry', 'Exit', 'Entry-Preis', 'Exit-Preis', 'PnL $', 'R', 'Grund'],
trades.map(t => `<tr><td><span class="tag long">${t.pair}</span></td><td>${fmtTs(new Date(t.entryTs).getTime())}</td><td>${fmtTs(new Date(t.exitTs).getTime())}</td><td>${fmt(t.entryPrice, 4)}</td><td>${fmt(t.exitPrice, 4)}</td><td class="${cls(t.pnl)}">${sign(t.pnl)}</td><td class="${cls(t.r)}">${sign(t.r, 2)}</td><td>${t.exitReason}</td></tr>`),
'Noch keine abgeschlossenen Trades.');
document.getElementById('decisions').innerHTML = table(
['Pair', '4h-Bar', 'Close', 'Donchian-High', 'EMA-200', 'ADX', 'Ergebnis'],
decisions.map(d => {
const res = d.signal === 'long' && !d.blockedBy ? '<span class="tag long">ENTRY</span>'
: d.signal === 'long' ? `<span class="tag">long, ${d.blockedBy}</span>`
: `<span class="tag">${d.blockedBy ?? ''}</span>`;
return `<tr><td>${d.pair}</td><td>${fmtTs(new Date(d.barTs).getTime())}</td><td>${fmt(d.close, 4)}</td><td>${fmt(d.donchianHigh, 4)}</td><td>${fmt(d.trendEma, 4)}</td><td>${fmt(d.adx, 1)}</td><td style="text-align:left">${res}</td></tr>`;
}),
'Noch keine Entscheidungen — die erste 4h-Bar schließt demnächst.');
const eng = pf.engine || {};
const ok = eng.lastCycleOk !== false;
document.getElementById('status').innerHTML =
`<span class="dot" style="background:${ok ? 'var(--green)' : 'var(--red)'}"></span>` +
(eng.lastCycleAt ? `Zyklus ${fmtTs(eng.lastCycleAt)}${ok ? '' : ' — FEHLER: ' + (eng.lastError ?? '?')}` : 'erster Zyklus läuft…');
} catch (e) {
document.getElementById('status').innerHTML = `<span class="dot" style="background:var(--red)"></span>API nicht erreichbar`;
}
}
refresh();
setInterval(refresh, 30000);
</script>
</body>
</html>