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 desiredMonthlyAllowance: z.coerce
.number() .number()
.min(0, "Monthly allowance must be a non-negative 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 // Type for form values
@ -49,6 +54,8 @@ type FormValues = z.infer<typeof formSchema>;
interface CalculationResult { interface CalculationResult {
fireNumber: number | null; fireNumber: number | null;
retirementAge: number | null; retirementAge: number | null;
inflationAdjustedAllowance: number | null;
retirementYears: number | null;
error?: string; error?: string;
} }
@ -59,68 +66,170 @@ export default function FireCalculatorForm() {
const form = useForm<FormValues>({ const form = useForm<FormValues>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
startingCapital: 10000, startingCapital: 50000,
monthlySavings: 500, monthlySavings: 1500,
currentAge: 30, currentAge: 25,
cagr: 7, cagr: 7,
desiredMonthlyAllowance: 2000, desiredMonthlyAllowance: 2000,
swr: 4, inflationRate: 2,
lifeExpectancy: 90,
}, },
}); });
function onSubmit(values: FormValues) { function onSubmit(values: FormValues) {
setResult(null); // Reset previous results setResult(null); // Reset previous results
const sc = values.startingCapital; const startingCapital = values.startingCapital;
const ms = values.monthlySavings; const monthlySavings = values.monthlySavings;
const ca = values.currentAge; const currentAge = values.currentAge;
const annualRate = values.cagr / 100; const annualGrowthRate = values.cagr / 100;
const monthlyAllowance = values.desiredMonthlyAllowance; const initialMonthlyAllowance = values.desiredMonthlyAllowance;
const safeWithdrawalRate = values.swr / 100; const annualInflation = values.inflationRate / 100;
const lifeExpectancy = values.lifeExpectancy;
// Calculate FIRE number (the amount needed for retirement) const monthlyGrowthRate = Math.pow(1 + annualGrowthRate, 1 / 12) - 1;
const fireNumber = (monthlyAllowance * 12) / safeWithdrawalRate; const monthlyInflationRate = Math.pow(1 + annualInflation, 1 / 12) - 1;
const maxIterations = 1000; // Safety limit for iterations
let currentCapital = sc; // Binary search for the required retirement capital
let age = ca; let low = initialMonthlyAllowance * 12; // Minimum: one year of expenses
const monthlyRate = Math.pow(1 + annualRate, 1 / 12) - 1; let high = initialMonthlyAllowance * 12 * 100; // Maximum: hundred years of expenses
const maxYears = 100; // Set a limit to prevent infinite loops let requiredCapital = 0;
let retirementAge = 0;
let finalInflationAdjustedAllowance = 0;
if (currentCapital >= fireNumber) { // First, find when retirement is possible with accumulation phase
setResult({ fireNumber, retirementAge: age }); let canRetire = false;
return; let currentCapital = startingCapital;
} let age = currentAge;
let monthlyAllowance = initialMonthlyAllowance;
let iterations = 0;
for (let year = 0; year < maxYears; year++) { // Accumulation phase simulation
const capitalAtYearStart = currentCapital; while (age < lifeExpectancy && iterations < maxIterations) {
// Simulate one year of saving and growth
for (let month = 0; month < 12; month++) { for (let month = 0; month < 12; month++) {
currentCapital += ms; currentCapital += monthlySavings;
currentCapital *= 1 + monthlyRate; currentCapital *= 1 + monthlyGrowthRate;
// Update allowance for inflation
monthlyAllowance *= 1 + monthlyInflationRate;
} }
age++; age++;
iterations++;
if (currentCapital >= fireNumber) { // Check each possible retirement capital target through binary search
setResult({ fireNumber, retirementAge: age }); const mid = (low + high) / 2;
return; 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) { // Test if this retirement capital is sufficient
setResult({ let testCapital = mid;
fireNumber: null, let testAge = age;
retirementAge: null, let testAllowance = monthlyAllowance;
error: "Cannot reach FIRE goal with current savings and growth rate.", let isSufficient = true;
});
return; // 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({ setResult({
fireNumber: null, fireNumber: null,
retirementAge: 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 // Helper function to format currency
const formatCurrency = (value: number | null) => { const formatCurrency = (value: number | null) => {
@ -218,7 +327,7 @@ export default function FireCalculatorForm() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
Desired Monthly Allowance in Retirement Desired Monthly Allowance (Today&apos;s Value)
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@ -233,13 +342,13 @@ export default function FireCalculatorForm() {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="swr" name="inflationRate"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Safe Withdrawal Rate (%)</FormLabel> <FormLabel>Annual Inflation Rate (%)</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="e.g., 4" placeholder="e.g., 2"
type="number" type="number"
step="0.1" step="0.1"
{...field} {...field}
@ -249,6 +358,23 @@ export default function FireCalculatorForm() {
</FormItem> </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> </div>
<Button type="submit" className="w-full"> <Button type="submit" className="w-full">
@ -281,6 +407,24 @@ export default function FireCalculatorForm() {
{result.retirementAge ?? "N/A"} {result.retirementAge ?? "N/A"}
</p> </p>
</div> </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> </div>
)} )}
</CardContent> </CardContent>