/** * Performance Metrics and Analysis * Comprehensive performance measurement tools for trading strategies and portfolios */ import { PortfolioMetrics } from './index'; export interface TradePerformance { totalTrades: number; winningTrades: number; losingTrades: number; winRate: number; averageWin: number; averageLoss: number; largestWin: number; largestLoss: number; profitFactor: number; expectancy: number; averageTradeReturn: number; consecutiveWins: number; consecutiveLosses: number; } export interface DrawdownAnalysis { maxDrawdown: number; maxDrawdownDuration: number; averageDrawdown: number; drawdownPeriods: Array<{ start: Date; end: Date; duration: number; magnitude: number; }>; } export interface ReturnAnalysis { totalReturn: number; annualizedReturn: number; compoundAnnualGrowthRate: number; volatility: number; annualizedVolatility: number; skewness: number; kurtosis: number; bestMonth: number; worstMonth: number; positiveMonths: number; negativeMonths: number; } /** * Calculate comprehensive trade performance metrics */ export function analyzeTradePerformance(trades: Array<{ pnl: number; date: Date }>): TradePerformance { if (trades.length === 0) { return { totalTrades: 0, winningTrades: 0, losingTrades: 0, winRate: 0, averageWin: 0, averageLoss: 0, largestWin: 0, largestLoss: 0, profitFactor: 0, expectancy: 0, averageTradeReturn: 0, consecutiveWins: 0, consecutiveLosses: 0 }; } const winningTrades = trades.filter(trade => trade.pnl > 0); const losingTrades = trades.filter(trade => trade.pnl < 0); const totalWins = winningTrades.reduce((sum, trade) => sum + trade.pnl, 0); const totalLosses = Math.abs(losingTrades.reduce((sum, trade) => sum + trade.pnl, 0)); const averageWin = winningTrades.length > 0 ? totalWins / winningTrades.length : 0; const averageLoss = losingTrades.length > 0 ? totalLosses / losingTrades.length : 0; const largestWin = winningTrades.length > 0 ? Math.max(...winningTrades.map(t => t.pnl)) : 0; const largestLoss = losingTrades.length > 0 ? Math.min(...losingTrades.map(t => t.pnl)) : 0; const profitFactor = totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0; const winRate = winningTrades.length / trades.length; const expectancy = (winRate * averageWin) - ((1 - winRate) * averageLoss); const totalPnL = trades.reduce((sum, trade) => sum + trade.pnl, 0); const averageTradeReturn = totalPnL / trades.length; // Calculate consecutive wins/losses let consecutiveWins = 0; let consecutiveLosses = 0; let currentWinStreak = 0; let currentLossStreak = 0; for (const trade of trades) { if (trade.pnl > 0) { currentWinStreak++; currentLossStreak = 0; consecutiveWins = Math.max(consecutiveWins, currentWinStreak); } else if (trade.pnl < 0) { currentLossStreak++; currentWinStreak = 0; consecutiveLosses = Math.max(consecutiveLosses, currentLossStreak); } } return { totalTrades: trades.length, winningTrades: winningTrades.length, losingTrades: losingTrades.length, winRate, averageWin, averageLoss, largestWin, largestLoss, profitFactor, expectancy, averageTradeReturn, consecutiveWins, consecutiveLosses }; } /** * Analyze drawdown characteristics */ export function analyzeDrawdowns(equityCurve: Array<{ value: number; date: Date }>): DrawdownAnalysis { if (equityCurve.length < 2) { return { maxDrawdown: 0, maxDrawdownDuration: 0, averageDrawdown: 0, drawdownPeriods: [] }; } let peak = equityCurve[0].value; let peakDate = equityCurve[0].date; let maxDrawdown = 0; let maxDrawdownDuration = 0; const drawdownPeriods: Array<{ start: Date; end: Date; duration: number; magnitude: number; }> = []; let currentDrawdownStart: Date | null = null; let drawdowns: number[] = []; for (let i = 1; i < equityCurve.length; i++) { const current = equityCurve[i]; if (current.value > peak) { // New peak - end any current drawdown if (currentDrawdownStart) { const drawdownMagnitude = (peak - equityCurve[i - 1].value) / peak; const duration = Math.floor((equityCurve[i - 1].date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24)); drawdownPeriods.push({ start: currentDrawdownStart, end: equityCurve[i - 1].date, duration, magnitude: drawdownMagnitude }); drawdowns.push(drawdownMagnitude); maxDrawdownDuration = Math.max(maxDrawdownDuration, duration); currentDrawdownStart = null; } peak = current.value; peakDate = current.date; } else { // In drawdown if (!currentDrawdownStart) { currentDrawdownStart = peakDate; } const drawdown = (peak - current.value) / peak; maxDrawdown = Math.max(maxDrawdown, drawdown); } } // Handle ongoing drawdown if (currentDrawdownStart) { const lastPoint = equityCurve[equityCurve.length - 1]; const drawdownMagnitude = (peak - lastPoint.value) / peak; const duration = Math.floor((lastPoint.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24)); drawdownPeriods.push({ start: currentDrawdownStart, end: lastPoint.date, duration, magnitude: drawdownMagnitude }); drawdowns.push(drawdownMagnitude); maxDrawdownDuration = Math.max(maxDrawdownDuration, duration); } const averageDrawdown = drawdowns.length > 0 ? drawdowns.reduce((sum, dd) => sum + dd, 0) / drawdowns.length : 0; return { maxDrawdown, maxDrawdownDuration, averageDrawdown, drawdownPeriods }; } /** * Analyze return characteristics */ export function analyzeReturns( returns: Array<{ return: number; date: Date }>, periodsPerYear: number = 252 ): ReturnAnalysis { if (returns.length === 0) { return { totalReturn: 0, annualizedReturn: 0, compoundAnnualGrowthRate: 0, volatility: 0, annualizedVolatility: 0, skewness: 0, kurtosis: 0, bestMonth: 0, worstMonth: 0, positiveMonths: 0, negativeMonths: 0 }; } const returnValues = returns.map(r => r.return); // Calculate basic statistics const totalReturn = returnValues.reduce((product, ret) => product * (1 + ret), 1) - 1; const averageReturn = returnValues.reduce((sum, ret) => sum + ret, 0) / returnValues.length; const annualizedReturn = Math.pow(1 + averageReturn, periodsPerYear) - 1; // Calculate CAGR const years = returns.length / periodsPerYear; const cagr = years > 0 ? Math.pow(1 + totalReturn, 1 / years) - 1 : 0; // Calculate volatility const variance = returnValues.reduce((sum, ret) => sum + Math.pow(ret - averageReturn, 2), 0) / (returnValues.length - 1); const volatility = Math.sqrt(variance); const annualizedVolatility = volatility * Math.sqrt(periodsPerYear); // Calculate skewness and kurtosis const skewness = calculateSkewness(returnValues); const kurtosis = calculateKurtosis(returnValues); // Monthly analysis const monthlyReturns = aggregateMonthlyReturns(returns); const bestMonth = monthlyReturns.length > 0 ? Math.max(...monthlyReturns) : 0; const worstMonth = monthlyReturns.length > 0 ? Math.min(...monthlyReturns) : 0; const positiveMonths = monthlyReturns.filter(ret => ret > 0).length; const negativeMonths = monthlyReturns.filter(ret => ret < 0).length; return { totalReturn, annualizedReturn, compoundAnnualGrowthRate: cagr, volatility, annualizedVolatility, skewness, kurtosis, bestMonth, worstMonth, positiveMonths, negativeMonths }; } /** * Calculate rolling performance metrics */ export function calculateRollingMetrics( returns: number[], windowSize: number, metricType: 'sharpe' | 'volatility' | 'return' = 'sharpe' ): number[] { if (returns.length < windowSize) return []; const rollingMetrics: number[] = []; for (let i = windowSize - 1; i < returns.length; i++) { const window = returns.slice(i - windowSize + 1, i + 1); switch (metricType) { case 'sharpe': rollingMetrics.push(calculateSharpeRatio(window)); break; case 'volatility': rollingMetrics.push(calculateVolatility(window)); break; case 'return': const avgReturn = window.reduce((sum, ret) => sum + ret, 0) / window.length; rollingMetrics.push(avgReturn); break; } } return rollingMetrics; } /** * Calculate performance attribution */ export function strategyPerformanceAttribution( portfolioReturns: number[], benchmarkReturns: number[], sectorWeights: number[], sectorReturns: number[] ): { allocationEffect: number; selectionEffect: number; interactionEffect: number; totalActiveReturn: number; } { if (portfolioReturns.length !== benchmarkReturns.length) { throw new Error('Portfolio and benchmark returns must have same length'); } const portfolioReturn = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length; const benchmarkReturn = benchmarkReturns.reduce((sum, ret) => sum + ret, 0) / benchmarkReturns.length; let allocationEffect = 0; let selectionEffect = 0; let interactionEffect = 0; for (let i = 0; i < sectorWeights.length; i++) { const portfolioWeight = sectorWeights[i]; const benchmarkWeight = 1 / sectorWeights.length; // Assuming equal benchmark weights const sectorReturn = sectorReturns[i]; // Allocation effect: (portfolio weight - benchmark weight) * (benchmark sector return - benchmark return) allocationEffect += (portfolioWeight - benchmarkWeight) * (sectorReturn - benchmarkReturn); // Selection effect: benchmark weight * (portfolio sector return - benchmark sector return) selectionEffect += benchmarkWeight * (sectorReturn - sectorReturn); // Simplified // Interaction effect: (portfolio weight - benchmark weight) * (portfolio sector return - benchmark sector return) interactionEffect += (portfolioWeight - benchmarkWeight) * (sectorReturn - sectorReturn); // Simplified } const totalActiveReturn = portfolioReturn - benchmarkReturn; return { allocationEffect, selectionEffect, interactionEffect, totalActiveReturn }; } /** * Calculate Omega ratio */ export function omegaRatio(returns: number[], threshold: number = 0): number { if (returns.length === 0) return 0; const gains = returns.filter(ret => ret > threshold).reduce((sum, ret) => sum + (ret - threshold), 0); const losses = returns.filter(ret => ret < threshold).reduce((sum, ret) => sum + Math.abs(ret - threshold), 0); return losses === 0 ? Infinity : gains / losses; } /** * Calculate gain-to-pain ratio */ export function gainToPainRatio(returns: number[]): number { if (returns.length === 0) return 0; const totalGain = returns.reduce((sum, ret) => sum + ret, 0); const totalPain = returns.filter(ret => ret < 0).reduce((sum, ret) => sum + Math.abs(ret), 0); return totalPain === 0 ? (totalGain > 0 ? Infinity : 0) : totalGain / totalPain; } /** * Calculate Martin ratio (modified Sharpe with downside deviation) */ export function martinRatio(returns: number[], riskFreeRate: number = 0): number { if (returns.length === 0) return 0; const averageReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const downsideReturns = returns.filter(ret => ret < riskFreeRate); if (downsideReturns.length === 0) return Infinity; const downsideDeviation = Math.sqrt( downsideReturns.reduce((sum, ret) => sum + Math.pow(ret - riskFreeRate, 2), 0) / returns.length ); return downsideDeviation === 0 ? Infinity : (averageReturn - riskFreeRate) / downsideDeviation; } /** * Calculate comprehensive portfolio metrics */ export function calculateStrategyMetrics( equityCurve: Array<{ value: number; date: Date }>, benchmarkReturns?: number[], riskFreeRate: number = 0.02 ): PortfolioMetrics { if (equityCurve.length < 2) { return { totalValue: 0, totalReturn: 0, totalReturnPercent: 0, dailyReturn: 0, dailyReturnPercent: 0, maxDrawdown: 0, sharpeRatio: 0, beta: 0, alpha: 0, volatility: 0 }; } const returns = []; for (let i = 1; i < equityCurve.length; i++) { const ret = (equityCurve[i].value - equityCurve[i - 1].value) / equityCurve[i - 1].value; returns.push(ret); } const totalValue = equityCurve[equityCurve.length - 1].value; const totalReturn = totalValue - equityCurve[0].value; const totalReturnPercent = (totalReturn / equityCurve[0].value) * 100; const dailyReturn = returns[returns.length - 1]; const dailyReturnPercent = dailyReturn * 100; const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown; const sharpeRatio = calculateSharpeRatio(returns, riskFreeRate); const volatility = calculateVolatility(returns); let beta = 0; let alpha = 0; if (benchmarkReturns && benchmarkReturns.length === returns.length) { beta = calculateBeta(returns, benchmarkReturns); alpha = calculateAlpha(returns, benchmarkReturns, riskFreeRate); } return { totalValue, totalReturn, totalReturnPercent, dailyReturn, dailyReturnPercent, maxDrawdown, sharpeRatio, beta, alpha, volatility }; } /** * Calculate Calmar Ratio */ export function calmarRatio(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number { const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown; const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; return maxDrawdown === 0 ? 0 : (avgReturn - riskFreeRate) / maxDrawdown; } /** * Calculate Sterling Ratio */ export function sterlingRatio(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number { const averageDrawdown = analyzeDrawdowns(equityCurve).averageDrawdown; const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; return averageDrawdown === 0 ? 0 : (avgReturn - riskFreeRate) / averageDrawdown; } /** * Calculate Ulcer Index */ export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number { let sumSquaredDrawdown = 0; let peak = equityCurve[0].value; for (const point of equityCurve) { peak = Math.max(peak, point.value); const drawdownPercent = (peak - point.value) / peak * 100; sumSquaredDrawdown += drawdownPercent * drawdownPercent; } return Math.sqrt(sumSquaredDrawdown / equityCurve.length); } /** * Calculate Ulcer Performance Index (UPI) */ export function ulcerPerformanceIndex(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number { const ui = ulcerIndex(equityCurve); const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; return ui === 0 ? 0 : (avgReturn - riskFreeRate) / ui; } /** * Calculate Information Ratio */ export function informationRatio(portfolioReturns: number[], benchmarkReturns: number[]): number { if (portfolioReturns.length !== benchmarkReturns.length) { throw new Error("Portfolio and benchmark returns must have the same length."); } const excessReturns = portfolioReturns.map((portfolioReturn, index) => portfolioReturn - benchmarkReturns[index]); const trackingError = calculateVolatility(excessReturns); const avgExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length; return trackingError === 0 ? 0 : avgExcessReturn / trackingError; } /** * Calculate Treynor Ratio */ export function treynorRatio(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number): number { const beta = calculateBeta(portfolioReturns, marketReturns); const avgPortfolioReturn = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length; return beta === 0 ? 0 : (avgPortfolioReturn - riskFreeRate) / beta; } /** * Calculate Jensen's Alpha (same as Alpha, but included for clarity) */ export function jensensAlpha(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number): number { return calculateAlpha(portfolioReturns, marketReturns, riskFreeRate); } /** * Calculate Capture Ratio (Up Capture and Down Capture) */ export function captureRatio(portfolioReturns: number[], benchmarkReturns: number[]): { upCaptureRatio: number; downCaptureRatio: number } { let upCapture = 0; let downCapture = 0; let upMarketPeriods = 0; let downMarketPeriods = 0; for (let i = 0; i < portfolioReturns.length; i++) { if (benchmarkReturns[i] > 0) { upCapture += portfolioReturns[i]; upMarketPeriods++; } else if (benchmarkReturns[i] < 0) { downCapture += portfolioReturns[i]; downMarketPeriods++; } } const upCaptureRatio = upMarketPeriods > 0 ? (upCapture / upMarketPeriods) / (benchmarkReturns.filter(r => r > 0).reduce((sum, r) => sum + r, 0) / upMarketPeriods) : 0; const downCaptureRatio = downMarketPeriods > 0 ? (downCapture / downMarketPeriods) / (benchmarkReturns.filter(r => r < 0).reduce((sum, r) => sum + r, 0) / downMarketPeriods) : 0; return { upCaptureRatio, downCaptureRatio }; } /** * Calculate Sortino Ratio */ export function sortinoRatio(returns: number[], riskFreeRate: number = 0): number { const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const downsideReturns = returns.filter(ret => ret < riskFreeRate); const downsideDeviation = Math.sqrt( downsideReturns.reduce((sum, ret) => sum + Math.pow(ret - riskFreeRate, 2), 0) / returns.length ); return downsideDeviation === 0 ? 0 : (avgReturn - riskFreeRate) / downsideDeviation; } /** * Calculate Tail Ratio */ export function tailRatio(returns: number[], tailPercent: number = 0.1): number { const numReturns = returns.length; const tailSize = Math.floor(numReturns * tailPercent); if (tailSize === 0) return 0; const sortedReturns = [...returns].sort((a, b) => a - b); const worstTail = sortedReturns.slice(0, tailSize); const bestTail = sortedReturns.slice(numReturns - tailSize); const avgWorst = worstTail.reduce((sum, ret) => sum + ret, 0) / tailSize; const avgBest = bestTail.reduce((sum, ret) => sum + ret, 0) / tailSize; return avgWorst === 0 ? 0 : avgBest / Math.abs(avgWorst); } /** * Calculate Value at Risk (VaR) */ export function valueAtRisk(returns: number[], confidenceLevel: number = 0.05): number { const sortedReturns = [...returns].sort((a, b) => a - b); const varIndex = Math.floor(confidenceLevel * returns.length); return sortedReturns[varIndex]; } /** * Calculate Conditional Value at Risk (CVaR) / Expected Shortfall */ export function conditionalValueAtRisk(returns: number[], confidenceLevel: number = 0.05): number { const sortedReturns = [...returns].sort((a, b) => a - b); const varIndex = Math.floor(confidenceLevel * returns.length); const tailReturns = sortedReturns.slice(0, varIndex + 1); return tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length; } /** * Calculate Rolling Beta */ export function calculateRollingBeta(portfolioReturns: number[], marketReturns: number[], windowSize: number): number[] { if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < windowSize) return []; const rollingBetas: number[] = []; for (let i = windowSize; i <= portfolioReturns.length; i++) { const portfolioWindow = portfolioReturns.slice(i - windowSize, i); const marketWindow = marketReturns.slice(i - windowSize, i); rollingBetas.push(calculateBeta(portfolioWindow, marketWindow)); } return rollingBetas; } /** * Calculate Rolling Alpha */ export function calculateRollingAlpha(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number, windowSize: number): number[] { if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < windowSize) return []; const rollingAlphas: number[] = []; for (let i = windowSize; i <= portfolioReturns.length; i++) { const portfolioWindow = portfolioReturns.slice(i - windowSize, i); const marketWindow = marketReturns.slice(i - windowSize, i); rollingAlphas.push(calculateAlpha(portfolioWindow, marketWindow, riskFreeRate)); } return rollingAlphas; } /** * Calculate Time Weighted Rate of Return (TWRR) */ export function timeWeightedRateOfReturn(cashFlows: Array<{ amount: number; date: Date; value: number }>): number { let totalReturn = 1; let previousValue = cashFlows[0].value; for (let i = 1; i < cashFlows.length; i++) { const current = cashFlows[i]; const periodReturn = (current.value - previousValue - current.amount) / (previousValue + current.amount); totalReturn *= (1 + periodReturn); previousValue = current.value; } return totalReturn - 1; } /** * Calculate Money Weighted Rate of Return (MWRR) - Approximation using IRR */ export function moneyWeightedRateOfReturn(cashFlows: Array<{ amount: number; date: Date; value: number }>): number { // Approximate MWRR using Internal Rate of Return (IRR) // This requires a numerical method or library for accurate IRR calculation // This is a simplified example and may not be accurate for all cases let totalCashFlow = 0; let totalWeightedCashFlow = 0; const startDate = cashFlows[0].date.getTime(); for (const cf of cashFlows) { const timeDiff = (cf.date.getTime() - startDate) / (1000 * 60 * 60 * 24 * 365); // Years totalCashFlow += cf.amount; totalWeightedCashFlow += cf.amount * timeDiff; } // Simplified approximation: MWRR ≈ totalCashFlow / totalWeightedCashFlow - 1 return totalCashFlow / totalWeightedCashFlow - 1; } // Helper functions function calculateSharpeRatio(returns: number[], riskFreeRate: number = 0): number { if (returns.length < 2) return 0; const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / (returns.length - 1); const stdDev = Math.sqrt(variance); return stdDev === 0 ? 0 : (avgReturn - riskFreeRate) / stdDev; } function calculateVolatility(returns: number[]): number { if (returns.length < 2) return 0; const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1); return Math.sqrt(variance); } function calculateBeta(portfolioReturns: number[], marketReturns: number[]): number { if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < 2) return 0; const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length; const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / marketReturns.length; let covariance = 0; let marketVariance = 0; for (let i = 0; i < portfolioReturns.length; i++) { const portfolioDiff = portfolioReturns[i] - portfolioMean; const marketDiff = marketReturns[i] - marketMean; covariance += portfolioDiff * marketDiff; marketVariance += marketDiff * marketDiff; } covariance /= (portfolioReturns.length - 1); marketVariance /= (marketReturns.length - 1); return marketVariance === 0 ? 0 : covariance / marketVariance; } function calculateAlpha( portfolioReturns: number[], marketReturns: number[], riskFreeRate: number ): number { const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length; const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / marketReturns.length; const beta = calculateBeta(portfolioReturns, marketReturns); return portfolioMean - (riskFreeRate + beta * (marketMean - riskFreeRate)); } function calculateSkewness(returns: number[]): number { if (returns.length < 3) return 0; const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length; const stdDev = Math.sqrt(variance); if (stdDev === 0) return 0; const skew = returns.reduce((sum, ret) => sum + Math.pow((ret - mean) / stdDev, 3), 0) / returns.length; return skew; } function calculateKurtosis(returns: number[]): number { if (returns.length < 4) return 0; const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length; const stdDev = Math.sqrt(variance); if (stdDev === 0) return 0; const kurt = returns.reduce((sum, ret) => sum + Math.pow((ret - mean) / stdDev, 4), 0) / returns.length; return kurt - 3; // Excess kurtosis } function aggregateMonthlyReturns(returns: Array<{ return: number; date: Date }>): number[] { const monthlyReturns: { [key: string]: number } = {}; for (const ret of returns) { const monthKey = `${ret.date.getFullYear()}-${ret.date.getMonth()}`; if (!monthlyReturns[monthKey]) { monthlyReturns[monthKey] = 1; } monthlyReturns[monthKey] *= (1 + ret.return); } return Object.values(monthlyReturns).map(cumReturn => cumReturn - 1); }