diff --git a/docs/walkforward-grid-2026-06-10.md b/docs/walkforward-grid-2026-06-10.md index 31ca04c..c8a0829 100644 --- a/docs/walkforward-grid-2026-06-10.md +++ b/docs/walkforward-grid-2026-06-10.md @@ -23,6 +23,41 @@ Entscheidungs-Timeframe parametrisiert (`--tf` Minuten; ATR/ADX/Aktivierung auf **Monoton schlechter, je kürzer der Timeframe** — exakt die Fee-Mathematik: ATR(1h) ≈ ⅓, ATR(15m) ≈ ⅙ von ATR(4h) → das Spacing schrumpft auf die Größenordnung der 0.3 % Round-Trip-Kosten, jeder TP verdient fast nichts, die Breakdown-Verluste bleiben gleich groß. tf 15m ist Totalverlust (MaxDD 97 %). Das war auch der Todesmechanismus des krypto-kuns-v1-Bots (1–15-min-Signale). +## Nachtrag 2: XRP-Datenanalyse + No-Stop-Grid (User-Ziel „good working GridBot") + +**XRP-Historie (3 Jahre, 4h):** Nur **1 von 106** rollierenden 30d-Fenstern ist eine enge +Range (<15 % Spanne) — XRP „ranged" im klassischen Sinn praktisch nie. Es oszilliert +aber in **breiten Bändern** über lange Strecken (2023-Q4–2024-Q3: 0.38–0.75; +2025: 1.6–3.7). 25 % der Fenster fallen >10 %, schlimmste 30d: −32 % (Jan 2026). +ATR%(4h) median 1.4–3 %. → Engmaschige Grids mit hartem Stop sterben an den Rändern; +wenn Grid, dann **breit + ohne Verlust-Verkäufe**. + +**No-Stop-Variante** (`--no-stop`: Lots werden nie mit Verlust verkauft, nur TP; +leeres Grid re-centert verlustfrei; `--pair` für Einzel-Pair): + +| Variante | OOS-PF | Trades | WinRate | MaxDD | Gate | +|---|---|---|---|---|---| +| H: no-stop, 2×ATR, 6 Levels, XRP only | 2.00 | 195 | 80.0 % | 31.6 % | ❌ DD + Fenster + Ratio | +| I: no-stop, 2×ATR, 6 Levels, 4 Pairs | 1.33 | 719 | 77.6 % | 31.9 % | ❌ DD + Fenster + Ratio | +| J: no-stop, 3×ATR, 8 Levels, XRP only | **2.74** | 104 | 77.9 % | **17.5 %** | ❌ Fenster + Ratio | + +**Volldurchlauf ohne Fenster-Artefakte** (J, 3 Jahre am Stück): **+49 %** (1000 → 1491), +MaxDD **10.2 %**, PF 4.18, 89 % WinRate — aber nur 27 Trades. Buy&Hold XRP: +130 % +bei −71 % Drawdown. Risk-adjusted schlägt das Grid Buy&Hold deutlich. + +**Ehrliche Einordnung:** +- Erste Grid-Variante mit echtem Edge-Signal — aber Params J wurden nach Sichtung + von H/I gewählt (Selektionsrisiko), und 27 Trades/3J sind statistisch dünn. +- Der Worst-Window-Fail ist teils Artefakt (30d-Fenster zwangsliquidieren Inventar + am Fensterende), teils echt: Crash-Monate erwischen das Inventar voll. +- **Strukturelles Tail-Risiko:** Ein No-Stop-Grid verkauft nie mit Verlust — bei + einem Absturz ohne Erholung (2018-Stil, −95 %) hält es Bags bis zum Ende. Voll + gefüllt liegt ~100 % des Pair-Budgets im Asset. Die 10 % MaxDD der letzten + 3 Jahre unterschätzen dieses Risiko systematisch. + +**Status:** Gate formal nicht bestanden (Fenster + Ratio). Paper-Probelauf als +zweite Engine wäre — wie beim Trend-Bot — eine bewusste User-Entscheidung. + ## Befund Klassische Grid-Pathologie, durch Regime-Filter abgemildert, aber nicht behoben: diff --git a/src/server/backtest/grid.ts b/src/server/backtest/grid.ts index e11e678..3e1adfa 100644 --- a/src/server/backtest/grid.ts +++ b/src/server/backtest/grid.ts @@ -13,6 +13,12 @@ export interface GridParams { adxMax: number; // Grid nur aktiv im Seitwärtsregime (ADX < adxMax) atrPeriod: number; tfMs: number; // Entscheidungs-Timeframe (Aktivierung/Deaktivierung, ATR/ADX-Basis) + /** + * true: Range-Breakdown/Ausbruch/Trendbeginn liquidiert alle Lots (harter Stop). + * false: Lots werden nie mit Verlust verkauft (nur TP oder end_of_data); + * Re-Center nur, wenn das Grid leer ist und der Preis die Range verlassen hat. + */ + hardStop: boolean; } export const DEFAULT_GRID_PARAMS: GridParams = { @@ -21,6 +27,7 @@ export const DEFAULT_GRID_PARAMS: GridParams = { adxMax: 20, atrPeriod: 14, tfMs: H4, + hardStop: true, }; export interface GridConfig { @@ -136,9 +143,12 @@ export function runGridBacktest(candles15ByPair: Map, cfg: GridC const g = grids.get(pair); if (g) { - const trendStart = !Number.isNaN(ctx.adx[i]) && ctx.adx[i] >= p.adxMax + 5; // Hysterese - if (bar.close < g.stopPrice || bar.close > g.upperExit || trendStart) { - liquidate(pair, barCloseTs, bar.close, 'grid_stop'); + const outOfRange = bar.close < g.stopPrice || bar.close > g.upperExit; + if (p.hardStop) { + const trendStart = !Number.isNaN(ctx.adx[i]) && ctx.adx[i] >= p.adxMax + 5; // Hysterese + if (outOfRange || trendStart) liquidate(pair, barCloseTs, bar.close, 'grid_stop'); + } else if (outOfRange && g.lots.every((l) => !l)) { + grids.delete(pair); // leeres Grid folgt dem Preis (Re-Center ohne Verlust) } } else if ( !Number.isNaN(ctx.atr[i]) && @@ -146,7 +156,7 @@ export function runGridBacktest(candles15ByPair: Map, cfg: GridC ctx.adx[i] < p.adxMax ) { const spacing = p.spacingAtrMult * ctx.atr[i]; - const budgetPerLevel = equity() / PAIRS.length / p.gridLevels; + const budgetPerLevel = equity() / contexts.length / p.gridLevels; if (spacing > 0 && budgetPerLevel >= cfg.minNotionalUsdt) { grids.set(pair, { center: bar.close, diff --git a/src/server/scripts/grid-walkforward.ts b/src/server/scripts/grid-walkforward.ts index 372f8fc..7e05004 100644 --- a/src/server/scripts/grid-walkforward.ts +++ b/src/server/scripts/grid-walkforward.ts @@ -19,7 +19,12 @@ const PARAMS = { gridLevels: argNum('--levels', DEFAULT_GRID_PARAMS.gridLevels), adxMax: argNum('--adx', DEFAULT_GRID_PARAMS.adxMax), tfMs: argNum('--tf', DEFAULT_GRID_PARAMS.tfMs / 60000) * 60000, // Minuten + hardStop: !process.argv.includes('--no-stop'), }; +const ONLY_PAIR = (() => { + const i = process.argv.indexOf('--pair'); + return i >= 0 ? (process.argv[i + 1] as Pair) : null; +})(); const START_CAPITAL = 1000; const EXEC = DEFAULT_EXEC; const MIN_NOTIONAL = 10; @@ -29,6 +34,7 @@ let dataFrom = 0; let dataTo = Number.MAX_SAFE_INTEGER; for (const pair of PAIRS) { + if (ONLY_PAIR && pair !== ONLY_PAIR) continue; const cov = await getCoverage(pair); if (!cov.from || !cov.to) throw new Error(`Keine Candles für ${pair} — erst 'bun run backfill' ausführen.`); candles15ByPair.set(pair, await getCandles(pair)); @@ -37,7 +43,7 @@ for (const pair of PAIRS) { console.log(`${pair}: ${cov.count} Candles (${cov.from.toISOString()} → ${cov.to.toISOString()})`); } -console.log(`\nATR-Grid (fix: spacing ${PARAMS.spacingAtrMult}×ATR, ${PARAMS.gridLevels} Levels, ADX < ${PARAMS.adxMax}, tf ${PARAMS.tfMs / 60000}m, long-only)`); +console.log(`\nATR-Grid (fix: spacing ${PARAMS.spacingAtrMult}×ATR, ${PARAMS.gridLevels} Levels, ADX < ${PARAMS.adxMax}, tf ${PARAMS.tfMs / 60000}m, ${PARAMS.hardStop ? 'hard-stop' : 'NO-STOP'}${ONLY_PAIR ? ', nur ' + ONLY_PAIR : ''}, long-only)`); console.log(`Walk-Forward über ${((dataTo - dataFrom) / 86400000).toFixed(0)} Tage…\n`); const windows = buildWindows(dataFrom, dataTo);