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:
@@ -1,5 +1,5 @@
|
||||
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_EXEC } from '../engine/portfolio';
|
||||
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);
|
||||
});
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -62,6 +62,16 @@ export interface GateResult {
|
||||
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 {
|
||||
const overfitRatio = g.oosProfitFactor > 0 ? g.avgTrainPf / g.oosProfitFactor : Infinity;
|
||||
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 windowsWithTrades = results.filter((r) => r.testMetrics.trades > 0);
|
||||
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 worst = pickWorstEligibleWindow(results.map((r) => r.testMetrics));
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user