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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user