new algorithm

This commit is contained in:
Felix Schulze 2025-04-29 18:45:58 +02:00
parent 896b0bf063
commit f05f3fe37c

View File

@ -40,7 +40,12 @@ const formSchema = z.object({
desiredMonthlyAllowance: z.coerce
.number()
.min(0, "Monthly allowance must be a non-negative number"),
swr: z.coerce.number().min(0.1, "Withdrawal rate must be at least 0.1%"),
inflationRate: z.coerce
.number()
.min(0, "Inflation rate must be a non-negative number"),
lifeExpectancy: z.coerce
.number()
.min(50, "Life expectancy must be at least 50"),
});
// Type for form values
@ -49,6 +54,8 @@ type FormValues = z.infer<typeof formSchema>;
interface CalculationResult {
fireNumber: number | null;
retirementAge: number | null;
inflationAdjustedAllowance: number | null;
retirementYears: number | null;
error?: string;
}
@ -59,68 +66,170 @@ export default function FireCalculatorForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
startingCapital: 10000,
monthlySavings: 500,
currentAge: 30,
startingCapital: 50000,
monthlySavings: 1500,
currentAge: 25,
cagr: 7,
desiredMonthlyAllowance: 2000,
swr: 4,
inflationRate: 2,
lifeExpectancy: 90,
},
});
function onSubmit(values: FormValues) {
setResult(null); // Reset previous results
const sc = values.startingCapital;
const ms = values.monthlySavings;
const ca = values.currentAge;
const annualRate = values.cagr / 100;
const monthlyAllowance = values.desiredMonthlyAllowance;
const safeWithdrawalRate = values.swr / 100;
const startingCapital = values.startingCapital;
const monthlySavings = values.monthlySavings;
const currentAge = values.currentAge;
const annualGrowthRate = values.cagr / 100;
const initialMonthlyAllowance = values.desiredMonthlyAllowance;
const annualInflation = values.inflationRate / 100;
const lifeExpectancy = values.lifeExpectancy;
// Calculate FIRE number (the amount needed for retirement)
const fireNumber = (monthlyAllowance * 12) / safeWithdrawalRate;
const monthlyGrowthRate = Math.pow(1 + annualGrowthRate, 1 / 12) - 1;
const monthlyInflationRate = Math.pow(1 + annualInflation, 1 / 12) - 1;
const maxIterations = 1000; // Safety limit for iterations
let currentCapital = sc;
let age = ca;
const monthlyRate = Math.pow(1 + annualRate, 1 / 12) - 1;
const maxYears = 100; // Set a limit to prevent infinite loops
// Binary search for the required retirement capital
let low = initialMonthlyAllowance * 12; // Minimum: one year of expenses
let high = initialMonthlyAllowance * 12 * 100; // Maximum: hundred years of expenses
let requiredCapital = 0;
let retirementAge = 0;
let finalInflationAdjustedAllowance = 0;
if (currentCapital >= fireNumber) {
setResult({ fireNumber, retirementAge: age });
return;
}
// First, find when retirement is possible with accumulation phase
let canRetire = false;
let currentCapital = startingCapital;
let age = currentAge;
let monthlyAllowance = initialMonthlyAllowance;
let iterations = 0;
for (let year = 0; year < maxYears; year++) {
const capitalAtYearStart = currentCapital;
// Accumulation phase simulation
while (age < lifeExpectancy && iterations < maxIterations) {
// Simulate one year of saving and growth
for (let month = 0; month < 12; month++) {
currentCapital += ms;
currentCapital *= 1 + monthlyRate;
currentCapital += monthlySavings;
currentCapital *= 1 + monthlyGrowthRate;
// Update allowance for inflation
monthlyAllowance *= 1 + monthlyInflationRate;
}
age++;
iterations++;
if (currentCapital >= fireNumber) {
setResult({ fireNumber, retirementAge: age });
return;
// Check each possible retirement capital target through binary search
const mid = (low + high) / 2;
if (high - low < 1) {
// Binary search converged
requiredCapital = mid;
break;
}
// Prevent infinite loop if savings don't outpace growth required
if (currentCapital <= capitalAtYearStart && ms <= 0) {
setResult({
fireNumber: null,
retirementAge: null,
error: "Cannot reach FIRE goal with current savings and growth rate.",
});
return;
// Test if this retirement capital is sufficient
let testCapital = mid;
let testAge = age;
let testAllowance = monthlyAllowance;
let isSufficient = true;
// Simulate retirement phase with this capital
while (testAge < lifeExpectancy) {
for (let month = 0; month < 12; month++) {
// Withdraw inflation-adjusted allowance
testCapital -= testAllowance;
// Grow remaining capital
testCapital *= 1 + monthlyGrowthRate;
// Adjust allowance for inflation
testAllowance *= 1 + monthlyInflationRate;
}
testAge++;
// Check if we've depleted capital before life expectancy
if (testCapital <= 0) {
isSufficient = false;
break;
}
}
// If loop finishes without reaching FIRE number
if (isSufficient) {
high = mid; // This capital or less might be enough
if (currentCapital >= mid) {
// We can retire now with this capital
canRetire = true;
retirementAge = age;
requiredCapital = mid;
finalInflationAdjustedAllowance = monthlyAllowance;
break;
}
} else {
low = mid; // We need more capital
}
}
// If we didn't find retirement possible in the loop
if (!canRetire && iterations < maxIterations) {
// Continue accumulation phase until we reach sufficient capital
while (age < lifeExpectancy && iterations < maxIterations) {
// Simulate one year
for (let month = 0; month < 12; month++) {
currentCapital += monthlySavings;
currentCapital *= 1 + monthlyGrowthRate;
monthlyAllowance *= 1 + monthlyInflationRate;
}
age++;
iterations++;
// Test with current capital
let testCapital = currentCapital;
let testAge = age;
let testAllowance = monthlyAllowance;
let isSufficient = true;
// Simulate retirement with current capital
while (testAge < lifeExpectancy) {
for (let month = 0; month < 12; month++) {
testCapital -= testAllowance;
testCapital *= 1 + monthlyGrowthRate;
testAllowance *= 1 + monthlyInflationRate;
}
testAge++;
if (testCapital <= 0) {
isSufficient = false;
break;
}
}
if (isSufficient) {
canRetire = true;
retirementAge = age;
requiredCapital = currentCapital;
finalInflationAdjustedAllowance = monthlyAllowance;
break;
}
}
}
if (canRetire) {
setResult({
fireNumber: requiredCapital,
retirementAge: retirementAge,
inflationAdjustedAllowance: finalInflationAdjustedAllowance,
retirementYears: lifeExpectancy - retirementAge,
error: undefined,
});
} else {
setResult({
fireNumber: null,
retirementAge: null,
error: `Could not reach FIRE goal within ${maxYears.toString()} years.`,
inflationAdjustedAllowance: null,
retirementYears: null,
error:
iterations >= maxIterations
? "Calculation exceeded maximum iterations."
: "Cannot reach FIRE goal before life expectancy with current parameters.",
});
}
}
// Helper function to format currency
const formatCurrency = (value: number | null) => {
@ -218,7 +327,7 @@ export default function FireCalculatorForm() {
render={({ field }) => (
<FormItem>
<FormLabel>
Desired Monthly Allowance in Retirement
Desired Monthly Allowance (Today&apos;s Value)
</FormLabel>
<FormControl>
<Input
@ -233,13 +342,13 @@ export default function FireCalculatorForm() {
/>
<FormField
control={form.control}
name="swr"
name="inflationRate"
render={({ field }) => (
<FormItem>
<FormLabel>Safe Withdrawal Rate (%)</FormLabel>
<FormLabel>Annual Inflation Rate (%)</FormLabel>
<FormControl>
<Input
placeholder="e.g., 4"
placeholder="e.g., 2"
type="number"
step="0.1"
{...field}
@ -249,6 +358,23 @@ export default function FireCalculatorForm() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="lifeExpectancy"
render={({ field }) => (
<FormItem>
<FormLabel>Life Expectancy (Age)</FormLabel>
<FormControl>
<Input
placeholder="e.g., 90"
type="number"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" className="w-full">
@ -281,6 +407,24 @@ export default function FireCalculatorForm() {
{result.retirementAge ?? "N/A"}
</p>
</div>
{result.inflationAdjustedAllowance && (
<div>
<Label>
Monthly Allowance at Retirement (Inflation Adjusted)
</Label>
<p className="text-2xl font-bold">
{formatCurrency(result.inflationAdjustedAllowance)}
</p>
</div>
)}
{result.retirementYears && (
<div>
<Label>Retirement Duration (Years)</Label>
<p className="text-2xl font-bold">
{result.retirementYears}
</p>
</div>
)}
</div>
)}
</CardContent>