new algorithm
This commit is contained in:
parent
896b0bf063
commit
f05f3fe37c
@ -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,67 +66,169 @@ 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 (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 loop finishes without reaching FIRE number
|
// If we didn't find retirement possible in the loop
|
||||||
setResult({
|
if (!canRetire && iterations < maxIterations) {
|
||||||
fireNumber: null,
|
// Continue accumulation phase until we reach sufficient capital
|
||||||
retirementAge: null,
|
while (age < lifeExpectancy && iterations < maxIterations) {
|
||||||
error: `Could not reach FIRE goal within ${maxYears.toString()} years.`,
|
// 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,
|
||||||
|
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
|
||||||
@ -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'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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user