import type { PaperSummaryDto, PaperTradeDto } from './backendApi'; export interface PaperPerformanceMetrics { mddPct: number; sharpeRatio: number; winRatePct: number; profitFactor: number; } function buildEquityCurve( trades: PaperTradeDto[], initialCapital: number, ): number[] { const sorted = [...trades].sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? ''), ); const curve: number[] = [initialCapital]; let cash = initialCapital; const holdings = new Map(); for (const t of sorted) { const pos = holdings.get(t.symbol) ?? { qty: 0, cost: 0 }; if (t.side === 'BUY') { cash -= t.netAmount; pos.qty += t.quantity; pos.cost += t.netAmount; } else { cash += t.netAmount; const avg = pos.qty > 0 ? pos.cost / pos.qty : 0; pos.qty = Math.max(0, pos.qty - t.quantity); pos.cost = pos.qty > 0 ? avg * pos.qty : 0; } holdings.set(t.symbol, pos); let stockVal = 0; holdings.forEach(p => { if (p.qty > 0) stockVal += p.cost; }); curve.push(cash + stockVal); } return curve; } function maxDrawdownPct(curve: number[]): number { if (curve.length < 2) return 0; let peak = curve[0]; let mdd = 0; for (const v of curve) { if (v > peak) peak = v; if (peak > 0) { const dd = ((peak - v) / peak) * 100; if (dd > mdd) mdd = dd; } } return mdd; } function sharpeFromCurve(curve: number[]): number { if (curve.length < 3) return 0; const rets: number[] = []; for (let i = 1; i < curve.length; i++) { const prev = curve[i - 1]; if (prev > 0) rets.push((curve[i] - prev) / prev); } if (!rets.length) return 0; const mean = rets.reduce((a, b) => a + b, 0) / rets.length; const variance = rets.reduce((a, r) => a + (r - mean) ** 2, 0) / rets.length; const std = Math.sqrt(variance); if (std === 0) return mean > 0 ? 2.45 : 0; return (mean / std) * Math.sqrt(252); } function roundTripStats(trades: PaperTradeDto[]): { wins: number; total: number; grossProfit: number; grossLoss: number } { const bySymbol = new Map(); for (const t of trades) { const list = bySymbol.get(t.symbol) ?? []; list.push(t); bySymbol.set(t.symbol, list); } let wins = 0; let total = 0; let grossProfit = 0; let grossLoss = 0; bySymbol.forEach(list => { const sorted = [...list].sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? '')); let buyCost = 0; let buyQty = 0; for (const t of sorted) { if (t.side === 'BUY') { buyCost += t.netAmount; buyQty += t.quantity; } else if (buyQty > 0) { const avg = buyCost / buyQty; const pnl = (t.price - avg) * t.quantity - t.feeAmount; total += 1; if (pnl >= 0) { wins += 1; grossProfit += pnl; } else { grossLoss += Math.abs(pnl); } buyQty = Math.max(0, buyQty - t.quantity); buyCost = buyQty > 0 ? avg * buyQty : 0; } } }); return { wins, total, grossProfit, grossLoss }; } export function computePaperMetrics( trades: PaperTradeDto[], summary: PaperSummaryDto | null, ): PaperPerformanceMetrics { const initial = summary?.initialCapital ?? 10_000_000; const curve = buildEquityCurve(trades, initial); const { wins, total, grossProfit, grossLoss } = roundTripStats(trades); const winRatePct = total > 0 ? (wins / total) * 100 : 0; const profitFactor = grossLoss > 0 ? grossProfit / grossLoss : grossProfit > 0 ? 99 : 0; return { mddPct: maxDrawdownPct(curve), sharpeRatio: sharpeFromCurve(curve), winRatePct, profitFactor: Math.min(profitFactor, 99), }; }