feat: Backtest-Metriken (PF, WinRate, MaxDD, AvgR)

This commit is contained in:
2026-06-09 20:31:21 +00:00
parent edf6793695
commit d738a2c9d0
2 changed files with 64 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
import { expect, test } from 'bun:test';
import { computeMetrics } from './metrics';
import type { ClosedTrade } from '../engine/portfolio';
function t(pnl: number): ClosedTrade {
return { pair: 'BTC_USDT', entryTs: 0, entryPrice: 1, exitTs: 1, exitPrice: 1, qty: 1, pnl, r: pnl / 10, exitReason: 'trailing_stop' };
}
test('ProfitFactor, WinRate, AvgR', () => {
const m = computeMetrics([t(30), t(-10), t(-10)], [], 1000);
expect(m.trades).toBe(3);
expect(m.winRate).toBeCloseTo(1 / 3);
expect(m.profitFactor).toBeCloseTo(30 / 20);
expect(m.totalPnl).toBeCloseTo(10);
expect(m.avgR).toBeCloseTo((3 - 1 - 1) / 3);
});
test('MaxDrawdown aus Equity-Kurve', () => {
const curve = [1000, 1100, 880, 990, 1200].map((equity, i) => ({ ts: i, equity }));
const m = computeMetrics([], curve, 1000);
expect(m.maxDrawdownPct).toBeCloseTo((1100 - 880) / 1100);
});
test('keine Verlierer → ProfitFactor Infinity, keine Trades → 0', () => {
expect(computeMetrics([t(5)], [], 1000).profitFactor).toBe(Infinity);
expect(computeMetrics([], [], 1000).profitFactor).toBe(0);
});

View File

@@ -0,0 +1,37 @@
import type { ClosedTrade } from '../engine/portfolio';
export interface EquityPoint {
ts: number;
equity: number;
}
export interface Metrics {
trades: number;
wins: number;
winRate: number;
profitFactor: number;
totalPnl: number;
maxDrawdownPct: number;
avgR: number;
}
export function computeMetrics(trades: ClosedTrade[], curve: EquityPoint[], startEquity: number): Metrics {
const wins = trades.filter((t) => t.pnl > 0);
const grossWin = wins.reduce((s, t) => s + t.pnl, 0);
const grossLoss = trades.filter((t) => t.pnl <= 0).reduce((s, t) => s - t.pnl, 0);
let peak = startEquity;
let maxDd = 0;
for (const p of curve) {
peak = Math.max(peak, p.equity);
maxDd = Math.max(maxDd, (peak - p.equity) / peak);
}
return {
trades: trades.length,
wins: wins.length,
winRate: trades.length ? wins.length / trades.length : 0,
profitFactor: grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0,
totalPnl: trades.reduce((s, t) => s + t.pnl, 0),
maxDrawdownPct: maxDd,
avgR: trades.length ? trades.reduce((s, t) => s + t.r, 0) / trades.length : 0,
};
}