- 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>
518 lines
25 KiB
HTML
518 lines
25 KiB
HTML
<!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: 16px; 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; }
|
||
.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; }
|
||
.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); }
|
||
.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.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; }
|
||
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>Paper · Trend (BTC ETH SOL XRP) + Grid (XRP)</small></h1>
|
||
<div id="status"><span class="dot" style="background:var(--muted)"></span>lade…</div>
|
||
</header>
|
||
|
||
<nav class="tabs">
|
||
<button class="tab active" data-tab="trading">Trading</button>
|
||
<button class="tab" data-tab="grid">GridBot</button>
|
||
</nav>
|
||
|
||
<section id="tab-trading">
|
||
<div class="kpis" id="kpis"></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>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>
|
||
</section>
|
||
|
||
<section id="tab-grid" hidden>
|
||
<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>
|
||
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);
|
||
const priceDec = (p) => p < 1 ? 5 : p < 10 ? 4 : p < 1000 ? 2 : 0;
|
||
|
||
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>`;
|
||
}
|
||
|
||
// ── Equity-Kurve (Linie) ────────────────────────────────────────────────
|
||
function makeEquityChart(canvasId) {
|
||
const cv = document.getElementById(canvasId);
|
||
let data = null;
|
||
function draw() {
|
||
if (!data) return;
|
||
const { curve, startCapital } = data;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const w = cv.clientWidth, h = 220;
|
||
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);
|
||
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);
|
||
}
|
||
return {
|
||
render(curve, startCapital) { data = { curve, startCapital }; draw(); },
|
||
redraw: draw,
|
||
};
|
||
}
|
||
|
||
// ── Preis-Chart (Candles + Buy/Sell-Marker + Tooltip) ───────────────────
|
||
// markers: [{ ts, price, kind: 'buy'|'sell'|'open', title, lines: [string] }]
|
||
// levels: [{ price, label, emph }] — horizontale gestrichelte Linien (Grid)
|
||
function makePriceChart(canvasId, tipId) {
|
||
const cv = document.getElementById(canvasId);
|
||
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);
|
||
}
|
||
|
||
function showTip(px, py, title, lines, titleClass) {
|
||
tip.innerHTML = `<div class="t-title ${titleClass || ''}">${title}</div>` + lines.join('<br>');
|
||
tip.style.display = 'block';
|
||
const wrapW = cv.clientWidth;
|
||
const tw = tip.offsetWidth, th = tip.offsetHeight;
|
||
let left = px + 14, top = py - th - 8;
|
||
if (left + tw > wrapW) left = px - tw - 14;
|
||
if (top < 0) top = py + 14;
|
||
tip.style.left = left + 'px'; tip.style.top = top + 'px';
|
||
}
|
||
|
||
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() {
|
||
try {
|
||
const [pf, stats, trades, decisions, grid, gridTradesAll] = await Promise.all([
|
||
fetch('/api/portfolio').then(r => r.json()),
|
||
fetch('/api/stats').then(r => r.json()),
|
||
fetch('/api/trades?limit=200').then(r => r.json()),
|
||
fetch('/api/decisions?limit=12').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 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));
|
||
|
||
equityChart.render(stats.equityCurve || [], stats.startCapital);
|
||
renderTrendPrice();
|
||
|
||
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.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.');
|
||
|
||
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.');
|
||
|
||
// ── GridBot-Tab ──
|
||
const gPnl = grid.equity - grid.startCapital;
|
||
document.getElementById('grid-kpis').innerHTML =
|
||
kpi('Equity', fmt(grid.equity) + ' $') +
|
||
kpi('PnL', sign(gPnl) + ' $', cls(gPnl)) +
|
||
kpi('Cash', fmt(grid.cash) + ' $') +
|
||
kpi('Trades', grid.stats.trades) +
|
||
kpi('Win Rate', grid.stats.trades ? fmt(grid.stats.winRate * 100, 0) + ' %' : '–');
|
||
|
||
const gs = grid.grids[0];
|
||
document.getElementById('grid-info').textContent = gs
|
||
? `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.';
|
||
|
||
// 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(
|
||
['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>`),
|
||
'Keine offenen Lots — warten auf den ersten Dip unter ein Level.');
|
||
|
||
document.getElementById('grid-trades').innerHTML = table(
|
||
['Entry', 'Exit', 'Entry-Preis', 'Exit-Preis', 'PnL $', 'Grund'],
|
||
grid.trades.map(t => `<tr><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>${t.exitReason}</td></tr>`),
|
||
'Noch keine Grid-Trades.');
|
||
|
||
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`;
|
||
}
|
||
}
|
||
|
||
if (location.hash === '#grid') showTab('grid');
|
||
refresh();
|
||
setInterval(refresh, 30000);
|
||
let resizeT;
|
||
window.addEventListener('resize', () => {
|
||
clearTimeout(resizeT);
|
||
resizeT = setTimeout(() => Object.values(charts).flat().forEach(c => c.redraw()), 150);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|