aggregateOos() aus runWalkForward herausgezogen und exportiert; beide Walk-Forward-Varianten (Donchian + Rotation) nutzen dieselbe OOS-Logik. Neues Script rotation-walkforward.ts mit identischem Report-Format und Persistenz in backtest_runs (kind='rotation-walkforward'). package.json: "rotation"-Script ergänzt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
import { PAIRS, type Candle, type Pair } from '../types';
|
||
import { getCandles, getCoverage } from '../market/candle-store';
|
||
import { buildWindows, aggregateOos } from '../backtest/walkforward';
|
||
import { runRotationBacktest, type RotationConfig } from '../backtest/rotation';
|
||
import { computeMetrics } from '../backtest/metrics';
|
||
import { DEFAULT_EXEC } from '../engine/portfolio';
|
||
import { db, sql } from '../db/client';
|
||
import { backtestRuns } from '../db/schema';
|
||
|
||
// --- Feste A-priori-Parameter (kein Grid, kein Overfitting möglich) ---
|
||
const LOOKBACK_BARS = 180; // 30 Tage × 6 Bars/Tag auf 4h-TF
|
||
const START_CAPITAL = 1000;
|
||
const EXEC = DEFAULT_EXEC;
|
||
|
||
const candles15ByPair = new Map<Pair, Candle[]>();
|
||
let dataFrom = 0;
|
||
let dataTo = Number.MAX_SAFE_INTEGER;
|
||
|
||
for (const pair of PAIRS) {
|
||
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));
|
||
dataFrom = Math.max(dataFrom, cov.from.getTime());
|
||
dataTo = Math.min(dataTo, cov.to.getTime());
|
||
console.log(`${pair}: ${cov.count} Candles (${cov.from.toISOString()} → ${cov.to.toISOString()})`);
|
||
}
|
||
|
||
console.log(`\nMomentum-Rotation (fix: lookback 30d, weekly, top-1, long-only)`);
|
||
console.log(`Walk-Forward über ${((dataTo - dataFrom) / 86400000).toFixed(0)} Tage…\n`);
|
||
|
||
const windows = buildWindows(dataFrom, dataTo);
|
||
|
||
type WindowResult = {
|
||
window: { trainFrom: number; trainTo: number; testFrom: number; testTo: number };
|
||
trainMetrics: ReturnType<typeof computeMetrics>;
|
||
testMetrics: ReturnType<typeof computeMetrics>;
|
||
testTrades: import('../engine/portfolio').ClosedTrade[];
|
||
testEquityCurve: import('../backtest/metrics').EquityPoint[];
|
||
};
|
||
|
||
const results: WindowResult[] = [];
|
||
|
||
for (const [wi, w] of windows.entries()) {
|
||
const trainCfg: RotationConfig = {
|
||
startCapital: START_CAPITAL,
|
||
exec: EXEC,
|
||
lookbackBars: LOOKBACK_BARS,
|
||
tradeFrom: w.trainFrom,
|
||
tradeTo: w.trainTo,
|
||
};
|
||
const testCfg: RotationConfig = {
|
||
startCapital: START_CAPITAL,
|
||
exec: EXEC,
|
||
lookbackBars: LOOKBACK_BARS,
|
||
tradeFrom: w.testFrom,
|
||
tradeTo: w.testTo,
|
||
};
|
||
|
||
const trainResult = runRotationBacktest(candles15ByPair, trainCfg);
|
||
const trainMetrics = computeMetrics(trainResult.trades, trainResult.equityCurve, START_CAPITAL);
|
||
|
||
const testResult = runRotationBacktest(candles15ByPair, testCfg);
|
||
const testMetrics = computeMetrics(testResult.trades, testResult.equityCurve, START_CAPITAL);
|
||
|
||
results.push({
|
||
window: w,
|
||
trainMetrics,
|
||
testMetrics,
|
||
testTrades: testResult.trades,
|
||
testEquityCurve: testResult.equityCurve,
|
||
});
|
||
|
||
console.log(
|
||
`Fenster ${wi + 1}/${windows.length}: Train-PF ${trainMetrics.profitFactor.toFixed(2)} ` +
|
||
`→ Test-PF ${testMetrics.profitFactor.toFixed(2)} bei ${testMetrics.trades} Trades`,
|
||
);
|
||
}
|
||
|
||
// OOS-Aggregat (gemeinsame Logik aus walkforward.ts)
|
||
const { oosMetrics, oosEquityCurve, gate } = aggregateOos(results, START_CAPITAL);
|
||
|
||
console.log('\n========== OOS-GESAMTERGEBNIS ==========');
|
||
const m = oosMetrics;
|
||
console.log(`Trades: ${m.trades} | WinRate: ${(m.winRate * 100).toFixed(1)}% | PF: ${m.profitFactor.toFixed(2)}`);
|
||
console.log(`TotalPnl: ${m.totalPnl.toFixed(2)} USDT | MaxDD: ${(m.maxDrawdownPct * 100).toFixed(1)}% | AvgR: ${m.avgR.toFixed(2)}`);
|
||
|
||
console.log('\n========== DEPLOY-GATE ==========');
|
||
for (const c of gate.checks) {
|
||
console.log(`${c.pass ? '✅' : '❌'} ${c.name}: ${Number.isFinite(c.value) ? c.value.toFixed(2) : c.value}`);
|
||
}
|
||
console.log(`\n→ GATE ${gate.pass ? 'BESTANDEN' : 'NICHT BESTANDEN'}`);
|
||
|
||
// Persistenz
|
||
await db.insert(backtestRuns).values({
|
||
kind: 'rotation-walkforward',
|
||
config: { startCapital: START_CAPITAL, exec: EXEC, lookbackBars: LOOKBACK_BARS } as any,
|
||
result: {
|
||
gate,
|
||
oosMetrics,
|
||
oosEquityCurve,
|
||
windows: results.map((r) => ({
|
||
window: r.window,
|
||
trainMetrics: r.trainMetrics,
|
||
testMetrics: r.testMetrics,
|
||
})),
|
||
} as any,
|
||
});
|
||
console.log('Run in backtest_runs gespeichert.');
|
||
await sql.end();
|