UNPKG

financial-calcs

Version:

Reusable financial calculation library for FERS, Social Security, retirement savings, and mortgage amortization

139 lines 6.08 kB
export function validateRetirementSavingsInput(input) { const errors = []; const { startYear, birthYear, initialBalance, initialContribution, estimatedYield, estimatedWithdrawRate, contributionIncreaseRate, withdrawStartAge, yearsToProject, } = input; if (startYear < 1900) errors.push({ field: "startYear", message: "Start Year cannot be before 1900" }); if (birthYear < 1900) errors.push({ field: "birthYear", message: "Birth Year cannot be before 1900" }); if (initialBalance < 0) errors.push({ field: "initialBalance", message: "Initial balance cannot be negative" }); if (initialContribution < 0) errors.push({ field: "initialContribution", message: "Contribution cannot be negative" }); if (estimatedYield < -100) errors.push({ field: "estimatedYield", message: "Estimated yield cannot be less than -100%" }); if (estimatedWithdrawRate < 0) errors.push({ field: "estimatedWithdrawRate", message: "Withdrawal rate cannot be negative" }); if (contributionIncreaseRate < -100) errors.push({ field: "contributionIncreaseRate", message: "Contribution increase rate cannot be less than -100%" }); if (withdrawStartAge < 0 || withdrawStartAge > 80) errors.push({ field: "withdrawStartAge", message: "Withdraw start age must be between 0 and 80" }); if (yearsToProject <= 0) errors.push({ field: "yearsToProject", message: "Must project at least 1 year" }); return errors; } // --- Main Projection --- export function calculateRetirementSavingsProjection(input) { return calculateRetirementSavingsProjectionWithOverrides({ ...input, yearOverrides: {} }); } export function calculateRetirementSavingsProjectionWithOverrides(input) { const { startYear, birthYear, initialBalance, initialContribution, estimatedYield, estimatedWithdrawRate, contributionIncreaseRate, withdrawStartAge, yearsToProject, yearOverrides = {} } = input; const errors = validateRetirementSavingsInput(input); if (errors.length > 0) { const err = new Error("Retirement Savings input validation failed"); err.validationErrors = errors; throw err; } let balance = Math.max(initialBalance, 0); let contribution = Math.max(initialContribution, 0); const rows = []; for (let i = 0; i < yearsToProject; i++) { const year = startYear + i; const age = year - birthYear; const isWithdrawing = age >= withdrawStartAge; const override = yearOverrides[year] || {}; const hasOverride = override.contribution !== undefined || override.yieldPercent !== undefined || override.withdrawRate !== undefined || override.annualWithdraw !== undefined || override.endingBalance !== undefined; const beginningBalance = balance; // // ----- Contribution ----- // if (override.contribution !== undefined) { contribution = Math.max(override.contribution, 0); } else { if (i > 0) { contribution = isWithdrawing ? 0 : contribution * (1 + contributionIncreaseRate / 100); } else if (isWithdrawing) { contribution = 0; } } // // ----- Withdrawal logic ----- // let annualWithdraw = 0; let withdrawRate = 0; if (isWithdrawing) { if (override.annualWithdraw !== undefined) { annualWithdraw = Math.max(override.annualWithdraw, 0); withdrawRate = beginningBalance > 0 ? (annualWithdraw / beginningBalance) * 100 : 0; } else if (override.withdrawRate !== undefined) { withdrawRate = override.withdrawRate; annualWithdraw = (withdrawRate / 100) * beginningBalance; } else { withdrawRate = estimatedWithdrawRate; annualWithdraw = (withdrawRate / 100) * beginningBalance; } } // // ----- Yield logic ----- // let yieldPercent = override.yieldPercent ?? estimatedYield; let yieldAmount = (yieldPercent / 100) * beginningBalance; // // ----- Normal ending balance (before override adjustments) ----- // let endingBalance = beginningBalance + yieldAmount + contribution - annualWithdraw; // // ----- endingBalance Override (Highest precedence) ----- // if (override.endingBalance !== undefined) { const forcedEnding = Math.max(override.endingBalance, 0); // Solve for yieldPercent needed to reach forced ending // ending = beg + (beg * y%) + contrib − withdraw // → y% = (ending - beg - contrib + withdraw) / beg * 100 if (beginningBalance > 0) { yieldPercent = ((forcedEnding - beginningBalance - contribution + annualWithdraw) / beginningBalance) * 100; yieldAmount = (yieldPercent / 100) * beginningBalance; } else { // If beginningBalance is zero, yield cannot influence outcome yieldPercent = 0; yieldAmount = 0; } endingBalance = forcedEnding; } const monthlyWithdraw = annualWithdraw / 12; rows.push({ year, age, beginningBalance, contribution, yieldPercent: Math.round(yieldPercent * 100) / 100, withdrawRate: Math.round(withdrawRate * 100) / 100, monthlyWithdraw, annualWithdraw, endingBalance, hasOverride }); balance = endingBalance; } return rows; } //# sourceMappingURL=savings.js.map