zfin/docs/guides/plan-retirement.md

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)