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