163 lines
7.1 KiB
Markdown
163 lines
7.1 KiB
Markdown
# 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:
|
||
|
||
```sh
|
||
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, p10–p90
|
||
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, p10–p90 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.
|