add additional data in examples/ directories

This commit is contained in:
Emil Lerch 2026-05-12 17:26:24 -07:00
parent bdd827734d
commit d94ffb3410
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 227 additions and 0 deletions

163
examples/README.md Normal file
View file

@ -0,0 +1,163 @@
# 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.

View file

@ -0,0 +1,8 @@
#!srfv1
# Symbol classification metadata for the post-retirement example.
symbol::VTI,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::SPY,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::QQQ,sector::Technology,geo::US,asset_class::US Large Cap
symbol::SCHD,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::AGG,sector::Bonds,geo::US,asset_class::Bonds

View file

@ -0,0 +1,14 @@
#!srfv1
# Symbol classification metadata for the pre-retirement example.
# Each line: symbol::<SYM>,sector::<S>,geo::<G>,asset_class::<A>
# Broad-market ETFs
symbol::VTI,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::SPY,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::QQQ,sector::Technology,geo::US,asset_class::US Large Cap
# Dividend ETF
symbol::SCHD,sector::Diversified,geo::US,asset_class::US Large Cap
# Bond ETF
symbol::AGG,sector::Bonds,geo::US,asset_class::Bonds

View file

@ -0,0 +1,14 @@
#!srfv1
# Symbol classification metadata for the pre-retirement example.
# Each line: symbol::<SYM>,sector::<S>,geo::<G>,asset_class::<A>
# Broad-market ETFs
symbol::VTI,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::SPY,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::QQQ,sector::Technology,geo::US,asset_class::US Large Cap
# Dividend ETF
symbol::SCHD,sector::Diversified,geo::US,asset_class::US Large Cap
# Bond ETF
symbol::AGG,sector::Bonds,geo::US,asset_class::Bonds

View file

@ -0,0 +1,14 @@
#!srfv1
# Symbol classification metadata for the pre-retirement example.
# Each line: symbol::<SYM>,sector::<S>,geo::<G>,asset_class::<A>
# Broad-market ETFs
symbol::VTI,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::SPY,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::QQQ,sector::Technology,geo::US,asset_class::US Large Cap
# Dividend ETF
symbol::SCHD,sector::Diversified,geo::US,asset_class::US Large Cap
# Bond ETF
symbol::AGG,sector::Bonds,geo::US,asset_class::Bonds

View file

@ -0,0 +1,14 @@
#!srfv1
# Symbol classification metadata for the pre-retirement example.
# Each line: symbol::<SYM>,sector::<S>,geo::<G>,asset_class::<A>
# Broad-market ETFs
symbol::VTI,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::SPY,sector::Diversified,geo::US,asset_class::US Large Cap
symbol::QQQ,sector::Technology,geo::US,asset_class::US Large Cap
# Dividend ETF
symbol::SCHD,sector::Diversified,geo::US,asset_class::US Large Cap
# Bond ETF
symbol::AGG,sector::Bonds,geo::US,asset_class::Bonds