310 lines
13 KiB
Markdown
310 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 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 (10th–90th 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)
|