diff --git a/hunterretire b/hunterretire new file mode 100644 index 00000000..3c1d920a --- /dev/null +++ b/hunterretire @@ -0,0 +1,201 @@ + +import { useState } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + PieChart, + Pie, + Cell +} from "recharts"; + +const COLORS = ["#8884d8", "#82ca9d", "#ffc658"]; + +function compoundGrowth(pv, contrib, rate, years, contribGrowth = 0) { + let values = []; + let value = pv; + for (let i = 0; i <= years; i++) { + values.push({ + year: i, + nominal: Math.round(value), + }); + value = (value + contrib) * (1 + rate); + contrib *= (1 + contribGrowth); + } + return values; +} + +function simulateDrawdown(startAmount, withdraw, returnRate, inflationRate, startAge) { + const result = []; + let value = startAmount; + let totalWithdrawn = 0; + let age = startAge; + let year = 0; + while (value > 0 && year < 100) { + const realWithdraw = withdraw * Math.pow(1 + inflationRate, year); + totalWithdrawn += realWithdraw; + value = (value - realWithdraw) * (1 + returnRate); + result.push({ + age, + balance: Math.max(value, 0), + withdrawn: Math.round(totalWithdrawn) + }); + age++; + year++; + } + return result; +} + +function adjustForInflation(values, inflationRate) { + return values.map(v => ({ + ...v, + real: Math.round(v.nominal / Math.pow(1 + inflationRate, v.year)) + })); +} + +function adjustedAverages(inflationRate, yearsOffset = 0) { + const baseAverages = { + 30: 40000, + 40: 120000, + 50: 210000, + 60: 330000, + 70: 400000 + }; + return Object.entries(baseAverages).map(([age, value]) => { + const years = parseInt(age) - 30 + yearsOffset; + const adjusted = value * Math.pow(1 + inflationRate, years); + return { age: parseInt(age), average: Math.round(adjusted) }; + }); +} + +export default function RetirementApp() { + const [age, setAge] = useState(35); + const [retirementAge, setRetirementAge] = useState(65); + const [savings, setSavings] = useState(50000); + const [contrib, setContrib] = useState(10000); + const [returnRate, setReturnRate] = useState(0.05); + const [inflation, setInflation] = useState(0.02); + const [growth, setGrowth] = useState(0.02); + const [withdrawal, setWithdrawal] = useState(40000); + const [scenario, setScenario] = useState("base"); + + const scenarioRates = { + base: { returnRate: returnRate, contribGrowth: growth }, + optimistic: { returnRate: returnRate + 0.02, contribGrowth: growth + 0.01 }, + pessimistic: { returnRate: returnRate - 0.02, contribGrowth: growth - 0.01 }, + }[scenario]; + + const years = retirementAge - age; + const nominalData = compoundGrowth(savings, contrib, scenarioRates.returnRate, years, scenarioRates.contribGrowth); + const allData = adjustForInflation(nominalData, inflation).map((d, i) => ({ + age: age + i, + ...d + })); + const averages = adjustedAverages(inflation, age - 30); + + const finalReal = allData[allData.length - 1]?.real || 0; + const finalNominal = allData[allData.length - 1]?.nominal || 0; + const contribTotal = contrib * years; + const investmentGain = finalNominal - savings - contribTotal; + + const pieData = [ + { name: "Initial Savings", value: savings }, + { name: "Contributions", value: contribTotal }, + { name: "Growth", value: investmentGain } + ]; + + const drawdownData = simulateDrawdown(finalNominal, withdrawal, scenarioRates.returnRate, inflation, retirementAge); + + return ( +
+

Retirement Projection App

+ +
+ + + + + + + + + +
+ + {/* Scenario explanation block */} +
+

Scenario Definitions:

+ +
+ + + + + + + `$${value.toLocaleString()}`} /> + + + + + + +

Post-Retirement Drawdown Projection

+ + + + + + `$${value.toLocaleString()}`} /> + + + + + + +

Breakdown of Final Savings (Nominal)

+ + + + {pieData.map((entry, index) => ( + + ))} + + `$${value.toLocaleString()}`} /> + + + + +

US Averages (Inflation Adjusted)

+ +
+ ); +} +