zfin/examples/README.md

163 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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