feat: GridBot als zweite Paper-Engine — No-Stop-XRP-Grid live

processGridCycle (Paritätstest gegen runGridBacktest), GridEngine mit
DB-Recovery (grid_state/grid_lots, bot_state id=2), bot-Spalte in
paper_trades/equity_snapshots, /api/grid, Dashboard-Panel.
Bewusster Paper-Probelauf trotz Gate-Fail (User-Entscheidung 2026-06-10).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 07:29:42 +00:00
parent f754b91acd
commit 021049b259
12 changed files with 1267 additions and 17 deletions

View File

@@ -52,6 +52,14 @@
<div class="panel"><h2>Letzte Entscheidungen (4h-Bars)</h2><div id="decisions"></div></div>
<div class="panel">
<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 id="grid-info" style="color:var(--muted);font-size:12.5px;margin-bottom:10px"></div>
<h2>Offene Lots</h2><div id="grid-lots"></div>
<h2 style="margin-top:14px">Grid-Trades</h2><div id="grid-trades"></div>
</div>
<script>
const fmt = (n, d = 2) => n == null || Number.isNaN(n) ? '' : n.toLocaleString('de-DE', { minimumFractionDigits: d, maximumFractionDigits: d });
const fmtTs = (ts) => ts == null ? '' : new Date(ts).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
@@ -109,11 +117,12 @@ function drawChart(curve, startCapital) {
async function refresh() {
try {
const [pf, stats, trades, decisions] = await Promise.all([
const [pf, stats, trades, decisions, grid] = await Promise.all([
fetch('/api/portfolio').then(r => r.json()),
fetch('/api/stats').then(r => r.json()),
fetch('/api/trades?limit=30').then(r => r.json()),
fetch('/api/decisions?limit=12').then(r => r.json()),
fetch('/api/grid').then(r => r.json()),
]);
const pnl = pf.equity - pf.startCapital;
@@ -150,6 +159,29 @@ async function refresh() {
}),
'Noch keine Entscheidungen — die erste 4h-Bar schließt demnächst.');
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.';
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 =