From d738a2c9d0a2fc5e149494dc00bfcf62bdaa46ae Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 20:31:21 +0000 Subject: [PATCH] feat: Backtest-Metriken (PF, WinRate, MaxDD, AvgR) --- src/server/backtest/metrics.test.ts | 27 +++++++++++++++++++++ src/server/backtest/metrics.ts | 37 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/server/backtest/metrics.test.ts create mode 100644 src/server/backtest/metrics.ts diff --git a/src/server/backtest/metrics.test.ts b/src/server/backtest/metrics.test.ts new file mode 100644 index 0000000..af1d2a1 --- /dev/null +++ b/src/server/backtest/metrics.test.ts @@ -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); +}); diff --git a/src/server/backtest/metrics.ts b/src/server/backtest/metrics.ts new file mode 100644 index 0000000..9a404cf --- /dev/null +++ b/src/server/backtest/metrics.ts @@ -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, + }; +}