zfin/docs/explanation/projections-model.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

141 lines
6.2 KiB
Markdown

# The retirement projection model
[`zfin projections`](../reference/cli/projections.md) simulates your
retirement portfolio against real market history. This page explains
the model so you can trust -- and correctly distrust -- its output. For
how to configure it, see the
[`projections.srf` reference](../reference/config/projections-srf.md)
and [Plan for retirement](../guides/plan-retirement.md).
## Historical simulation, not a formula
Rather than assume a single average return, zfin replays your portfolio
through actual historical sequences drawn from the **Shiller dataset**
(US equity total returns and CPI back to 1871). Each simulated run uses
a real historical path of returns and inflation, so the spread of
outcomes reflects real sequences -- including bad-timing sequences like
retiring into 1929, 1973, or 2000. This is the same family of method
as FIRECalc.
## Two phases
Every projection runs the same two phases in order:
1. **Accumulation** -- contributions added each year, no spending. Its
length comes from your retirement-date input. With no input, it's
zero years (an already-retired view).
2. **Distribution** -- annual spending withdrawn (CPI-adjusted by
default), no contributions. Its length is the configured `horizon`.
[Life events](../reference/config/projections-srf.md#event-fields)
(Social Security, pensions, tuition, healthcare) adjust the cash flow
in both phases.
## How inflation is handled
Inflation isn't a fixed assumption. Each historical cycle uses that
start year's **actual CPI sequence** alongside its actual returns (the
Shiller dataset carries both), so a cycle beginning in 1966 replays
1966's stagflation while one beginning in 2009 replays low-inflation
years.
The simulation runs in **nominal dollars**, which means the output
mixes two units -- and knowing which is which is the difference between
a sensible plan and a badly misread one:
- **Flows are entered in today's dollars and inflated forward.** Your
`annual_contribution`, `target_spending`, and inflation-adjusted life
events are amounts in *today's* dollars; each simulated year the model
multiplies them by that cycle's cumulative CPI, holding their
purchasing power constant. Set `contribution_inflation_adjusted`,
`target_spending_inflation_adjusted`, or an event's
`inflation_adjusted` to `false` to pin a flow at a flat nominal amount
instead (e.g. a fixed pension with no COLA).
- **Safe-withdrawal figures are in today's dollars.** "You could spend
~$264k/yr at 99%" means ~$264k of *today's* purchasing power, with the
actual dollar amount rising each retirement year to keep pace with
inflation.
- **Portfolio and terminal values are nominal (future dollars).** The
"Median portfolio at retirement" and the
`Terminal Portfolio Value (nominal, ...)` percentiles are **not**
inflation-adjusted. A ~$244M median balance 50 years out is heavily
inflated dollars, not $244M of today's purchasing power -- judge it
against the inflated spending it has to support, never against today's
prices.
This split is deliberate and matches FIRECalc: you plan spending in real
(today's) terms while the balance compounds in nominal terms.
## Percentile bands
Across all the historical runs, zfin reports the distribution of
outcomes rather than a single number:
- **p10 (pessimistic)** -- only 10% of histories did worse.
- **p50 (median)** -- the middle outcome.
- **p90 (optimistic)** -- only 10% did better.
```
Terminal Portfolio Value (nominal, at 99% withdrawal rate)
25 Year 35 Year
Pessimistic (p10) $6,739,560.02 $11,597,557.94
Median (p50) $30,023,255.68 $66,794,741.87
Optimistic (p90) $103,184,321.05 $279,372,182.75
```
The wide spread is the point: it shows sequence-of-returns risk
honestly instead of hiding it behind an average.
## Confidence and safe withdrawal
The **Safe Withdrawal** table answers "how much could I spend and still
not run out?" at chosen confidence levels (90/95/99%). A 99% safe
withdrawal is the spending level that survived 99% of historical
sequences over that horizon -- the most conservative. Higher confidence
and longer horizons both lower the safe number.
## The earliest-retirement search
When you set a `target_spending` instead of a date, zfin inverts the
question: for each (horizon x confidence) cell it searches for the
**earliest** accumulation length (up to 50 years) that sustains your
spending, and renders the grid of answers. One cell is promoted to the
headline (see
[promotion rules](../reference/config/projections-srf.md#the-two-retirement-planning-inputs)).
If no length within the cap works, the cell is **infeasible** -- shown
honestly rather than fudged.
## The caveat that matters most
zfin states this loudly by design, and so does this page:
> The actuals overlay and evaluation views
> (`--overlay-actuals`, `--convergence`, `--return-backtest`) tell you
> whether the model was **directionally honest** -- did your real
> trajectory fall within the bands it would have drawn. They do **not**
> tell you whether a safe-withdrawal claim is **accurate**. An SWR
> claim is a 30-year claim; there is at most ~12 years of weekly
> history and a year or two of native snapshots to check it against.
> No one will have data to validate a full-retirement SWR within our
> lifetimes.
Treat the projection as a disciplined way to compare scenarios and
visualize sequence risk -- not as a promise about your specific future.
## Assumptions to keep in mind
- **Allocation** is a single stock/bond blend (`target_stock_pct`), not
your exact holdings.
- **Inflation** comes from each historical cycle's own CPI; flows are
real (today's-dollar) and balances are nominal. See
[How inflation is handled](#how-inflation-is-handled).
- **Taxes** are not modeled. Withdrawal figures are pre-tax.
- **Imported-value overlays** scale today's allocation to a historical
total when lot-level history isn't available, because a `liquid::`
row can't reconstruct past composition.
## See also
- [Plan for retirement](../guides/plan-retirement.md) -- the guided walkthrough.
- [`projections.srf` reference](../reference/config/projections-srf.md) -- every input.
- [`zfin projections`](../reference/cli/projections.md) -- flags and evaluation views.