diff --git a/docs/walkforward-grid-2026-06-10.md b/docs/walkforward-grid-2026-06-10.md index 2913793..31ca04c 100644 --- a/docs/walkforward-grid-2026-06-10.md +++ b/docs/walkforward-grid-2026-06-10.md @@ -10,6 +10,19 @@ | B: spacing 1.5×ATR | 1.03 | 514 | 56.0 % | 10.6 % | 1.53 | ❌ PF + Fenster | | C: ADX<15 (strenger) | 0.94 | 425 | 63.3 % | 6.5 % | 1.24 | ❌ PF + Fenster | +## Nachtrag: kürzere Timeframes (User-Frage „wird das auf kürzeren Einheiten besser?") + +Entscheidungs-Timeframe parametrisiert (`--tf` Minuten; ATR/ADX/Aktivierung auf 1h bzw. 15m statt 4h): + +| Variante | OOS-PF | Trades | WinRate | MaxDD | Gate | +|---|---|---|---|---|---| +| D: tf 1h, spacing 1.0×ATR | 0.59 | 4410 | 63.9 % | 57.1 % | ❌ | +| E: tf 1h, spacing 1.5×ATR | 0.64 | 2246 | 54.3 % | 37.6 % | ❌ | +| F: tf 15m, spacing 1.0×ATR | 0.29 | 19 783 | 47.1 % | **97.2 %** | ❌ | +| G: tf 15m, spacing 2.0×ATR | 0.47 | 6259 | 44.7 % | 67.8 % | ❌ | + +**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). + ## 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 0f3c23c..e11e678 100644 --- a/src/server/backtest/grid.ts +++ b/src/server/backtest/grid.ts @@ -1,6 +1,6 @@ import type { Candle, Pair } from '../types'; import { PAIRS } from '../types'; -import { aggregate4h, H4 } from '../market/aggregate'; +import { aggregateTf, H4 } from '../market/aggregate'; import { atr } from '../indicators/atr'; import { adx } from '../indicators/adx'; import type { ClosedTrade, ExecConfig } from '../engine/portfolio'; @@ -8,10 +8,11 @@ import type { EquityPoint } from './metrics'; import type { BacktestResult } from './runner'; export interface GridParams { - spacingAtrMult: number; // Level-Abstand = mult × ATR(14, 4h) bei Aktivierung + spacingAtrMult: number; // Level-Abstand = mult × ATR(atrPeriod, tf) bei Aktivierung gridLevels: number; // Buy-Levels unterhalb des Centers adxMax: number; // Grid nur aktiv im Seitwärtsregime (ADX < adxMax) atrPeriod: number; + tfMs: number; // Entscheidungs-Timeframe (Aktivierung/Deaktivierung, ATR/ADX-Basis) } export const DEFAULT_GRID_PARAMS: GridParams = { @@ -19,6 +20,7 @@ export const DEFAULT_GRID_PARAMS: GridParams = { gridLevels: 4, adxMax: 20, atrPeriod: 14, + tfMs: H4, }; export interface GridConfig { @@ -50,8 +52,8 @@ interface ActiveGrid { /** * ATR-Grid mit ADX-Regime-Filter, long-only, je Pair unabhängig. - * 4h-Close: Aktivierung/Deaktivierung; 15m: Fills (Sells vor Buys — - * ein im selben Bar gekaufter Lot kann nicht im selben Bar verkaufen). + * Tf-Close (default 4h): Aktivierung/Deaktivierung; 15m: Fills (Sells vor + * Buys — ein im selben Bar gekaufter Lot kann nicht im selben Bar verkaufen). * Fee/Slippage-Mathematik identisch zu Portfolio (pessimistische Fills). */ export function runGridBacktest(candles15ByPair: Map, cfg: GridConfig): BacktestResult { @@ -106,7 +108,7 @@ export function runGridBacktest(candles15ByPair: Map, cfg: GridC // --- Kontexte + gemergte 15m-Timeline (wie runner.ts) --- const contexts = PAIRS.filter((pr) => candles15ByPair.has(pr)).map((pair) => { const c15 = candles15ByPair.get(pair)!; - const c4h = aggregate4h(c15); + const c4h = aggregateTf(c15, p.tfMs); return { pair, c4h, atr: atr(c4h, p.atrPeriod), adx: adx(c4h, p.atrPeriod), next4h: 0 }; }); const byPair = new Map(contexts.map((c) => [c.pair, c])); @@ -123,13 +125,13 @@ export function runGridBacktest(candles15ByPair: Map, cfg: GridC for (const { ts, pair, candle } of timeline) { const ctx = byPair.get(pair)!; - const bucket = Math.floor(ts / H4) * H4; + const bucket = Math.floor(ts / p.tfMs) * p.tfMs; - // 1) Neu abgeschlossene 4h-Bars: Deaktivierung / Aktivierung + // 1) Neu abgeschlossene Tf-Bars: Deaktivierung / Aktivierung while (ctx.next4h < ctx.c4h.length && ctx.c4h[ctx.next4h].ts < bucket) { const i = ctx.next4h++; const bar = ctx.c4h[i]; - const barCloseTs = bar.ts + H4; + const barCloseTs = bar.ts + p.tfMs; if (barCloseTs < cfg.tradeFrom || barCloseTs >= cfg.tradeTo) continue; const g = grids.get(pair); diff --git a/src/server/market/aggregate.ts b/src/server/market/aggregate.ts index b5eaeb5..06021bf 100644 --- a/src/server/market/aggregate.ts +++ b/src/server/market/aggregate.ts @@ -3,15 +3,15 @@ import type { Candle } from '../types'; export const H4 = 4 * 60 * 60 * 1000; /** - * Aggregiert 15m-Candles (sortiert, ts aufsteigend) zu 4h-Candles. + * Aggregiert 15m-Candles (sortiert, ts aufsteigend) zu Buckets von `tfMs`. * Der letzte Bucket wird verworfen, weil nicht feststellbar ist, ob er - * abgeschlossen ist — der Backtest-Runner arbeitet nur auf geschlossenen Candles. + * abgeschlossen ist — Backtests arbeiten nur auf geschlossenen Candles. */ -export function aggregate4h(c15: Candle[]): Candle[] { +export function aggregateTf(c15: Candle[], tfMs: number): Candle[] { const out: Candle[] = []; let cur: Candle | null = null; for (const c of c15) { - const bucket = Math.floor(c.ts / H4) * H4; + const bucket = Math.floor(c.ts / tfMs) * tfMs; if (cur && cur.ts === bucket) { cur.high = Math.max(cur.high, c.high); cur.low = Math.min(cur.low, c.low); @@ -24,3 +24,9 @@ export function aggregate4h(c15: Candle[]): Candle[] { } return out; // letzter Bucket absichtlich nicht gepusht } + +/** Aggregiert 15m-Candles zu 4h-Candles. */ +export function aggregate4h(c15: Candle[]): Candle[] { + return aggregateTf(c15, H4); +} + diff --git a/src/server/scripts/grid-walkforward.ts b/src/server/scripts/grid-walkforward.ts index 768fa23..372f8fc 100644 --- a/src/server/scripts/grid-walkforward.ts +++ b/src/server/scripts/grid-walkforward.ts @@ -14,10 +14,11 @@ function argNum(flag: string, def: number): number { return i >= 0 ? Number(process.argv[i + 1]) : def; } const PARAMS = { - ...DEFAULT_GRID_PARAMS, // spacing 1×ATR, 4 Levels, ADX < 20 + ...DEFAULT_GRID_PARAMS, // spacing 1×ATR, 4 Levels, ADX < 20, tf 4h spacingAtrMult: argNum('--spacing', DEFAULT_GRID_PARAMS.spacingAtrMult), gridLevels: argNum('--levels', DEFAULT_GRID_PARAMS.gridLevels), adxMax: argNum('--adx', DEFAULT_GRID_PARAMS.adxMax), + tfMs: argNum('--tf', DEFAULT_GRID_PARAMS.tfMs / 60000) * 60000, // Minuten }; const START_CAPITAL = 1000; const EXEC = DEFAULT_EXEC; @@ -36,7 +37,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}, long-only)`); +console.log(`\nATR-Grid (fix: spacing ${PARAMS.spacingAtrMult}×ATR, ${PARAMS.gridLevels} Levels, ADX < ${PARAMS.adxMax}, tf ${PARAMS.tfMs / 60000}m, long-only)`); console.log(`Walk-Forward über ${((dataTo - dataFrom) / 86400000).toFixed(0)} Tage…\n`); const windows = buildWindows(dataFrom, dataTo);