313 lines
13 KiB
Markdown
313 lines
13 KiB
Markdown
# Plan for retirement
|
|
|
|
**Goal:** configure a `projections.srf` and read the output to answer
|
|
the two questions that matter -- "given my retirement date, what can I
|
|
spend?" and "given my desired spending, when can I retire?"
|
|
|
|
This guide uses the five bundled households. Each is fully configured;
|
|
run them and compare:
|
|
|
|
```bash
|
|
ZFIN_HOME=examples/pre-retirement-both zfin projections
|
|
```
|
|
|
|
For the field-by-field file format, see the
|
|
[`projections.srf` reference](../reference/config/projections-srf.md);
|
|
for how the simulation works under the hood, see
|
|
[The retirement projection model](../explanation/projections-model.md).
|
|
|
|
## Why this is the payoff
|
|
|
|
Everything you've recorded -- lots, accounts, contributions -- feeds the
|
|
question most tools answer badly: *can I actually retire, and on what?*
|
|
zfin answers it by **replaying history**. The engine implements the
|
|
[FIRECalc](https://firecalc.com/) algorithm over Robert Shiller's market
|
|
dataset going back to **1871**: it runs your portfolio through every
|
|
historical starting year as a separate retirement -- 1871, 1872, ... --
|
|
including the cohorts who retired into 1929, 1966, or 2000. The spread
|
|
of those real outcomes is exactly what becomes the percentile bands and
|
|
safe-withdrawal numbers below.
|
|
|
|
Because it's the same method over the same dataset, results track
|
|
[FIRECalc.com](https://firecalc.com/) closely -- within roughly +6-9% on
|
|
safe-withdrawal dollars, in the slightly-optimistic direction (a June
|
|
2026 audit pinned the gap to zfin's equity-return series and made it a
|
|
regression suite). For the full method, its assumptions, and the parity
|
|
evidence, see
|
|
[The retirement projection model](../explanation/projections-model.md).
|
|
|
|
## Keeping the Shiller data current
|
|
|
|
That historical dataset is **compiled into the binary** (from
|
|
`src/data/ie_data.csv`), not fetched at runtime -- so it's one of the
|
|
few things in zfin that needs a periodic refresh. Once each year's
|
|
final market and CPI numbers are published, the dataset should be
|
|
updated to add that year. zfin tracks this for you: once the dataset is
|
|
overdue, **every command** (CLI and TUI) prints a one-line reminder to
|
|
stderr, above its normal output -- so you don't have to go looking for
|
|
it. [`zfin doctor`](../reference/cli/doctor.md) also reports it in its
|
|
Environment section (a `WARN` instead of `OK`, with the date it was
|
|
last updated).
|
|
|
|
Because the data is embedded, refreshing it means updating the
|
|
**binary**, not clearing a cache:
|
|
|
|
- **Built from source:** pull the latest repo and `zig build` -- the
|
|
refreshed dataset recompiles in.
|
|
- **Pre-built binary:** install a newer release.
|
|
|
|
It's not urgent -- stale data just means you're missing the most recent
|
|
year of history, and projections still run -- so treat it as "refresh
|
|
when convenient."
|
|
|
|
## The two questions
|
|
|
|
zfin runs a two-phase historical Monte Carlo: an **accumulation** phase
|
|
(contributions in, no spending) followed by a **distribution** phase
|
|
(spending out, no contributions). What you put in `projections.srf`
|
|
decides which question the display answers:
|
|
|
|
| You configure | zfin answers | Try the example |
|
|
|----------------------------------------------------------|-------------------------------|---------------------------|
|
|
| A retirement **date** (`retirement_age`/`retirement_at`) | "What can I spend?" | `pre-retirement-age` |
|
|
| A target **spending** (`target_spending`) | "When can I retire?" | `pre-retirement-spending` |
|
|
| **Both** | both, side by side | `pre-retirement-both` |
|
|
| **Neither** | already-retired drawdown view | `post-retirement` |
|
|
|
|
Every projection also opens with a benchmark comparison and a
|
|
`Projected return` line -- your holdings' blended expected return,
|
|
which feeds the simulation.
|
|
|
|
## Question 1: "What can I spend?" (target date)
|
|
|
|
`pre-retirement-age` sets `retirement_age:num:65` and an
|
|
`annual_contribution`, but no target spending:
|
|
|
|
```bash
|
|
ZFIN_HOME=examples/pre-retirement-age zfin projections
|
|
```
|
|
|
|
The **Accumulation phase** block gives the dated headline and the
|
|
projected portfolio at retirement:
|
|
|
|
```
|
|
Accumulation phase:
|
|
Years until possible retirement: 19 (2046-04-12, ages 65/62)
|
|
Annual contributions: $80,000 (CPI-adjusted)
|
|
Median portfolio at retirement: $7,599,829.01
|
|
Range (10th-90th percentile): $5,576,011.69 to $17,552,083.29
|
|
```
|
|
|
|
Below it, the **Safe Withdrawal** table shows the sustainable annual
|
|
spend at each horizon and confidence level (FIRECalc-style historical
|
|
simulation):
|
|
|
|
```
|
|
Safe Withdrawal (FIRECalc historical simulation)
|
|
25 Year 35 Year 50 Year
|
|
90% safe withdrawal $331,658 $298,238 $291,267
|
|
99% safe withdrawal $301,038 $280,477 $251,851
|
|
```
|
|
|
|
Read it as: "retiring in 2046 with this portfolio, I could withdraw
|
|
~$252k/yr and be 99% confident it lasts 50 years (historically)."
|
|
|
|
## Question 2: "When can I retire?" (target spending)
|
|
|
|
`pre-retirement-spending` sets `target_spending:num:80000` but no date.
|
|
zfin searches for the earliest year that sustains that spending and
|
|
renders the **Earliest retirement** grid:
|
|
|
|
```bash
|
|
ZFIN_HOME=examples/pre-retirement-spending zfin projections
|
|
```
|
|
|
|
```
|
|
Earliest retirement (target spending: $80,000/yr CPI-adjusted)
|
|
25 Year 35 Year 50 Year
|
|
90% confidence 2030-06-19 2030-06-19 2030-06-19
|
|
95% confidence 2030-06-19 2030-06-19 2030-06-19
|
|
99% confidence 2031-06-19 2031-06-19 2031-06-19
|
|
```
|
|
|
|
One cell is **promoted** to the Accumulation-phase headline. The
|
|
default rule picks the longest horizon at 99% confidence that keeps the
|
|
oldest person under age 100. Override it by annotating one horizon line
|
|
in `projections.srf`:
|
|
|
|
```srf
|
|
type::config,horizon:num:35,retirement_target:num:95
|
|
```
|
|
|
|
## Both questions at once
|
|
|
|
`pre-retirement-both` sets a date **and** a spending target, so both
|
|
blocks render back to back -- "I planned to retire in 2046; at these
|
|
confidence levels I could actually retire as early as 2030." The
|
|
configured date wins the headline; the grid is the comparison.
|
|
|
|
## When a plan isn't feasible
|
|
|
|
`pre-retirement-spending-target` sets an aggressive
|
|
`target_spending:num:2400000` and pins the headline to the
|
|
longest-horizon, highest-confidence cell -- which turns out to be
|
|
unreachable inside the default 50-year search:
|
|
|
|
```bash
|
|
ZFIN_HOME=examples/pre-retirement-spending-target zfin projections
|
|
```
|
|
|
|
```
|
|
Accumulation phase:
|
|
Years until possible retirement: not feasible
|
|
|
|
Earliest retirement (target spending: $2,400,000/yr CPI-adjusted)
|
|
25 Year 35 Year 50 Year
|
|
99% confidence 2075-06-19 infeasible infeasible
|
|
```
|
|
|
|
The headline reports "not feasible" honestly, and the grid still shows
|
|
which cells *do* work so you can choose a reachable anchor.
|
|
|
|
## Already retired: the drawdown view
|
|
|
|
`post-retirement` configures neither input -- it's a distribution-only
|
|
household:
|
|
|
|
```bash
|
|
ZFIN_HOME=examples/post-retirement zfin projections
|
|
```
|
|
|
|
The accumulation block collapses to a single line, confirming no
|
|
pre-retirement growth is being modeled:
|
|
|
|
```
|
|
Accumulation phase:
|
|
Years until possible retirement: none
|
|
```
|
|
|
|
Everything else -- the median-value chart, terminal-value percentiles,
|
|
and safe-withdrawal table over the configured horizons -- behaves as a
|
|
pure drawdown projection.
|
|
|
|
## Life events
|
|
|
|
Social Security, pensions, tuition, and late-life healthcare are
|
|
modeled as `type::event` lines (positive = income, negative =
|
|
expense). They appear in the Life Events block and shift the cash-flow
|
|
math in both phases:
|
|
|
|
```
|
|
Life Events
|
|
Social Security (Pat) +$38,400/yr age 70 (in 25yr)
|
|
College Tuition -$55,000/yr age 50 (in 5yr), 4yr
|
|
```
|
|
|
|
See [event fields](../reference/config/projections-srf.md#event-fields).
|
|
|
|
## Check the model against reality
|
|
|
|
Once you have [snapshot history](snapshots-and-history.md) (or imported
|
|
back-values), zfin can grade its own past projections three ways:
|
|
|
|
- **Actuals overlay** -- plot your realized trajectory on top of the
|
|
bands the model *would have drawn* from a past date. Did reality stay
|
|
inside the envelope?
|
|
|
|
```bash
|
|
zfin projections --as-of 1Y --overlay-actuals
|
|
```
|
|
|
|
- **Convergence** (`--convergence`) -- as data accumulated, did the
|
|
model's predicted retirement date settle down, or keep drifting?
|
|
|
|
- **Return back-test** (`--return-backtest`) -- was the
|
|
expected-return assumption honest next to the realized forward
|
|
returns?
|
|
|
|
The CLI prints these as text and braille; the TUI draws them as real
|
|
charts (next section).
|
|
|
|
> A caveat zfin states loudly: these show whether the model was
|
|
> **directionally honest** -- did your actual path fall within the
|
|
> bands it drew -- **not** whether a safe-withdrawal claim holds over a
|
|
> full 30-year retirement. There isn't enough history to answer the
|
|
> latter, and won't be within our lifetimes.
|
|
|
|
## In the interactive TUI
|
|
|
|
The CLI gives you the numbers; the **Projections tab** in the TUI
|
|
(`zfin i`, then tab over to Projections) is where it comes alive, with
|
|
high-fidelity charts the plain terminal can't draw. Press `?` for the
|
|
full keymap; the projections-specific keys:
|
|
|
|
| Key | Does |
|
|
|-------|----------------------------------------------------------------------------------------------------------------|
|
|
| `v` | Show/hide the **percentile-band chart** -- the median line with the p10-p90 envelope across the horizon. |
|
|
| `d` | Set an **as-of date** -- back-date the whole projection to any past date (auto-snaps to the nearest snapshot). |
|
|
| `o` | Overlay your **realized actuals** on the bands (needs an as-of date plus snapshot/imported history). |
|
|
| `z` | Zoom the overlay's x-axis to roughly `[as-of, today + horizon]`. |
|
|
| `c` | **Convergence** chart -- the model's predicted retirement date over time. |
|
|
| `b` | **Return back-test** chart -- expected vs. realized forward returns. |
|
|
| `e` | Show/hide the life-events annotations. |
|
|
| `Esc` | Clear the as-of date, back to the live view. |
|
|
|
|
A typical what-if loop: open the Projections tab, press `d` and enter a
|
|
date a few years back, then `o` to drop your real trajectory onto the
|
|
bands the model would have drawn then -- a visual, honest check of how
|
|
the projection has held up. `c` and `b` then grade the model's
|
|
retirement-date and return assumptions over time.
|
|
|
|
Charts render as crisp Kitty graphics when your terminal supports it,
|
|
and fall back to braille otherwise (see
|
|
[`--chart`](../reference/cli/interactive.md) and
|
|
[The interactive TUI](../reference/tui.md)).
|
|
|
|
## Example: a complete `projections.srf`
|
|
|
|
This is the
|
|
[`pre-retirement-both`](../../examples/pre-retirement-both/projections.srf)
|
|
household: Pat (born 1981) and Sam (born 1983), retiring at 65 and
|
|
targeting $80k/yr, with both an accumulation and a distribution phase.
|
|
Copy it as a starting point and change the numbers to yours.
|
|
|
|
```srf
|
|
#!srfv1
|
|
|
|
# Accumulation phase (while still working)
|
|
type::config,retirement_age:num:65
|
|
type::config,annual_contribution:num:80000
|
|
type::config,contribution_inflation_adjusted:bool:true
|
|
|
|
# Distribution phase (in retirement)
|
|
type::config,target_stock_pct:num:80
|
|
type::config,target_spending:num:80000
|
|
type::config,target_spending_inflation_adjusted:bool:true
|
|
type::config,horizon:num:25
|
|
type::config,horizon:num:35
|
|
type::config,horizon_age:num:95
|
|
|
|
# The two people (drive ages, retirement_age, and life-event timing)
|
|
type::birthdate,date::1981-04-12
|
|
type::birthdate,date::1983-09-08,person:num:2
|
|
|
|
# Life events (positive = income, negative = expense)
|
|
type::event,name::Social Security (Pat),start_age:num:70,person:num:1,amount:num:38400
|
|
type::event,name::Social Security (Sam),start_age:num:70,person:num:2,amount:num:36000
|
|
type::event,name::College Tuition,start_age:num:50,person:num:1,duration:num:4,amount:num:-55000
|
|
```
|
|
|
|
Run it with `ZFIN_HOME=examples/pre-retirement-both zfin projections`;
|
|
every field is documented in the
|
|
[`projections.srf` reference](../reference/config/projections-srf.md).
|
|
|
|
## Next steps
|
|
|
|
- [`projections.srf` reference](../reference/config/projections-srf.md) -- every field.
|
|
- [The retirement projection model](../explanation/projections-model.md) -- the math and assumptions.
|
|
- [The interactive TUI](../reference/tui.md) -- the Projections tab and its charts.
|
|
- [Snapshots and history](snapshots-and-history.md) -- build the actuals the overlay needs.
|
|
|
|
---
|
|
|
|
[Previous: Snapshots and history](snapshots-and-history.md) | [Next: Audit against your brokerage](audit-against-brokerage.md) | [Documentation home](../README.md)
|