zfin/examples
2026-05-13 11:15:04 -07:00
..
post-retirement adding previously ignored example files 2026-05-13 11:15:04 -07:00
pre-retirement-age adding previously ignored example files 2026-05-13 11:15:04 -07:00
pre-retirement-both adding previously ignored example files 2026-05-13 11:15:04 -07:00
pre-retirement-spending adding previously ignored example files 2026-05-13 11:15:04 -07:00
pre-retirement-spending-target adding previously ignored example files 2026-05-13 11:15:04 -07:00
README.md add additional data in examples/ directories 2026-05-12 17:26:24 -07:00

zfin examples

Realistic-but-fictional portfolio configurations for spot-checking zfin features and showing users how the configuration files fit together. Run any zfin command against an example by setting ZFIN_HOME to its directory:

ZFIN_HOME=examples/pre-retirement-both              zfin projections
ZFIN_HOME=examples/pre-retirement-age               zfin projections
ZFIN_HOME=examples/pre-retirement-spending          zfin projections
ZFIN_HOME=examples/pre-retirement-spending-target   zfin projections
ZFIN_HOME=examples/post-retirement                  zfin projections
ZFIN_HOME=examples/pre-retirement-both              zfin --tui

All names, share counts, account numbers, prices, and life events in these examples are fictional. Do not interpret them as advice.

Available examples

The five scenarios share the same fictional couple and balance sheet (~$1.3M, age ~45, contributing $80k/yr) for the four pre-retirement variants, and a separate retired couple for the distribution example. Only the projections.srf configuration differs across the pre-retirement variants — making it easy to see how each retirement-planning input shapes the output.

Background: how the projection accepts input

The simulation always runs the same two-phase model: an accumulation phase (contributions in, no spending) followed by a distribution phase (spending out, no contributions). What the user configures in projections.srf decides which questions the display answers:

  • Target retirement date (retirement_age or retirement_at) — "Given my retirement date, what can I spend?" Produces the Accumulation phase block: median portfolio at retirement, p10p90 range, and the dated headline retirement line.
  • Target spending (target_spending) — "Given my desired spending, when can I retire?" Produces the Earliest retirement grid (one cell per horizon × confidence) and promotes one cell into the Accumulation phase block as the headline.
  • Both — both blocks render back-to-back. The configured retirement date wins for the headline; the grid is the side-by-side comparison.
  • Neither — distribution-only mode. The Accumulation phase block reduces to a soft "Years until possible retirement: none" line.

pre-retirement-age/

Input: target retirement date only. retirement_age:num:65 is set, target_spending is not. Output renders:

  • Accumulation phase block — median portfolio at the configured retirement date, p10p90 range, and the Years until possible retirement: 19 (2046-04-12, ages 65/62) line showing both partners' ages at retirement.
  • Standard Safe Withdrawal table for the configured horizons.
  • No "Earliest retirement" grid (no spending target to search against).

pre-retirement-spending/

Input: target spending only. target_spending:num:80000 is set, retirement_age/retirement_at are not. Output renders:

  • Earliest retirement grid — one cell per (horizon × confidence) showing the earliest year the household can retire and sustain $80k/yr at that confidence over that distribution horizon.
  • The Accumulation phase block is populated by promoting one cell from the grid into the headline retirement line, plus the median portfolio at retirement and p10-p90 range. The default promotion rule walks horizons longest → shortest and picks the longest one whose end year keeps the oldest configured person under age 100, at 99% confidence (most conservative). If even the shortest horizon overshoots, it's used anyway.
  • The grid stays rendered for transparency — the user can see how the headline cell compares to the rest of the matrix.

pre-retirement-spending-target/

Same household and balance sheet as pre-retirement-spending/, but with a much higher target_spending ($2.4M/yr) AND an explicit retirement_target:num:99 annotation on the horizon_age:num:95 record. That combination demonstrates two things at once:

  • The explicit override mechanism: instead of the default promotion rule (longest horizon at 99% confidence, capped at age 100), the user explicitly anchors the headline to the resolved horizon_age:95 × 99% cell. The override survives age-resolution — it rides on horizon_age records too, not just horizon.
  • The "not feasible" rendering path: the annotated cell turns out to be infeasible at this spending level (no value of accumulation_years ≤ 50 sustains $2.4M/yr at 99% over a 50-year retirement). The headline line renders "Years until possible retirement: not feasible" instead of a date, and the contribution / median lines below it are suppressed. The full Earliest retirement grid still renders so the user can see which (horizon × confidence) cells DO work and pick a different anchor.

Use this variant when the user has a preferred planning anchor that's different from "longest feasible horizon at maximum conservatism." Allowed retirement_target values are 90, 95, 99. At most one horizon may carry the annotation; configuring two or more drops them all (warning logged) and falls back to the default rule.

pre-retirement-both/

Inputs: both target retirement date AND target spending. Both retirement_age and target_spending are set. Output renders both blocks back-to-back so the user can compare "what I planned" (target retirement date) against "what's earliest feasible" (target spending). The configured retirement date wins for the headline line; the Earliest retirement grid is rendered below for comparison.

post-retirement/

A retired couple, ~age 68, ~$2M total. Distribution-only mode (no target_spending, no retirement_age/retirement_at). Demonstrates the legacy projection behavior, including the soft "Years until possible retirement: none" line that confirms the model isn't projecting any pre-retirement growth.

Includes a late-life healthcare expense modeled as a life event starting at age 80, plus Social Security already in pay status. Asset allocation target: 60/40 (more bond-heavy than the pre-retirement examples).

Configuration file map

Every example contains:

  • portfolio.srf — open lots, one per line. The source of truth for shares, cost basis, and account assignment.
  • accounts.srf — tax type and institution metadata for each account name referenced by portfolio.srf.
  • metadata.srf — sector / geography / asset-class classifications for each symbol.
  • projections.srf — retirement projection configuration: birthdates, target allocation, horizons, life events, and (in the pre-retirement variants) the accumulation-phase / earliest-retirement fields. This is the only file that differs across the four pre-retirement variants.

There's no watchlist.srf in either example. Add one if you're spot-checking watchlist features.

Symbol selection

The examples use symbols whose candle data is commonly cached and kept fresh by ongoing cron jobs:

  • Equity ETFs: SPY, VTI, QQQ, SCHD
  • Bond ETF: AGG

If you add a symbol not in this list, run zfin quote <SYM> once inside the example directory to populate the cache before running analytics commands.