Files
trade-kuns/public/index.html

573 lines
29 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: 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) + Trump</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>
<button class="tab" data-tab="trump">Trump</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>
<section id="tab-trump" hidden>
<div class="kpis" id="trump-cards"></div>
<div class="panel"><h2>Offene Positionen</h2><div id="trump-positions"></div></div>
<div class="panel"><h2>Events</h2><div id="trump-events"></div></div>
<div class="panel"><h2>Trades</h2><div id="trump-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;
// HTML-Escaping für DB-Felder mit externem Ursprung (Truth-Feed/On-chain)
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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: [],
trump: [],
};
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';
document.getElementById('tab-trump').hidden = name !== 'trump';
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, trump, trumpTradesAll] = 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()),
fetch('/api/trump').then(r => r.json()),
fetch('/api/trades?bot=trump&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.');
// ── Trump-Tab ──
// Equity ≈ cash + Σ qty×entryPrice (Näherung; kein Marktpreis verfügbar im API)
const trumpPositions = trump.positions || [];
const trumpEvents = trump.events || [];
const trumpEquityApprox = (trump.cash || 0) + trumpPositions.reduce((s, p) => s + p.qty * p.entryPrice, 0);
const trumpPnl = trumpEquityApprox - (trump.startCapital || 0);
const evCount = trumpEvents.length;
document.getElementById('trump-cards').innerHTML =
kpi('Equity (ca.)', fmt(trumpEquityApprox) + ' $') +
kpi('PnL', sign(trumpPnl) + ' $', cls(trumpPnl)) +
kpi('Cash', fmt(trump.cash) + ' $') +
kpi('Offene Pos.', trumpPositions.length) +
kpi('Events', evCount >= 50 ? '50+' : evCount);
document.getElementById('trump-positions').innerHTML = table(
['Pair', 'Entry-Zeit', 'Entry-Preis', 'Qty', 'Exit fällig'],
trumpPositions.map(p => `<tr><td><span class="tag long">${esc(p.pair)}</span></td><td>${fmtTs(p.entryTs)}</td><td>${fmt(p.entryPrice, priceDec(p.entryPrice))}</td><td>${fmt(p.qty, 4)}</td><td>${fmtTs(p.exitDueTs)}</td></tr>`),
'Keine offenen Positionen.');
document.getElementById('trump-events').innerHTML = table(
['Zeit', 'Quelle', 'Coin', 'Instrument', 'Notional $', 'Ref'],
trumpEvents.map(e => {
// href nur mit https-Schema (Truth-Refs kommen aus externem Feed)
const safeHref = e.source === 'onchain'
? `https://etherscan.io/tx/${esc(e.ref)}`
: /^https:\/\//i.test(e.ref) ? esc(e.ref) : null;
const refLabel = esc(e.source === 'onchain' ? e.ref.slice(0, 12) + '…' : e.ref.slice(0, 24) + (e.ref.length > 24 ? '…' : ''));
const ref = safeHref ? `<a href="${safeHref}" target="_blank" rel="noopener" style="color:var(--accent)">${refLabel}</a>` : refLabel;
return `<tr><td>${fmtTs(e.eventTs)}</td><td>${esc(e.source ?? '')}</td><td>${esc(e.token ?? '')}</td><td>${esc(e.instrument ?? '')}</td><td>${e.notionalUsd != null ? fmt(e.notionalUsd) + ' $' : ''}</td><td style="text-align:left">${ref}</td></tr>`;
}),
'Keine Events vorhanden.');
document.getElementById('trump-trades').innerHTML = table(
['Entry', 'Exit', 'Entry-Preis', 'Exit-Preis', 'PnL $', 'Grund'],
trumpTradesAll.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>${esc(t.exitReason)}</td></tr>`),
'Noch keine Trump-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');
else if (location.hash === '#trump') showTab('trump');
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>