test: Walk-Forward End-to-End-Leak-Test, Cold-Start dokumentiert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
import { expect, test } from 'bun:test';
|
import { expect, test } from 'bun:test';
|
||||||
import { buildWindows, evaluateGate, PARAM_GRID } from './walkforward';
|
import { buildWindows, evaluateGate, PARAM_GRID, runWalkForward } from './walkforward';
|
||||||
|
import { DEFAULT_RISK } from '../engine/sizing';
|
||||||
|
import { DEFAULT_EXEC } from '../engine/portfolio';
|
||||||
|
import type { Candle, Pair } from '../types';
|
||||||
|
|
||||||
const DAY = 24 * 60 * 60 * 1000;
|
const DAY = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
@@ -34,3 +37,94 @@ test('Gate: alle Kriterien müssen bestehen', () => {
|
|||||||
// PF<0.5 zählt nur bei ≥5 Trades im Fenster
|
// PF<0.5 zählt nur bei ≥5 Trades im Fenster
|
||||||
expect(evaluateGate({ ...good, worstWindow: { profitFactor: 0.2, trades: 3 } }).pass).toBe(true);
|
expect(evaluateGate({ ...good, worstWindow: { profitFactor: 0.2, trades: 3 } }).pass).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synthetische 15m-Candle-Serie für einen einzelnen Walk-Forward-Durchlauf.
|
||||||
|
*
|
||||||
|
* Preis-Modell: stetiger Aufwärtstrend (+0.006%/15m) + deterministisches
|
||||||
|
* Rauschen (LCG-PRNG, Seed 42) + alle 480 Candles (≈5 Tage) ein -1.5%
|
||||||
|
* Pullback. Das erzeugt wiederholt Donchian-Breakout-Signale, während der
|
||||||
|
* Chandelier-Stop durch Pullbacks ausgelöst wird → tatsächlich geschlossene Trades.
|
||||||
|
*
|
||||||
|
* Länge: 151 Tage × 96 Candles/Tag = 14496 15m-Candles
|
||||||
|
* → buildWindows(dataFrom, dataTo) mit Default 120d/30d/30d ergibt genau 1 Fenster.
|
||||||
|
* Laufzeit: ~40ms (18 PARAM_GRID-Kombos × 1 Fenster).
|
||||||
|
*/
|
||||||
|
function buildSyntheticCandles(): { candles: Candle[]; origin: number } {
|
||||||
|
const INTERVAL = 15 * 60 * 1000;
|
||||||
|
const H4 = 4 * 60 * 60 * 1000;
|
||||||
|
// Starte auf einer sauberen 4h-Grenze
|
||||||
|
const origin = 1_700_000_000_000 - (1_700_000_000_000 % H4);
|
||||||
|
const N = 151 * 96; // 14496
|
||||||
|
|
||||||
|
// Deterministischer LCG-PRNG (Seed 42)
|
||||||
|
let seed = 42;
|
||||||
|
const rand = () => { seed = (seed * 1664525 + 1013904223) & 0xffffffff; return (seed >>> 0) / 0xffffffff; };
|
||||||
|
|
||||||
|
const candles: Candle[] = [];
|
||||||
|
let price = 30_000;
|
||||||
|
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
const ts = origin + i * INTERVAL;
|
||||||
|
const noise = (rand() - 0.5) * 0.0005; // ±0.025% pro 15m
|
||||||
|
price = price * (1 + 0.00006 + noise); // +0.006% Drift
|
||||||
|
if (i > 0 && i % 480 === 240) price *= 0.985; // -1.5% Pullback alle ~5 Tage
|
||||||
|
const open = price;
|
||||||
|
const close = price;
|
||||||
|
const high = price * (1 + rand() * 0.002);
|
||||||
|
const low = price * (1 - rand() * 0.002);
|
||||||
|
candles.push({ ts, open, high, low, close, volume: 10 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { candles, origin };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('runWalkForward: OOS-Leak-Test mit synthetischen Daten', () => {
|
||||||
|
const { candles, origin } = buildSyntheticCandles();
|
||||||
|
const dataFrom = origin;
|
||||||
|
const dataTo = origin + 151 * DAY;
|
||||||
|
|
||||||
|
const data = new Map<Pair, Candle[]>([['BTC_USDT', candles]]);
|
||||||
|
|
||||||
|
const result = runWalkForward(
|
||||||
|
data,
|
||||||
|
{ startCapital: 1000, risk: DEFAULT_RISK, exec: DEFAULT_EXEC, maxPositions: 4 },
|
||||||
|
dataFrom,
|
||||||
|
dataTo,
|
||||||
|
);
|
||||||
|
|
||||||
|
// (c) Mindestens 1 Fenster
|
||||||
|
expect(result.windows.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
|
let totalTrades = 0;
|
||||||
|
|
||||||
|
for (const wr of result.windows) {
|
||||||
|
const { window: w, testTrades, testEquityCurve } = wr;
|
||||||
|
|
||||||
|
// (a) Jeder Test-Trade hat entryTs innerhalb des Test-Fensters
|
||||||
|
for (const trade of testTrades) {
|
||||||
|
expect(trade.entryTs).toBeGreaterThanOrEqual(w.testFrom);
|
||||||
|
expect(trade.entryTs).toBeLessThan(w.testTo);
|
||||||
|
// exitTs darf == testTo sein (forced close am Ende)
|
||||||
|
expect(trade.exitTs).toBeGreaterThanOrEqual(w.testFrom);
|
||||||
|
expect(trade.exitTs).toBeLessThanOrEqual(w.testTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (b) Jeder Equity-Punkt liegt nach testFrom
|
||||||
|
for (const pt of testEquityCurve) {
|
||||||
|
expect(pt.ts).toBeGreaterThanOrEqual(w.testFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTrades += testTrades.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (d) OOS-Equity-Kurve ist nicht-fallend in ts
|
||||||
|
for (let i = 1; i < result.oosEquityCurve.length; i++) {
|
||||||
|
expect(result.oosEquityCurve[i].ts).toBeGreaterThanOrEqual(result.oosEquityCurve[i - 1].ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity-Check: mindestens 1 Trade (damit Assertions nicht vacuous sind)
|
||||||
|
expect(totalTrades).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
console.log(`[walkforward e2e] Fenster: ${result.windows.length}, OOS-Trades gesamt: ${totalTrades}`);
|
||||||
|
}, 15_000); // 15s Timeout für 18 PARAM_GRID-Kombos × 1 Fenster
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export interface Window {
|
|||||||
testTo: number;
|
testTo: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollierende Train/Test-Fenster. Hinweis: Im ersten Fenster sind die ersten
|
||||||
|
* ~EMA200-Perioden (≈33 Tage auf 4h) Indikator-Cold-Start — effektive
|
||||||
|
* Train-Länge von Fenster 0 ist entsprechend kürzer als bei späteren Fenstern.
|
||||||
|
*/
|
||||||
export function buildWindows(dataFrom: number, dataTo: number, trainDays = 120, testDays = 30, stepDays = 30): Window[] {
|
export function buildWindows(dataFrom: number, dataTo: number, trainDays = 120, testDays = 30, stepDays = 30): Window[] {
|
||||||
const out: Window[] = [];
|
const out: Window[] = [];
|
||||||
for (let start = dataFrom; start + (trainDays + testDays) * DAY <= dataTo; start += stepDays * DAY) {
|
for (let start = dataFrom; start + (trainDays + testDays) * DAY <= dataTo; start += stepDays * DAY) {
|
||||||
|
|||||||
Reference in New Issue
Block a user