# 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`,
   or - when you set a `horizon_age` - the years until the *last
   surviving* household member reaches that age of death (see
   [Mortality](#mortality-the-surviving-spouse)).
   Spending is flat in real terms unless you set
   [`spending_change`](../reference/config/projections-srf.md#declining-spending-the-smile)
   to taper or grow it year over year (the Blanchett "spending smile").

[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 `max_accumulation_years`, 50
years by default) 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. A young saver with a runway longer than 50
years can raise the cap via
[`max_accumulation_years`](../reference/config/projections-srf.md#config-fields).

For a plain `horizon` the distribution length is fixed, so a later
retirement means the money has to last from a later start to a fixed
number of years out. For an age-anchored `horizon_age` column the
distribution instead *shrinks* as retirement slides later, because the
end (the last survivor's death) is pinned: the total span from today is
constant and the column header reads `to age 95` rather than a year
count.

## Mortality: the surviving spouse

A `horizon_age` turns on a mortality model that a plain numeric horizon
doesn't have. It rests on the financial-planning standard for couples:
**the money must last until the last surviving member dies**, because
the household needs income for as long as *either* spouse is alive
(Blanchett, "How to Estimate 'The End' of Retirement," *Journal of
Financial Planning*, 2021). So the horizon is anchored on the
**youngest** member (who reaches the age of death latest), not the
oldest.

Two adjustments fire at the **first** death (the oldest member reaching
the age of death):

- **Spending steps down.** A surviving spouse needs less than the
  couple but far more than half - shared costs (housing, utilities,
  insurance) don't fall when one person dies. zfin scales base spending
  by [`survivor_spending_pct`](../reference/config/projections-srf.md#survivor-spending-survivor_spending_pct)
  (default 75%, a 25% cut). That default is the conservative edge of the
  standard equivalence-scale range (the OECD-modified scale implies
  ~67%, the square-root scale ~71%); financial-planning software is
  gentler at ~80%. The income side falls harder than the need side -
  the Chicago Fed found household income drops ~37% at widowhood but the
  standard-of-living-adjusted decline is only ~11% - which is why the
  knob is about *need*, not income.
- **The deceased's income stops.** Each person's Social Security,
  pension, and wages (and their own late-life expenses), entered as
  `type::event`, terminate the year that person dies. Survivor benefits
  that partly continue (a pension's survivor percentage, Social
  Security's keep-the-higher rule) are modeled as a separate event on
  the surviving spouse - see the
  [config reference](../reference/config/projections-srf.md#modeling-survivor-benefits).

These two effects pull in opposite directions on the headline number:
the last-survivor horizon lengthens the plan (more conservative), while
the survivor spending cut and the (correct) retention of only the
survivor's own benefits shorten the funding need. Modeling both is more
faithful than either the old "stop at the first death" truncation or a
naive "fund the longer life at full couple spending."

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

## Parity with FIRECalc

Because zfin re-implements the FIRECalc method over the same Shiller
dataset, its numbers should -- and do -- **track
[FIRECalc.com](https://firecalc.com/) closely, while running
systematically a little more optimistic**. This section records the
evidence behind that claim from a June 2026 audit, so "tracks closely"
isn't just an assertion. The cross-checks are pinned as a regression
suite (`FIRECalc parity: ...` tests in `analytics/projections.zig`).

### Method

FIRECalc 3.0 was driven directly through its web form (the same
1871-2025 Shiller span zfin embeds, "data thru 1/1/2026"). For an
apples-to-apples comparison, FIRECalc's **expense ratio was set to 0%**
(matching zfin with `expense_ratio:num:0`) and its fixed-income model
left at the default "Long Interest" (10-year Treasury). Zeroing the fee
on both sides removes it as a variable so the references below isolate
the one structural difference, the equity return series. zfin uses the
Treasury *yield* for bonds rather than a bond-price series, which is why
the comparisons hold the allocation at familiar blends.

### What matches, and by how much

Safe-withdrawal dollars (today's dollars, FIRECalc fee=0), the headline
"how much can I spend" number:

| Scenario (portfolio / alloc / horizon / confidence) | FIRECalc |   zfin   |   Δ   |
|-----------------------------------------------------|---------:|---------:|------:|
| $1M / 100% / 30y / 95%                              | $39,697  | $42,717  | +7.6% |
| $1M / 75-25 / 30y / 95%                             | $41,221  | $44,036  | +6.8% |
| $1M / 100% / 45y / 95%                              | $35,835  | $37,906  | +5.8% |
| $1M / 100% / 20y / 95%                              | $45,879  | $49,660  | +8.2% |
| $1M / 100% / 30y / 90%                              | $43,804  | $47,138  | +7.6% |
| $1M / 100% / 30y / 99%                              | $35,864  | $38,098  | +6.2% |
| $7.7M / 100% / 45y / 99%                            | $254,461 | $275,724 | +8.4% |
| $7.7M / 82% / 45y / 99%                             | $262,770 | $286,314 | +9.0% |

Success rate ($1M, $40k/yr, 30yr, fee=0): FIRECalc 94.4% vs zfin 97.6%
(100% stock); FIRECalc 96.8% vs zfin 99.2% (75/25) -- zfin ~+2-3pp.

Terminal portfolio value (same scenario, **nominal** dollars): median
FIRECalc $5.12M vs zfin $5.71M (+11%); p90 $12.76M vs $14.75M (+16%).

One contributions (accumulation-phase) cross-check -- $500k start,
$30k/yr added for 10 years, then 30-year drawdown, 95% -- lands at
FIRECalc $55,154 vs zfin $53,468 (-3.1%), the one case where zfin came
out *lower* (more conservative). That flip is a real modeling
difference in how the two tools treat the accumulation phase, not
noise -- see "Accumulation phase" below.

### Why zfin runs a little hot: methodology, not a bug

The divergence was isolated with a **$0-spending, 100%-stock** run,
which removes withdrawals, withdrawal timing, fees, and bonds from the
picture entirely. For the 1966 cohort, zfin's year-30 *nominal* balance
is **$20.24M vs FIRECalc's $18.60M** -- a ratio of 0.919 over 30 years,
i.e. FIRECalc's equity returns compound about **0.2-0.3%/yr lower** than
zfin's. That is the entire discrepancy: the gap is in the **equity
total-return series**, not the withdrawal logic.

The reason zfin is higher is that **zfin uses the gold-standard
construction and FIRECalc uses a coarser one**:

- **zfin** reconstructs each year's nominal total return directly from
  Shiller's **Real Total Return Price** index -- the canonical
  academic S&P total-return series, in which dividends are reinvested
  **monthly** -- times that year's CPI change (see
  `build/gen_shiller.zig`). The reconstruction recovers Shiller's
  published nominal total return exactly.
- **FIRECalc** computes "market growth + dividends" in the lineage of
  the 1998 Trinity Study and John Greaney's *Retire Early* spreadsheet
  (FIRECalc's own
  [methodology page](https://www.firecalc.com/intro.php) describes
  this). That construction reinvests dividends more coarsely (annually,
  in effect), which **systematically understates compounding** by
  roughly a quarter-percent a year versus the monthly-reinvested index.

So zfin's equity returns are slightly higher **because they are more
accurate** -- monthly dividend reinvestment is what actually happened.
Over 30-45 year horizons that ~0.25%/yr compounds into the +6-9%
safe-withdrawal gap, and the worst cohorts (which set the
safe-withdrawal floor) diverge most because small per-year differences
explode near the failure boundary. FIRECalc's own FAQ concedes the
point -- it notes that implementations differ on exactly these details
and "all of the studies converge on the same basic results."

**Honest caveat (cuts the other way):** a more accurate *historical*
return series does not make the *forecast* more accurate -- nobody can
predict your future returns. It only means zfin replays history with
better-constructed inputs. If you specifically want to reproduce
FIRECalc's output, expect zfin to read a few percent higher for this
reason, by design.

### Other differences

- **Terminal values: nominal vs real.** FIRECalc's *on-screen* ending
  balances are **real** (start-of-retirement dollars); zfin's terminal
  bands are **nominal**. (FIRECalc's spreadsheet *export* is nominal,
  which is what the terminal-value table above compares against.) Don't
  compare zfin's nominal terminal bands to FIRECalc's on-screen ending
  range without deflating one of them first.
- **Accumulation phase: zfin models it through history, FIRECalc
  doesn't.** For runs with a pre-retirement contribution phase, the
  two tools differ by design. FIRECalc always reports "N possible
  *<distribution>* year periods" -- e.g. 126 thirty-year periods --
  *regardless of accumulation length* (verified at 0, 10, and 25
  years of accumulation, all 126). Since a 1871 distribution start
  with 25 years of accumulation would need 1846-1870 data that
  predates the dataset, FIRECalc cannot be replaying history during
  accumulation: it grows the pre-retirement portfolio
  **deterministically** and only Monte-Carlos the distribution. zfin
  instead runs the **full accumulation + distribution span through one
  continuous historical sequence** (116 cohorts for a 10+30yr run), so
  it also captures **accumulation-phase sequence-of-returns risk** -- a
  bad market in the years just before retirement. That extra realism
  is why zfin reads slightly *lower* (more conservative) on
  contribution scenarios. It's a deliberate fidelity gain, not a
  discrepancy to reconcile.

### The bottom line

Treat zfin's safe-withdrawal numbers as **tracking FIRECalc within
roughly +6-9%, in the optimistic direction** -- a pure equity-engine
gap that holds with fees matched (both tools default to a 0.18% fee).
If you want a FIRECalc-conservative read, mentally haircut zfin's
safe-spending figure by ~5-10%. The parity
suite asserts zfin stays within -3% / +15% of these references, so a
future engine change that drifts materially further -- in either
direction -- trips a test.

## 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.
- **Fees** are modeled as a flat annual expense-ratio drag, defaulting
  to 0.18% (configurable via
  [`expense_ratio`](../reference/config/projections-srf.md)); set it to
  your portfolio's blended ratio.
- **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.
