import { MoneyWeightedReturnOptions, MoneyWeightedReturnOptionsSchema } from '../schemas/MoneyWeightedReturnOptionsSchema'; import { MoneyWeightedReturnResult, MoneyWeightedReturnResultSchema } from '../schemas/MoneyWeightedReturnResultSchema'; import { calculateIRR } from '../utils/irr/calculateIRR'; import { calculateNPV } from '../utils/irr/calculateNPV'; /** * Calculate Money-Weighted Return (MWR) using Internal Rate of Return (IRR) * * MWR measures the actual return earned by an investor based on their * specific cash flow timing and amounts. It's also known as Internal Rate of Return. * * The MWR is the discount rate that makes the NPV of all cash flows equal to zero: * NPV = CF₀ + CF₁/(1+r) + CF₂/(1+r)² + ... + CFₙ/(1+r)ⁿ = 0 * * @param options - Cash flows, dates, final value, and IRR calculation parameters * @returns MWR result with period and annualized returns * * @example * ```typescript * const mwr = calculateMoneyWeightedReturn({ * cashFlows: [-1000, 100, -50], * dates: [new Date('2023-01-01'), new Date('2023-06-01'), new Date('2023-12-01')], * finalValue: 1200, * initialValue: 0 * }); * ``` */ export function calculateMoneyWeightedReturn( options: MoneyWeightedReturnOptions ): MoneyWeightedReturnResult { const { cashFlows, dates, finalValue, initialValue, maxIterations, tolerance } = MoneyWeightedReturnOptionsSchema.parse(options); if (cashFlows.length !== dates.length) { throw new Error('Cash flows and dates must have same length'); } if (cashFlows.length < 2) { throw new Error('At least 2 cash flows required for MWR calculation'); } // Create all cash flows including initial and final values const allCashFlows: number[] = []; const allDates: Date[] = []; // Add initial value if provided if (initialValue > 0) { allCashFlows.push(-initialValue); // Negative for initial investment allDates.push(dates[0]); // Use first date as initial date } // Add intermediate cash flows for (let i = 0; i < cashFlows.length; i++) { allCashFlows.push(cashFlows[i]); allDates.push(dates[i]); } // Add final value as positive cash flow // If final value is on the same date as the last intermediate cash flow, combine them const lastDate = dates[dates.length - 1]; const lastDateInAllDates = allDates[allDates.length - 1]; if (lastDate.getTime() === lastDateInAllDates.getTime()) { // Combine final value with last intermediate cash flow at the same date allCashFlows[allCashFlows.length - 1] += finalValue; } else { // Add final value as separate cash flow at its own date allCashFlows.push(finalValue); allDates.push(lastDate); } // Calculate time periods in years from first date const startDate = allDates[0]; const timePeriods: number[] = allDates.map(date => { const diffTime = date.getTime() - startDate.getTime(); return diffTime / (1000 * 60 * 60 * 24 * 365.25); // Convert to years }); // Calculate IRR using improved Newton-Raphson with Bisection fallback const irrResult = calculateIRR(allCashFlows, timePeriods, maxIterations, tolerance); // Calculate total time period const totalTimeYears = timePeriods[timePeriods.length - 1]; // Annualize MWR const annualizedMWR = Math.pow(1 + irrResult.rate, 1 / totalTimeYears) - 1; // Calculate NPV at the found rate const npv = calculateNPV(allCashFlows, timePeriods, irrResult.rate); return MoneyWeightedReturnResultSchema.parse({ mwr: irrResult.rate, annualizedMWR, cashFlowCount: allCashFlows.length, timePeriodYears: totalTimeYears, npv, iterations: irrResult.iterations, method: irrResult.method, }); }