How the Decision tools retirement and fire planner calculator is verified
Inputs
| Input | Field | Type |
|---|---|---|
| Your age | currentAge | integer |
| Invested today | currentPortfolio | number |
| Take-home income / yr | annualIncome | number |
| Spending / yr | annualSpending | number |
| Retirement spending / yr (today's money) | retirementSpending | number |
| Nominal return per year (%) | nominalReturnPct | number |
| Inflation per year (%) | inflationPct | number |
| Safe withdrawal rate (%) | swrPct | number |
| State or workplace pension / yr | pensionAnnual | number |
| Target retirement age (for Coast FI) | targetRetirementAge | integer |
| Project this many years | horizonYears | integer |
The formula
Each derived value below is computed by the Valem engine from the expression shown, in this order. Nothing else runs — there is no hidden code path.
| Derived value | Field | Formula (JSONata) |
|---|---|---|
| Saved per year | annualContribution | $round(annualIncome - annualSpending, 2) |
| Savings rate | savingsRatePct | annualIncome <= 0 ? 0 : $round((annualIncome - annualSpending) / annualIncome * 100, 1) |
| Real return | realReturnPct | $round(((1 + nominalReturnPct / 100) / (1 + inflationPct / 100) - 1) * 100, 4) |
| The number you need | fiNumber | $max([0, $round((retirementSpending - pensionAnnual) / (swrPct / 100), 2)]) |
| Coast FI number | coastNumber | ( $n := targetRetirementAge - currentAge; $n <= 0 ? fiNumber : $round(fiNumber / $power(1 + realReturnPct / 100, $n), 2) ) |
| Portfolio against the number you need (today's money) | projection | $map([0..horizonYears], function($y) { { "year": $y, "age": currentAge + $y, "contributed": $round(currentPortfolio + annualContribution * $y, 2), "portfolio": $round($grow(currentPortfolio, annualContribution, realReturnPct / 100, $y), 2), "target": fiNumber } }) |
| Financially independent at | fiAge | ( $hits := $map(projection, function($p) { $p.portfolio >= $p.target ? $p.age : 0 })[$ > 0]; $count($hits) = 0 ? 0 : $min($hits) ) |
| Years to get there | yearsToFi | fiAge > 0 ? fiAge - currentAge : 0 |
| Fi Reached | fiReached | fiAge > 0 |
| Final Portfolio | finalPortfolio | ( $p := $map(projection, function($r) { $r.portfolio }); $i := $count($p) - 1; $p[$i] ) |
| Income at the end | sustainableWithdrawal | $round(finalPortfolio * swrPct / 100, 2) |
Test vectors (4)
Every case is executed against this model on each build, by the Java engine and independently by a JavaScript one. A mismatch of a single cent fails the build, so the page cannot ship advertising a figure the model no longer produces.
Case 1 — age 30 with 100k invested, saving 40k of 100k, 7% nominal against 3% inflation, 4% withdrawal rate
given currentAge | 30 |
|---|---|
given currentPortfolio | 100000 |
given annualIncome | 100000 |
given annualSpending | 60000 |
given retirementSpending | 60000 |
given nominalReturnPct | 7 |
given inflationPct | 3 |
given swrPct | 4 |
expect annualContribution | 40000 |
expect savingsRatePct | 40 |
expect realReturnPct | 3.8835 |
expect fiNumber | 1500000 |
expect coastNumber | 478293.04 |
expect projection[0].portfolio | 100000 |
expect projection[1].portfolio | 143883.5 |
expect projection[22].portfolio | 1582779.59 |
expect fiAge | 52 |
expect yearsToFi | 22 |
expect fiReached | true |
expect finalPortfolio | 4157295.48 |
expect sustainableWithdrawal | 166291.82 |
Case 2 — saving 60% instead of 40% brings independence forward five years
given annualSpending | 40000 |
|---|---|
expect annualContribution | 60000 |
expect savingsRatePct | 60 |
expect fiNumber | 1500000 |
expect fiAge | 47 |
expect yearsToFi | 17 |
Case 3 — a 12,000 pension cuts the number you need by exactly the pension capitalised at the withdrawal rate
given pensionAnnual | 12000 |
|---|---|
expect fiNumber | 1200000 |
expect coastNumber | 382634.43 |
expect fiAge | 48 |
expect yearsToFi | 18 |
Case 4 — spending everything you earn — the pot still compounds, but never reaches the target in the horizon
given annualSpending | 100000 |
|---|---|
expect annualContribution | 0 |
expect savingsRatePct | 0 |
expect fiAge | 0 |
expect fiReached | false |
expect yearsToFi | 0 |
expect finalPortfolio | 459053.11 |
Sources
Check it yourself — and reuse it
The whole model as JSON — inputs, outputs, formulas, vectors and sources in one re-runnable document.
This model is published under CC BY 4.0: use it, adapt it, build on it, with attribution. The licence covers the model — the expression of each rule and the arrangement of the catalog. The rates and thresholds are law, and nobody licenses those; the sources above are where they come from.
← Back to the Decision tools retirement and fire planner calculator
Built with Valem — a live, verifiable calculator you can fork and embed.