feat: ATR-GridBot mit Regime-Filter — Walk-Forward, Gate nicht bestanden

3 fixe Varianten (spacing 1.0/1.5×ATR, ADX<20/15): OOS-PF 0.87/1.03/0.94.
Grid-Stops bei Range-Breakdowns fressen die TP-Gewinne — kein Paper-Deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 06:43:38 +00:00
parent 2dcab7f24d
commit b5dd953afc
7 changed files with 474 additions and 1 deletions

View File

@@ -0,0 +1,112 @@
import { PAIRS, type Candle, type Pair } from '../types';
import { getCandles, getCoverage } from '../market/candle-store';
import { buildWindows, aggregateOos } from '../backtest/walkforward';
import { runGridBacktest, DEFAULT_GRID_PARAMS, type GridConfig } from '../backtest/grid';
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-Search; Varianten nur als bewusste
// Design-Entscheidung via CLI: --spacing 1.5 --levels 4 --adx 15) ---
function argNum(flag: string, def: number): number {
const i = process.argv.indexOf(flag);
return i >= 0 ? Number(process.argv[i + 1]) : def;
}
const PARAMS = {
...DEFAULT_GRID_PARAMS, // spacing 1×ATR, 4 Levels, ADX < 20
spacingAtrMult: argNum('--spacing', DEFAULT_GRID_PARAMS.spacingAtrMult),
gridLevels: argNum('--levels', DEFAULT_GRID_PARAMS.gridLevels),
adxMax: argNum('--adx', DEFAULT_GRID_PARAMS.adxMax),
};
const START_CAPITAL = 1000;
const EXEC = DEFAULT_EXEC;
const MIN_NOTIONAL = 10;
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(`\nATR-Grid (fix: spacing ${PARAMS.spacingAtrMult}×ATR, ${PARAMS.gridLevels} Levels, ADX < ${PARAMS.adxMax}, 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 mkCfg = (tradeFrom: number, tradeTo: number): GridConfig => ({
startCapital: START_CAPITAL,
exec: EXEC,
params: PARAMS,
minNotionalUsdt: MIN_NOTIONAL,
tradeFrom,
tradeTo,
});
const trainResult = runGridBacktest(candles15ByPair, mkCfg(w.trainFrom, w.trainTo));
const trainMetrics = computeMetrics(trainResult.trades, trainResult.equityCurve, START_CAPITAL);
const testResult = runGridBacktest(candles15ByPair, mkCfg(w.testFrom, w.testTo));
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`,
);
}
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'}`);
await db.insert(backtestRuns).values({
kind: 'grid-walkforward',
config: { startCapital: START_CAPITAL, exec: EXEC, params: PARAMS, minNotionalUsdt: MIN_NOTIONAL } 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();