Sequence of Returns Risk Simulator

Sequence of Returns Risk Simulator

Portfolio & Retirement Inputs

Age you plan to start withdrawals.
If current age < retirement age, this is grown by "Accumulation Phase Return".
Only applies if Current Age < Retirement Age.
Desired income in first year of retirement (today's dollars).
How long to simulate withdrawals after retirement.

Return & Risk Assumptions

Return before retirement age, if applicable.
How many years of poor returns at start of retirement.
Return after the initial poor years.

This simulator uses fixed average returns for normal/poor periods to illustrate Sequence of Returns Risk. Real markets are more variable.

Sequence of Returns Risk Simulation

Click "Run SoRR Simulation" to view results. Ensure all inputs are set.

Nest Egg lasted for the simulated ${data.inputs.retirementDuration} years.

`; html += `

Final Balance After ${data.inputs.retirementDuration} Years: ${formatCurrency(data.retirementSummary.finalBalance)}

`; } html += `

Total Withdrawn During Retirement: ${formatCurrency(data.retirementSummary.totalWithdrawn)}

`; html += `
`; // Accumulation Table (if applicable) if(data.accumulationTable.length > 0) { html += `

Accumulation Phase Projection (Up to Age ${data.inputs.retirementAge}):

`; html += `
`; html += ``; data.accumulationTable.forEach(row => { html += ``; }); html += `
YearAgeStart BalanceContributionsGrowthEnd Balance
${row.year}${row.age} ${formatCurrency(row.yearStartBalance)}${formatCurrency(row.savingsThisYear)} ${formatCurrency(row.investmentGrowth)}${formatCurrency(row.endBalance)}
`; } else { html += `

No accumulation phase simulated (Current Age is Retirement Age or older).

`; } // Decumulation Table if (data.decumulationTable.length > 0) { html += `

Retirement (Decumulation) Phase Projection:

`; html += `
`; html += ``; data.decumulationTable.forEach(row => { html += ``; }); html += `
Ret. YearAgeStart BalanceAnnual ReturnWithdrawalGrowthEnd Balance
${row.year}${row.age} ${formatCurrency(row.startBalance)} ${formatPercent(row.annualReturnUsed/100, 1)} ${formatCurrency(row.withdrawal)} ${formatCurrency(row.growth)} ${formatCurrency(row.endBalance)} ${row.notes && !row.notes.includes('Poor Return Year') ? `(${row.notes})` : ''}
`; } else { html += `

No decumulation phase to display.

`; } simulationResultsDiv.innerHTML = html; } if (runSimulationBtn) { runSimulationBtn.addEventListener('click', runSORRSimulation); } // --- PDF Download --- function loadJsPdfIfNeeded(callback) { if (jsPdfLoaded) { if (callback) callback(); return; } const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; script.onload = () => { jsPdfLoaded = true; console.log("jsPDF loaded dynamically."); if (callback) callback(); }; script.onerror = () => { console.error("Failed to load jsPDF."); alert("Error: Could not load PDF library."); }; document.head.appendChild(script); } function downloadResultsAsPdf() { if (!jsPdfLoaded) { alert("PDF library not loaded."); return; } if (!simulationDataForPdf) { alert("No simulation data to download. Please run the simulation first."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF({ unit: 'pt', format: 'a4' }); const data = simulationDataForPdf; const pageMargin = 40; const pageWidth = doc.internal.pageSize.getWidth() - 2 * pageMargin; let y = pageMargin; function addMainTitle(text) { doc.setFontSize(18); doc.setFont(undefined, 'bold'); doc.setTextColor(44, 62, 80); // Primary doc.text(text, doc.internal.pageSize.getWidth() / 2, y, { align: 'center' }); y += 35; } function addSectionTitle(text) { if (y > doc.internal.pageSize.getHeight() - 80) { doc.addPage(); y = pageMargin; } doc.setFontSize(14); doc.setFont(undefined, 'bold'); doc.setTextColor(52, 152, 219); // Secondary doc.text(text, pageMargin, y); y += 25; } function addLine(key, value, keyColor = [52,73,94], valueColor = [52,73,94]) { if (y > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); y = pageMargin; } doc.setFontSize(10); doc.setFont(undefined, 'bold'); doc.setTextColor(keyColor[0], keyColor[1], keyColor[2]); doc.text(key, pageMargin, y); doc.setFont(undefined, 'normal'); doc.setTextColor(valueColor[0], valueColor[1], valueColor[2]); const valueText = String(value); doc.text(valueText, pageMargin + 240, y, { align: 'left', maxWidth: pageWidth - 240 - 5 }); y += 18; } function addInfo(text, isError = false) { if (y > doc.internal.pageSize.getHeight() - 50) { doc.addPage(); y = pageMargin; } doc.setFontSize(8.5); doc.setFont(undefined, 'italic'); doc.setTextColor(isError ? 231 : 108, isError ? 76 : 120, isError ? 60 : 125); const splitText = doc.splitTextToSize(text, pageWidth); doc.text(splitText, pageMargin, y); y += (doc.getTextDimensions(splitText).h) + 10; } function addTable(headers, tableData, columnWidths) { if (y > doc.internal.pageSize.getHeight() - 60) { doc.addPage(); y = pageMargin; } doc.setFontSize(8); const headerFillColor = [52, 152, 219]; const headerTextColor = [255,255,255]; const rowTextColor = [52,73,94]; doc.setFillColor(headerFillColor[0], headerFillColor[1], headerFillColor[2]); doc.setTextColor(headerTextColor[0], headerTextColor[1], headerTextColor[2]); doc.setFont(undefined, 'bold'); let currentX = pageMargin; headers.forEach((header, i) => { doc.rect(currentX, y, columnWidths[i], 20, 'F'); doc.text(header, currentX + 5, y + 14); currentX += columnWidths[i]; }); y += 20; doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); doc.setFont(undefined, 'normal'); tableData.forEach((rowArray) => { if (y > doc.internal.pageSize.getHeight() - 35) { doc.addPage(); y = pageMargin; currentX = pageMargin; doc.setFillColor(headerFillColor[0], headerFillColor[1], headerFillColor[2]); doc.setTextColor(headerTextColor[0], headerTextColor[1], headerTextColor[2]); doc.setFont(undefined, 'bold'); headers.forEach((header, i) => { doc.rect(currentX, y, columnWidths[i], 20, 'F'); doc.text(header, currentX + 5, y + 14); currentX += columnWidths[i]; }); y += 20; doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); doc.setFont(undefined, 'normal'); } currentX = pageMargin; rowArray.forEach((cell, i) => { doc.rect(currentX, y, columnWidths[i], 18); const cellText = String(cell); // Check if cell is for "Annual Return" and if it's a poor return year let cellFillColor = null; if (headers[i] === "Ann. Return (%)" && cell.includes('-')) { // Simple check for negative return // Could also pass the raw 'notes' field for more precise poor year highlighting } if (cellText.toLowerCase().includes("depleted")) { doc.setTextColor(220, 53, 69); // Error color } else { doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); } const textLines = doc.splitTextToSize(cellText, columnWidths[i] - 8); doc.text(textLines, currentX + 4, y + 12); currentX += columnWidths[i]; }); doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); // Reset color y += 18; }); y += 10; } addMainTitle("Sequence of Returns Risk Simulation Report"); addInfo(`Report Generated: ${new Date().toLocaleString()}`); y += 10; addSectionTitle("Input Parameters"); addLine("Current Age:", `${data.inputs.currentAge} years`); addLine("Retirement Age:", `${data.inputs.retirementAge} years`); addLine("Initial Portfolio (at Current Age/Retirement):", formatCurrency(data.inputs.initialPortfolioAtRetirementInput)); if (data.inputs.currentAge < data.inputs.retirementAge) { addLine("Annual Savings (Pre-Retirement):", formatCurrency(data.inputs.annualSavings)); addLine("Avg. Annual Return (Accumulation):", formatPercent(data.inputs.accumulationReturnRatePct/100)); } addLine("Initial Annual Withdrawal:", formatCurrency(data.inputs.initialAnnualWithdrawal)); addLine("Retirement Duration Simulated:", `${data.inputs.retirementDuration} years`); addLine("Number of Initial Poor Return Years:", `${data.inputs.numPoorReturnYears} years`); addLine("Annual Return During Poor Years:", formatPercent(data.inputs.poorReturnRatePct/100)); addLine("Avg. Annual Return (Normal Retirement Years):", formatPercent(data.inputs.normalReturnRatePct/100)); addLine("Expected Avg. Annual Inflation:", formatPercent(data.inputs.inflationRatePct/100)); addLine("Adjust Withdrawals for Inflation:", data.inputs.adjustWithdrawalsForInflation ? "Yes" : "No"); y += 10; addSectionTitle("Simulation Summary"); addLine("Portfolio Value at Retirement Start (Age " + data.inputs.retirementAge + "):", formatCurrency(data.accumulationSummary.portfolioAtRetirementActualStart)); addLine("Actual Initial Withdrawal Rate:", `${data.retirementSummary.actualInitialSWR.toFixed(2)}%`); if (data.retirementSummary.fundsDepletedYear !== null && data.retirementSummary.fundsDepletedYear <= data.inputs.retirementDuration) { addLine("Portfolio Longevity:", `Depleted in Year ${data.retirementSummary.fundsDepletedYear} of retirement`,[220,53,69],[220,53,69]); addLine("Age at Depletion:", `${data.inputs.retirementAge + data.retirementSummary.fundsDepletedYear -1} years old`); } else { addLine("Portfolio Longevity:", `Lasted for simulated ${data.inputs.retirementDuration} years`,[40,167,69],[40,167,69]); addLine("Final Balance After "+data.inputs.retirementDuration+" Years:", formatCurrency(data.retirementSummary.finalBalance)); } addLine("Total Withdrawn During Retirement:", formatCurrency(data.retirementSummary.totalWithdrawn)); y += 10; if (data.accumulationTable.length > 0) { addSectionTitle("Accumulation Phase Projection (Up to Age " + data.inputs.retirementAge + ")"); const accHeaders = ["Year", "Age", "Start Bal.", "Savings", "Growth", "End Bal."]; const accColWidths = [35, 35, 100, 80, 80, 100]; const accTableFormattedData = data.accumulationTable.map(r => [ r.year, r.age, formatCurrency(r.yearStartBalance,0,0), formatCurrency(r.savingsThisYear,0,0), formatCurrency(r.investmentGrowth,0,0), formatCurrency(r.endBalance,0,0) ]); addTable(accHeaders, accTableFormattedData, accColWidths); } if (data.decumulationTable.length > 0) { addSectionTitle("Retirement (Decumulation) Phase Projection"); const decHeaders = ["Ret. Yr", "Age", "Start Bal.", "Ann. Return (%)", "Withdrawal", "Growth", "End Bal."]; const decColWidths = [45, 35, 90, 65, 80, 80, 95]; const decTableFormattedData = data.decumulationTable.map(r => [ r.year, r.age, formatCurrency(r.startBalance,0,0), `${r.annualReturnUsed.toFixed(1)}%`, // Annual Return Used formatCurrency(r.withdrawal,0,0), formatCurrency(r.growth,0,0), formatCurrency(r.endBalance,0,0) + (r.notes && !r.notes.includes('Poor Return Year') ? ` (${r.notes.substring(0,10)}..)` : '') ]); addTable(decHeaders, decTableFormattedData, decColWidths); } addInfo("Disclaimer: This simulation uses simplified assumptions for returns and does not account for full market volatility, taxes, or fees. It is for illustrative and educational purposes only and should not be considered financial advice. Consult with a qualified financial advisor for personalized retirement planning."); // Footer const pageCount = doc.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setFontSize(8); doc.setTextColor(150); doc.text(`Page ${i} of ${pageCount} - Sequence of Returns Risk Simulator`, pageMargin, doc.internal.pageSize.getHeight() - 20); } doc.save('Sequence_of_Returns_Risk_Simulation.pdf'); } if (downloadPdfBtn) { downloadPdfBtn.addEventListener('click', () => loadJsPdfIfNeeded(downloadResultsAsPdf)); } // --- Initialization --- showTab(0); loadJsPdfIfNeeded(); });
Scroll to Top