zfin/docs/guides/plan-retirement.md
Emil Lerch 74fc219afd
All checks were successful
Generic zig build / build (push) Successful in 5m48s
Generic zig build / publish-macos (push) Successful in 11s
Generic zig build / deploy (push) Successful in 23s
add docs/guides
2026-06-22 14:53:53 -07:00

310 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 should track
[FIRECalc.com](https://firecalc.com/) closely. For the full method and
its assumptions, 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,871,732.10
Range (10th90th percentile): $5,807,693.45 to $18,240,675.15
```
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 $347,601 $311,857 $308,728
99% safe withdrawal $314,920 $293,374 $264,002
```
Read it as: "retiring in 2046 with this portfolio, I could withdraw
~$264k/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 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)