feat: Dashboard mit Tabs (Trading/GridBot) und Preis-Charts mit Trade-Markern

- Candlestick-Charts (4h) mit Buy/Sell-Markern und Hover-Tooltips (Trade-Details, OHLC)
- Trend-Chart mit Pair-Auswahl (BTC/ETH/SOL/XRP), offene Positionen als Kreis-Marker
- Grid-Chart (XRP) mit Grid-Level-Linien (Center + L1-L8) und Lot-Markern
- GridBot-Tab mit eigener Equity-Kurve (Daten gab es schon, waren ungenutzt)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 07:41:12 +00:00
parent 021049b259
commit 2bd566ce5e

View File

@@ -12,18 +12,33 @@
} }
* { box-sizing: border-box; margin: 0; } * { 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; } 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; } header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
h1 { font-size: 20px; font-weight: 600; } h1 { font-size: 20px; font-weight: 600; }
h1 small { color: var(--muted); font-weight: 400; font-size: 13px; } h1 small { color: var(--muted); font-weight: 400; font-size: 13px; }
#status { margin-left: auto; font-size: 12px; color: var(--muted); } #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; } #status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; }
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 20px; }
.tab { background: none; border: none; border-bottom: 2px solid transparent; color: var(--muted); font: 600 14px system-ui; padding: 8px 16px; cursor: pointer; }
.tab:hover { color: var(--text); }
.tab.active { color: var(--text); border-bottom-color: var(--accent); }
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 20px; } .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 { 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 .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; } .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 { 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); } .panel h2 { font-size: 14px; font-weight: 600; margin-bottom: 10px; color: var(--muted); }
.panel-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.panel-head h2 { margin-bottom: 0; }
.pair-btns { margin-left: auto; display: flex; gap: 4px; }
.pair-btns button { background: var(--bg); border: 1px solid var(--border); color: var(--muted); font: 600 11px var(--mono); padding: 3px 9px; border-radius: 6px; cursor: pointer; }
.pair-btns button.active { color: var(--text); border-color: var(--accent); }
.legend { font-size: 11px; color: var(--muted); }
.legend .b { color: var(--green); } .legend .s { color: var(--red); }
canvas { width: 100%; height: 220px; display: block; } canvas { width: 100%; height: 220px; display: block; }
canvas.price { height: 280px; cursor: crosshair; }
.chart-wrap { position: relative; }
.tooltip { position: absolute; pointer-events: none; background: #1c2128; border: 1px solid var(--border); border-radius: 6px; padding: 7px 10px; font: 12px/1.6 var(--mono); white-space: nowrap; z-index: 10; display: none; box-shadow: 0 4px 12px rgba(0,0,0,.5); }
.tooltip .t-title { font-weight: 700; margin-bottom: 2px; }
table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12.5px; } 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); } 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; } td { text-align: right; padding: 6px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; }
@@ -38,33 +53,65 @@
</head> </head>
<body> <body>
<header> <header>
<h1>trade-kuns <small>Donchian-Trendfolge · Paper · BTC ETH SOL XRP</small></h1> <h1>trade-kuns <small>Paper · Trend (BTC ETH SOL XRP) + Grid (XRP)</small></h1>
<div id="status"><span class="dot" style="background:var(--muted)"></span>lade…</div> <div id="status"><span class="dot" style="background:var(--muted)"></span>lade…</div>
</header> </header>
<div class="kpis" id="kpis"></div> <nav class="tabs">
<button class="tab active" data-tab="trading">Trading</button>
<button class="tab" data-tab="grid">GridBot</button>
</nav>
<div class="panel"><h2>Equity-Kurve (4h)</h2><canvas id="chart" height="220"></canvas></div> <section id="tab-trading">
<div class="kpis" id="kpis"></div>
<div class="panel"><h2>Offene Positionen</h2><div id="positions"></div></div> <div class="panel">
<div class="panel-head">
<h2>Preis-Chart (4h) <span class="legend"><span class="b">Buy</span> · ▼ <span class="s">Sell</span> · ○ offen</span></h2>
<div class="pair-btns" id="pair-btns"></div>
</div>
<div class="chart-wrap">
<canvas id="price-trend" class="price" height="280"></canvas>
<div class="tooltip" id="tip-trend"></div>
</div>
</div>
<div class="panel"><h2>Abgeschlossene Trades</h2><div id="trades"></div></div> <div class="panel"><h2>Equity-Kurve (4h)</h2><canvas id="chart" height="220"></canvas></div>
<div class="panel"><h2>Letzte Entscheidungen (4h-Bars)</h2><div id="decisions"></div></div> <div class="panel"><h2>Offene Positionen</h2><div id="positions"></div></div>
<div class="panel"> <div class="panel"><h2>Abgeschlossene Trades</h2><div id="trades"></div></div>
<h2>GridBot XRP <span class="tag">No-Stop · 3×ATR · 8 Levels · Paper</span></h2>
<div class="kpis" id="grid-kpis" style="margin-bottom:12px"></div> <div class="panel"><h2>Letzte Entscheidungen (4h-Bars)</h2><div id="decisions"></div></div>
<div id="grid-info" style="color:var(--muted);font-size:12.5px;margin-bottom:10px"></div> </section>
<h2>Offene Lots</h2><div id="grid-lots"></div>
<h2 style="margin-top:14px">Grid-Trades</h2><div id="grid-trades"></div> <section id="tab-grid" hidden>
</div> <div class="kpis" id="grid-kpis"></div>
<div class="panel" style="padding:10px 16px"><div id="grid-info" style="color:var(--muted);font-size:12.5px"></div></div>
<div class="panel">
<div class="panel-head">
<h2>XRP-Chart (4h) <span class="legend"><span class="b">Buy</span> · ▼ <span class="s">Sell</span> · ○ offen · ┄ Grid-Levels</span></h2>
</div>
<div class="chart-wrap">
<canvas id="price-grid" class="price" height="280"></canvas>
<div class="tooltip" id="tip-grid"></div>
</div>
</div>
<div class="panel"><h2>Equity-Kurve (4h)</h2><canvas id="grid-chart" height="220"></canvas></div>
<div class="panel"><h2>Offene Lots</h2><div id="grid-lots"></div></div>
<div class="panel"><h2>Grid-Trades</h2><div id="grid-trades"></div></div>
</section>
<script> <script>
const fmt = (n, d = 2) => n == null || Number.isNaN(n) ? '' : n.toLocaleString('de-DE', { minimumFractionDigits: d, maximumFractionDigits: d }); 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 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 cls = (n) => n > 0 ? 'pos' : n < 0 ? 'neg' : '';
const sign = (n, d = 2) => (n > 0 ? '+' : '') + fmt(n, d); const sign = (n, d = 2) => (n > 0 ? '+' : '') + fmt(n, d);
const priceDec = (p) => p < 1 ? 5 : p < 10 ? 4 : p < 1000 ? 2 : 0;
function kpi(label, value, klass = '') { function kpi(label, value, klass = '') {
return `<div class="kpi"><div class="label">${label}</div><div class="value ${klass}">${value}</div></div>`; return `<div class="kpi"><div class="label">${label}</div><div class="value ${klass}">${value}</div></div>`;
@@ -75,56 +122,295 @@ function table(headers, rows, emptyMsg) {
return `<table><thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead><tbody>${rows.join('')}</tbody></table>`; return `<table><thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead><tbody>${rows.join('')}</tbody></table>`;
} }
function drawChart(curve, startCapital) { // ── Equity-Kurve (Linie) ────────────────────────────────────────────────
const cv = document.getElementById('chart'); function makeEquityChart(canvasId) {
const dpr = window.devicePixelRatio || 1; const cv = document.getElementById(canvasId);
const w = cv.clientWidth, h = 220; let data = null;
cv.width = w * dpr; cv.height = h * dpr; function draw() {
const ctx = cv.getContext('2d'); if (!data) return;
ctx.scale(dpr, dpr); const { curve, startCapital } = data;
ctx.clearRect(0, 0, w, h); const dpr = window.devicePixelRatio || 1;
if (curve.length < 2) { const w = cv.clientWidth, h = 220;
ctx.fillStyle = '#8b949e'; ctx.font = '13px system-ui'; if (!w) return; // Tab versteckt
ctx.fillText('Noch keine Equity-Historie — der erste 4h-Bucket kommt.', 10, h / 2); cv.width = w * dpr; cv.height = h * dpr;
return; 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.lineWidth = 1;
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);
} }
const pad = { l: 54, r: 8, t: 10, b: 22 }; return {
const xs = curve.map(p => p.ts), ys = curve.map(p => p.equity); render(curve, startCapital) { data = { curve, startCapital }; draw(); },
const xmin = xs[0], xmax = xs[xs.length - 1]; redraw: draw,
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); // ── Preis-Chart (Candles + Buy/Sell-Marker + Tooltip) ───────────────────
ctx.strokeStyle = '#21262d'; ctx.fillStyle = '#8b949e'; ctx.font = '11px system-ui'; // markers: [{ ts, price, kind: 'buy'|'sell'|'open', title, lines: [string] }]
for (let i = 0; i <= 4; i++) { // levels: [{ price, label, emph }] — horizontale gestrichelte Linien (Grid)
const v = ymin + (ymax - ymin) * i / 4, y = Y(v); function makePriceChart(canvasId, tipId) {
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(w - pad.r, y); ctx.stroke(); const cv = document.getElementById(canvasId);
ctx.fillText(fmt(v, 0), 6, y + 4); const tip = document.getElementById(tipId);
const H = 280;
let data = null; // { candles, markers, levels }
let hits = []; // [{ x, y, m }] Marker-Hitboxen in CSS-px
let geo = null; // { pad, w, xmin, xmax, ymin, ymax, barW }
function draw() {
if (!data) return;
const { candles, markers, levels } = data;
const dpr = window.devicePixelRatio || 1;
const w = cv.clientWidth;
if (!w) return; // Tab versteckt
cv.width = w * dpr; cv.height = H * dpr;
const ctx = cv.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, w, H);
hits = [];
if (candles.length < 2) {
ctx.fillStyle = '#8b949e'; ctx.font = '13px system-ui';
ctx.fillText('Noch keine Candles geladen.', 10, H / 2);
return;
}
const pad = { l: 56, r: 10, t: 12, b: 22 };
const xmin = candles[0].ts, xmax = candles[candles.length - 1].ts;
let ymin = Infinity, ymax = -Infinity;
for (const c of candles) { if (c.low < ymin) ymin = c.low; if (c.high > ymax) ymax = c.high; }
for (const m of markers) if (m.ts >= xmin) { if (m.price < ymin) ymin = m.price; if (m.price > ymax) ymax = m.price; }
const yspan = (ymax - ymin) || 1; ymin -= yspan * .07; ymax += yspan * .10;
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);
const barW = Math.max(1, Math.min(9, (w - pad.l - pad.r) / candles.length * 0.7));
geo = { pad, w, xmin, xmax, ymin, ymax, X, Y };
const dec = priceDec(ymax);
// Gitter + Y-Achse
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, dec), 4, y + 4);
}
// Grid-Levels
for (const lv of levels || []) {
if (lv.price < ymin || lv.price > ymax) continue;
const y = Y(lv.price);
ctx.strokeStyle = lv.emph ? '#58a6ff' : '#3d444d';
ctx.setLineDash([3, 4]);
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(w - pad.r, y); ctx.stroke();
ctx.setLineDash([]);
if (lv.label) {
ctx.fillStyle = lv.emph ? '#58a6ff' : '#6e7681';
ctx.fillText(lv.label, w - pad.r - ctx.measureText(lv.label).width - 2, y - 3);
}
}
// Candles
for (const c of candles) {
const x = X(c.ts), up = c.close >= c.open;
ctx.strokeStyle = ctx.fillStyle = up ? '#3fb950' : '#f85149';
ctx.beginPath(); ctx.moveTo(x, Y(c.high)); ctx.lineTo(x, Y(c.low)); ctx.stroke();
const yo = Y(c.open), yc = Y(c.close);
ctx.fillRect(x - barW / 2, Math.min(yo, yc), barW, Math.max(1, Math.abs(yc - yo)));
}
// Marker
for (const m of markers) {
if (m.ts < xmin || m.ts > xmax) continue;
const x = X(m.ts), y = Y(m.price);
const buyish = m.kind !== 'sell';
ctx.fillStyle = ctx.strokeStyle = buyish ? '#3fb950' : '#f85149';
const my = buyish ? y + 11 : y - 11; // Buy unter, Sell über dem Preis
if (m.kind === 'open') {
ctx.lineWidth = 1.8;
ctx.beginPath(); ctx.arc(x, my, 4.5, 0, Math.PI * 2); ctx.stroke();
ctx.lineWidth = 1;
} else {
ctx.beginPath();
if (buyish) { ctx.moveTo(x, my - 5); ctx.lineTo(x - 5, my + 4); ctx.lineTo(x + 5, my + 4); }
else { ctx.moveTo(x, my + 5); ctx.lineTo(x - 5, my - 4); ctx.lineTo(x + 5, my - 4); }
ctx.closePath(); ctx.fill();
}
hits.push({ x, y: my, m });
}
// X-Achse
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);
} }
ctx.strokeStyle = '#8b949e'; ctx.setLineDash([4, 4]);
ctx.beginPath(); ctx.moveTo(pad.l, Y(startCapital)); ctx.lineTo(w - pad.r, Y(startCapital)); ctx.stroke(); function showTip(px, py, title, lines, titleClass) {
ctx.setLineDash([]); tip.innerHTML = `<div class="t-title ${titleClass || ''}">${title}</div>` + lines.join('<br>');
const last = ys[ys.length - 1]; tip.style.display = 'block';
ctx.strokeStyle = last >= startCapital ? '#3fb950' : '#f85149'; ctx.lineWidth = 1.8; const wrapW = cv.clientWidth;
ctx.beginPath(); const tw = tip.offsetWidth, th = tip.offsetHeight;
curve.forEach((p, i) => i ? ctx.lineTo(X(p.ts), Y(p.equity)) : ctx.moveTo(X(p.ts), Y(p.equity))); let left = px + 14, top = py - th - 8;
ctx.stroke(); if (left + tw > wrapW) left = px - tw - 14;
ctx.fillStyle = '#8b949e'; if (top < 0) top = py + 14;
ctx.fillText(fmtTs(xmin), pad.l, h - 6); tip.style.left = left + 'px'; tip.style.top = top + 'px';
const endLabel = fmtTs(xmax); }
ctx.fillText(endLabel, w - pad.r - ctx.measureText(endLabel).width, h - 6);
cv.addEventListener('mousemove', (ev) => {
if (!data || !geo) return;
const r = cv.getBoundingClientRect();
const px = ev.clientX - r.left, py = ev.clientY - r.top;
// 1) Marker in der Nähe?
let best = null, bestD = 144; // 12px Radius²
for (const h of hits) {
const d = (h.x - px) ** 2 + (h.y - py) ** 2;
if (d < bestD) { bestD = d; best = h; }
}
if (best) {
const m = best.m;
showTip(px, py, m.title, m.lines, m.kind === 'sell' ? 'neg' : 'pos');
return;
}
// 2) sonst: Candle-OHLC unter dem Cursor
const { candles } = data;
const span = geo.xmax - geo.xmin;
const ts = geo.xmin + (px - geo.pad.l) / (geo.w - geo.pad.l - geo.pad.r) * span;
let ci = -1, cd = Infinity;
for (let i = 0; i < candles.length; i++) {
const d = Math.abs(candles[i].ts - ts);
if (d < cd) { cd = d; ci = i; }
}
if (ci < 0 || px < geo.pad.l || px > geo.w - geo.pad.r) { tip.style.display = 'none'; return; }
const c = candles[ci], dec = priceDec(c.high);
showTip(px, py, fmtTs(c.ts), [
`O ${fmt(c.open, dec)} · H ${fmt(c.high, dec)}`,
`L ${fmt(c.low, dec)} · C ${fmt(c.close, dec)}`,
`Vol ${fmt(c.volume, 0)}`,
]);
});
cv.addEventListener('mouseleave', () => { tip.style.display = 'none'; });
return {
render(candles, markers, levels) { data = { candles, markers: markers || [], levels: levels || [] }; draw(); },
redraw: draw,
};
}
// ── Marker-Erzeugung ────────────────────────────────────────────────────
function tradeMarkers(trades, { withR = false, label = '' } = {}) {
const ms = [];
for (const t of trades) {
const entryTs = new Date(t.entryTs).getTime(), exitTs = new Date(t.exitTs).getTime();
const dec = priceDec(t.entryPrice);
ms.push({
ts: entryTs, price: t.entryPrice, kind: 'buy', title: `▲ BUY ${label || t.pair}`,
lines: [fmtTs(entryTs), `Preis ${fmt(t.entryPrice, dec)} $`, `Menge ${fmt(t.qty, 4)}`],
});
ms.push({
ts: exitTs, price: t.exitPrice, kind: 'sell', title: `▼ SELL ${label || t.pair}`,
lines: [
fmtTs(exitTs), `Preis ${fmt(t.exitPrice, dec)} $`,
`PnL <span class="${cls(t.pnl)}">${sign(t.pnl)} $</span>` + (withR ? ` · R <span class="${cls(t.r)}">${sign(t.r, 2)}</span>` : ''),
`Grund: ${t.exitReason}`, `Entry ${fmtTs(entryTs)} @ ${fmt(t.entryPrice, dec)}`,
],
});
}
return ms;
}
// ── Tabs ────────────────────────────────────────────────────────────────
const charts = {
trading: [], // wird unten befüllt
grid: [],
};
function showTab(name) {
document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab === name));
document.getElementById('tab-trading').hidden = name !== 'trading';
document.getElementById('tab-grid').hidden = name !== 'grid';
history.replaceState(null, '', '#' + name);
requestAnimationFrame(() => (charts[name] || []).forEach(c => c.redraw()));
}
document.querySelectorAll('.tab').forEach(b => b.addEventListener('click', () => showTab(b.dataset.tab)));
// ── Pair-Auswahl Trend-Chart ────────────────────────────────────────────
const PAIRS = ['BTC_USDT', 'ETH_USDT', 'SOL_USDT', 'XRP_USDT'];
let selPair = 'BTC_USDT';
document.getElementById('pair-btns').innerHTML =
PAIRS.map(p => `<button data-pair="${p}" class="${p === selPair ? 'active' : ''}">${p.replace('_USDT', '')}</button>`).join('');
document.querySelectorAll('#pair-btns button').forEach(b => b.addEventListener('click', () => {
selPair = b.dataset.pair;
document.querySelectorAll('#pair-btns button').forEach(x => x.classList.toggle('active', x.dataset.pair === selPair));
renderTrendPrice();
}));
const equityChart = makeEquityChart('chart');
const gridEquityChart = makeEquityChart('grid-chart');
const trendPriceChart = makePriceChart('price-trend', 'tip-trend');
const gridPriceChart = makePriceChart('price-grid', 'tip-grid');
charts.trading = [equityChart, trendPriceChart];
charts.grid = [gridEquityChart, gridPriceChart];
let lastTrades = [], lastPositions = [];
async function renderTrendPrice() {
try {
const candles = await fetch(`/api/candles?pair=${selPair}&tf=4h&limit=300`).then(r => r.json());
const markers = tradeMarkers(lastTrades.filter(t => t.pair === selPair), { withR: true });
for (const p of lastPositions.filter(p => p.pair === selPair)) {
const dec = priceDec(p.entryPrice);
markers.push({
ts: p.entryTs, price: p.entryPrice, kind: 'open', title: `○ OFFEN ${p.pair}`,
lines: [
`Entry ${fmtTs(p.entryTs)} @ ${fmt(p.entryPrice, dec)}`, `Menge ${fmt(p.qty, 4)}`,
`Stop ${fmt(p.stop, dec)}`,
`PnL <span class="${cls(p.unrealizedPnl)}">${sign(p.unrealizedPnl)} $</span>`,
],
});
}
trendPriceChart.render(candles, markers);
} catch { /* nächster Refresh versucht's wieder */ }
} }
async function refresh() { async function refresh() {
try { try {
const [pf, stats, trades, decisions, grid] = await Promise.all([ const [pf, stats, trades, decisions, grid, gridTradesAll] = await Promise.all([
fetch('/api/portfolio').then(r => r.json()), fetch('/api/portfolio').then(r => r.json()),
fetch('/api/stats').then(r => r.json()), fetch('/api/stats').then(r => r.json()),
fetch('/api/trades?limit=30').then(r => r.json()), fetch('/api/trades?limit=200').then(r => r.json()),
fetch('/api/decisions?limit=12').then(r => r.json()), fetch('/api/decisions?limit=12').then(r => r.json()),
fetch('/api/grid').then(r => r.json()), fetch('/api/grid').then(r => r.json()),
fetch('/api/trades?bot=grid&limit=200').then(r => r.json()),
]); ]);
lastTrades = trades;
lastPositions = pf.positions;
// ── Trading-Tab ──
const pnl = pf.equity - pf.startCapital; const pnl = pf.equity - pf.startCapital;
const pnlPct = pf.startCapital ? pnl / pf.startCapital * 100 : 0; const pnlPct = pf.startCapital ? pnl / pf.startCapital * 100 : 0;
document.getElementById('kpis').innerHTML = document.getElementById('kpis').innerHTML =
@@ -137,7 +423,8 @@ async function refresh() {
kpi('Max DD', fmt(stats.maxDrawdownPct * 100, 1) + ' %') + kpi('Max DD', fmt(stats.maxDrawdownPct * 100, 1) + ' %') +
kpi('BTC Buy&Hold', stats.btcBuyHoldPct == null ? '' : sign(stats.btcBuyHoldPct, 1) + ' %', cls(stats.btcBuyHoldPct)); kpi('BTC Buy&Hold', stats.btcBuyHoldPct == null ? '' : sign(stats.btcBuyHoldPct, 1) + ' %', cls(stats.btcBuyHoldPct));
drawChart(stats.equityCurve || [], stats.startCapital); equityChart.render(stats.equityCurve || [], stats.startCapital);
renderTrendPrice();
document.getElementById('positions').innerHTML = table( document.getElementById('positions').innerHTML = table(
['Pair', 'Entry', 'Entry-Preis', 'Letzter', 'Stop', 'Wert $', 'PnL $'], ['Pair', 'Entry', 'Entry-Preis', 'Letzter', 'Stop', 'Wert $', 'PnL $'],
@@ -146,7 +433,7 @@ async function refresh() {
document.getElementById('trades').innerHTML = table( document.getElementById('trades').innerHTML = table(
['Pair', 'Entry', 'Exit', 'Entry-Preis', 'Exit-Preis', 'PnL $', 'R', 'Grund'], ['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>`), trades.slice(0, 30).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.'); 'Noch keine abgeschlossenen Trades.');
document.getElementById('decisions').innerHTML = table( document.getElementById('decisions').innerHTML = table(
@@ -159,6 +446,7 @@ async function refresh() {
}), }),
'Noch keine Entscheidungen — die erste 4h-Bar schließt demnächst.'); 'Noch keine Entscheidungen — die erste 4h-Bar schließt demnächst.');
// ── GridBot-Tab ──
const gPnl = grid.equity - grid.startCapital; const gPnl = grid.equity - grid.startCapital;
document.getElementById('grid-kpis').innerHTML = document.getElementById('grid-kpis').innerHTML =
kpi('Equity', fmt(grid.equity) + ' $') + kpi('Equity', fmt(grid.equity) + ' $') +
@@ -172,6 +460,30 @@ async function refresh() {
? `Aktives Grid: Center ${fmt(gs.center, 4)} · Spacing ${fmt(gs.spacing, 4)} · Range ${fmt(gs.lowerBound, 4)}${fmt(gs.upperBound, 4)} · Budget/Level ${fmt(gs.budgetPerLevel)} $ · aktiviert ${fmtTs(gs.activatedTs)} · XRP jetzt ${fmt(gs.lastPrice, 4)}` ? `Aktives Grid: Center ${fmt(gs.center, 4)} · Spacing ${fmt(gs.spacing, 4)} · Range ${fmt(gs.lowerBound, 4)}${fmt(gs.upperBound, 4)} · Budget/Level ${fmt(gs.budgetPerLevel)} $ · aktiviert ${fmtTs(gs.activatedTs)} · XRP jetzt ${fmt(gs.lastPrice, 4)}`
: 'Kein aktives Grid — Aktivierung beim nächsten 4h-Close.'; : 'Kein aktives Grid — Aktivierung beim nächsten 4h-Close.';
// XRP-Chart mit Grid-Levels und Markern
const xrpCandles = await fetch('/api/candles?pair=XRP_USDT&tf=4h&limit=300').then(r => r.json());
const gMarkers = tradeMarkers(gridTradesAll, { label: 'XRP' });
for (const l of grid.lots) {
const dec = priceDec(l.entryPrice);
gMarkers.push({
ts: l.entryTs, price: l.entryPrice, kind: 'open', title: `○ LOT L${l.levelIdx + 1}`,
lines: [
`Entry ${fmtTs(l.entryTs)} @ ${fmt(l.entryPrice, dec)}`, `Menge ${fmt(l.qty, 4)}`,
`PnL <span class="${cls(l.unrealizedPnl)}">${sign(l.unrealizedPnl)} $</span>`,
],
});
}
const levels = [];
if (gs) {
levels.push({ price: gs.center, label: 'Center', emph: true });
for (let k = 0; k < (grid.config.gridLevels ?? 8); k++) {
levels.push({ price: gs.center - (k + 1) * gs.spacing, label: `L${k + 1}` });
}
}
gridPriceChart.render(xrpCandles, gMarkers, levels);
gridEquityChart.render(grid.equityCurve || [], grid.startCapital);
document.getElementById('grid-lots').innerHTML = table( document.getElementById('grid-lots').innerHTML = table(
['Level', 'Entry', 'Entry-Preis', 'Letzter', 'Wert $', 'PnL $'], ['Level', 'Entry', 'Entry-Preis', 'Letzter', 'Wert $', 'PnL $'],
grid.lots.map(l => `<tr><td>L${l.levelIdx + 1}</td><td>${fmtTs(l.entryTs)}</td><td>${fmt(l.entryPrice, 4)}</td><td>${fmt(l.lastPrice, 4)}</td><td>${fmt(l.value)}</td><td class="${cls(l.unrealizedPnl)}">${sign(l.unrealizedPnl)}</td></tr>`), grid.lots.map(l => `<tr><td>L${l.levelIdx + 1}</td><td>${fmtTs(l.entryTs)}</td><td>${fmt(l.entryPrice, 4)}</td><td>${fmt(l.lastPrice, 4)}</td><td>${fmt(l.value)}</td><td class="${cls(l.unrealizedPnl)}">${sign(l.unrealizedPnl)}</td></tr>`),
@@ -191,8 +503,15 @@ async function refresh() {
document.getElementById('status').innerHTML = `<span class="dot" style="background:var(--red)"></span>API nicht erreichbar`; document.getElementById('status').innerHTML = `<span class="dot" style="background:var(--red)"></span>API nicht erreichbar`;
} }
} }
if (location.hash === '#grid') showTab('grid');
refresh(); refresh();
setInterval(refresh, 30000); setInterval(refresh, 30000);
let resizeT;
window.addEventListener('resize', () => {
clearTimeout(resizeT);
resizeT = setTimeout(() => Object.values(charts).flat().forEach(c => c.redraw()), 150);
});
</script> </script>
</body> </body>
</html> </html>