fix: Gate-Check 4 — Fenster <5 Trades maskieren keine Verstöße mehr

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 21:12:11 +00:00
parent 986ec4446d
commit 26166c5f3c
2 changed files with 29 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import { expect, test } from 'bun:test'; import { expect, test } from 'bun:test';
import { buildWindows, evaluateGate, PARAM_GRID, runWalkForward } from './walkforward'; import { buildWindows, evaluateGate, PARAM_GRID, pickWorstEligibleWindow, runWalkForward } from './walkforward';
import { DEFAULT_RISK } from '../engine/sizing'; import { DEFAULT_RISK } from '../engine/sizing';
import { DEFAULT_EXEC } from '../engine/portfolio'; import { DEFAULT_EXEC } from '../engine/portfolio';
import type { Candle, Pair } from '../types'; import type { Candle, Pair } from '../types';
@@ -38,6 +38,23 @@ test('Gate: alle Kriterien müssen bestehen', () => {
expect(evaluateGate({ ...good, worstWindow: { profitFactor: 0.2, trades: 3 } }).pass).toBe(true); expect(evaluateGate({ ...good, worstWindow: { profitFactor: 0.2, trades: 3 } }).pass).toBe(true);
}); });
test('Gate-Check 4: Fenster mit <5 Trades maskiert kein verletzendes Fenster', () => {
// PF 0 bei 3 Trades (irrelevant) darf PF 0 bei 6 Trades (Verstoß) nicht verdecken
const worst = pickWorstEligibleWindow([
{ profitFactor: 0, trades: 3 },
{ profitFactor: 0, trades: 6 },
{ profitFactor: 1.4, trades: 10 },
]);
expect(worst).toEqual({ profitFactor: 0, trades: 6 });
expect(evaluateGate({
oosProfitFactor: 1.5, oosTrades: 30, oosMaxDrawdownPct: 0.15,
worstWindow: worst, avgTrainPf: 2.0,
}).pass).toBe(false);
// keine Fenster mit >=5 Trades → Check 4 besteht
expect(pickWorstEligibleWindow([{ profitFactor: 0, trades: 3 }])).toEqual({ profitFactor: Infinity, trades: 0 });
});
/** /**
* Synthetische 15m-Candle-Serie für einen einzelnen Walk-Forward-Durchlauf. * Synthetische 15m-Candle-Serie für einen einzelnen Walk-Forward-Durchlauf.
* *

View File

@@ -62,6 +62,16 @@ export interface GateResult {
checks: GateCheck[]; checks: GateCheck[];
} }
/** Schlechtestes Test-Fenster UNTER den für Check 4 relevanten (>= 5 Trades). */
export function pickWorstEligibleWindow(metricsList: { profitFactor: number; trades: number }[]): { profitFactor: number; trades: number } {
return metricsList
.filter((m) => m.trades >= 5)
.reduce(
(acc, m) => (m.profitFactor < acc.profitFactor ? { profitFactor: m.profitFactor, trades: m.trades } : acc),
{ profitFactor: Infinity, trades: 0 },
);
}
export function evaluateGate(g: GateInput): GateResult { export function evaluateGate(g: GateInput): GateResult {
const overfitRatio = g.oosProfitFactor > 0 ? g.avgTrainPf / g.oosProfitFactor : Infinity; const overfitRatio = g.oosProfitFactor > 0 ? g.avgTrainPf / g.oosProfitFactor : Infinity;
const windowFail = g.worstWindow.trades >= 5 && g.worstWindow.profitFactor < 0.5; const windowFail = g.worstWindow.trades >= 5 && g.worstWindow.profitFactor < 0.5;
@@ -145,11 +155,7 @@ export function runWalkForward(
} }
const oosMetrics = computeMetrics(oosTrades, oosEquityCurve, baseCfg.startCapital); const oosMetrics = computeMetrics(oosTrades, oosEquityCurve, baseCfg.startCapital);
const windowsWithTrades = results.filter((r) => r.testMetrics.trades > 0); const worst = pickWorstEligibleWindow(results.map((r) => r.testMetrics));
const worst = windowsWithTrades.reduce(
(acc, r) => (r.testMetrics.profitFactor < acc.profitFactor ? { profitFactor: r.testMetrics.profitFactor, trades: r.testMetrics.trades } : acc),
{ profitFactor: Infinity, trades: 0 },
);
const finiteTrainPfs = results.map((r) => Math.min(r.trainMetrics.profitFactor, 10)); // Infinity kappen const finiteTrainPfs = results.map((r) => Math.min(r.trainMetrics.profitFactor, 10)); // Infinity kappen
const avgTrainPf = finiteTrainPfs.reduce((s, v) => s + v, 0) / Math.max(1, finiteTrainPfs.length); const avgTrainPf = finiteTrainPfs.reduce((s, v) => s + v, 0) / Math.max(1, finiteTrainPfs.length);