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

13 KiB
Raw Blame History

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:

ZFIN_HOME=examples/pre-retirement-both zfin projections

For the field-by-field file format, see the projections.srf reference; for how the simulation works under the hood, see The retirement projection model.

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 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 closely. For the full method and its assumptions, see The retirement projection model.

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 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:

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:

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:

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:

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:

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.

Check the model against reality

Once you have snapshot history (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?

    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 and The interactive TUI).

Example: a complete projections.srf

This is the pre-retirement-both 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.

#!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.

Next steps


Previous: Snapshots and history | Next: Audit against your brokerage | Documentation home