UNPKG

@spaik/mcp-server-roi

Version:

MCP server for AI ROI prediction and tracking with Monte Carlo simulations

193 lines 7.86 kB
import { calculateNPV, calculateNPVDerivative, calculatePaybackPeriod, generateCashFlows as generateCashFlowsUtil, calculateBreakEvenDate as calculateBreakEvenDateUtil, FINANCIAL_CONSTANTS } from './financial-utils.js'; import { createLogger } from '../../utils/logger.js'; import { CalculationError } from '../../utils/errors.js'; import { validateRate } from '../../utils/validators.js'; export class FinancialCalculator { logger = createLogger({ component: 'FinancialCalculator' }); /** * Calculate Net Present Value (NPV) * @param cashFlows Array of cash flows (negative for costs, positive for benefits) * @param discountRate Monthly discount rate (e.g., 0.1/12 for 10% annual) */ calculateNPV(cashFlows, discountRate) { try { this.logger.debug('Calculating NPV', { cashFlowCount: cashFlows.length, discountRate }); // Validate inputs if (!Array.isArray(cashFlows) || cashFlows.length === 0) { throw new CalculationError('Cash flows must be a non-empty array'); } validateRate(discountRate, 'discountRate'); // Validate each cash flow cashFlows.forEach((flow, index) => { if (!isFinite(flow)) { throw new CalculationError(`Invalid cash flow at index ${index}`, { index, value: flow }); } }); const npv = calculateNPV(cashFlows, discountRate); if (!isFinite(npv)) { throw new CalculationError('NPV calculation resulted in non-finite value', { cashFlowsLength: cashFlows.length, discountRate }); } return npv; } catch (error) { this.logger.error('NPV calculation failed', error); throw error; } } /** * Calculate Internal Rate of Return (IRR) * Uses Newton's method for approximation */ calculateIRR(cashFlows, initialGuess = 0.1) { try { this.logger.debug('Calculating IRR', { cashFlowCount: cashFlows.length, initialGuess }); // Validate inputs if (!Array.isArray(cashFlows) || cashFlows.length === 0) { throw new CalculationError('Cash flows must be a non-empty array'); } // Check if all cash flows have the same sign (no IRR exists) const hasPositive = cashFlows.some(cf => cf > 0); const hasNegative = cashFlows.some(cf => cf < 0); if (!hasPositive || !hasNegative) { throw new CalculationError('IRR requires cash flows with different signs (both inflows and outflows)'); } const maxIterations = 100; const tolerance = 1e-6; let rate = initialGuess; let iterations = 0; for (let i = 0; i < maxIterations; i++) { iterations++; try { const npv = calculateNPV(cashFlows, rate); const npvDerivative = calculateNPVDerivative(cashFlows, rate); if (Math.abs(npvDerivative) < tolerance) { this.logger.warn('IRR calculation: derivative too small', { iteration: i, derivative: npvDerivative }); break; } let newRate = rate - npv / npvDerivative; // Bound the rate to prevent extreme values if (newRate < -0.99) { newRate = -0.99; } else if (newRate > 10) { newRate = 10; } if (Math.abs(newRate - rate) < tolerance) { this.logger.debug('IRR converged', { iterations: i + 1, finalRate: newRate }); return newRate; } rate = newRate; } catch (error) { this.logger.error('Error in IRR iteration', error, { iteration: i }); throw new CalculationError(`IRR calculation failed at iteration ${i}`, { iteration: i, rate, error: error.message }); } } // If we didn't converge, throw an error throw new CalculationError('IRR calculation did not converge', { iterations, lastRate: rate, tolerance }); } catch (error) { this.logger.error('IRR calculation failed', error); throw error; } } /** * Calculate payback period in months */ calculatePaybackPeriod(cashFlows) { return calculatePaybackPeriod(cashFlows); } /** * Calculate 5-year ROI percentage */ calculate5YearROI(totalCosts, monthlyBenefits, ongoingMonthlyCosts = 0) { // Validate inputs if (!isFinite(totalCosts) || totalCosts < 0) { this.logger.warn('Invalid totalCosts for ROI calculation', { totalCosts }); return 0; } if (!isFinite(monthlyBenefits) || monthlyBenefits < 0) { this.logger.warn('Invalid monthlyBenefits for ROI calculation', { monthlyBenefits }); return 0; } const totalBenefits = monthlyBenefits * FINANCIAL_CONSTANTS.FIVE_YEAR_MONTHS; const totalOngoingCosts = ongoingMonthlyCosts * FINANCIAL_CONSTANTS.FIVE_YEAR_MONTHS; const netBenefit = totalBenefits - totalCosts - totalOngoingCosts; const totalInvestment = totalCosts + totalOngoingCosts; // Log calculation details for debugging this.logger.debug('5-Year ROI Calculation', { totalCosts, monthlyBenefits, ongoingMonthlyCosts, totalBenefits, totalOngoingCosts, netBenefit, totalInvestment }); // Avoid division by zero - if no investment, return 0 or infinity based on benefits if (totalInvestment === 0) { return netBenefit > 0 ? Infinity : 0; } const roi = (netBenefit / totalInvestment) * 100; // Sanity check - ROI above 10,000% is likely an error if (roi > 10000) { this.logger.warn('Suspiciously high ROI calculated', { roi, totalCosts, monthlyBenefits, totalInvestment, netBenefit }); // Cap at a more reasonable maximum return 10000; } return roi; } /** * Generate monthly cash flows from costs and benefits */ generateCashFlows(initialInvestment, monthlyBenefits, timelineMonths, implementationMonths = FINANCIAL_CONSTANTS.DEFAULT_IMPLEMENTATION_MONTHS, rampUpMonths = FINANCIAL_CONSTANTS.DEFAULT_RAMP_UP_MONTHS, ongoingMonthlyCosts = 0) { const config = { initialInvestment, monthlyBenefits, timelineMonths, implementationMonths, rampUpMonths, ongoingMonthlyCosts }; return generateCashFlowsUtil(config); } /** * Calculate break-even date */ calculateBreakEvenDate(startDate, paybackPeriodMonths) { return calculateBreakEvenDateUtil(startDate, paybackPeriodMonths); } } //# sourceMappingURL=financial.js.map