feat: Backtest-Metriken (PF, WinRate, MaxDD, AvgR)
This commit is contained in:
27
src/server/backtest/metrics.test.ts
Normal file
27
src/server/backtest/metrics.test.ts
Normal 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);
|
||||||
|
});
|
||||||
37
src/server/backtest/metrics.ts
Normal file
37
src/server/backtest/metrics.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user