From d94ffb3410d74000505e6a979f89e002ea855a11 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 12 May 2026 17:26:24 -0700 Subject: [PATCH] add additional data in examples/ directories --- examples/README.md | 163 ++++++++++++++++++ examples/post-retirement/metadata.srf | 8 + examples/pre-retirement-age/metadata.srf | 14 ++ examples/pre-retirement-both/metadata.srf | 14 ++ .../metadata.srf | 14 ++ examples/pre-retirement-spending/metadata.srf | 14 ++ 6 files changed, 227 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/post-retirement/metadata.srf create mode 100644 examples/pre-retirement-age/metadata.srf create mode 100644 examples/pre-retirement-both/metadata.srf create mode 100644 examples/pre-retirement-spending-target/metadata.srf create mode 100644 examples/pre-retirement-spending/metadata.srf diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..86cb0b2 --- /dev/null +++ b/examples/README.md @@ -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, 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 ` once +inside the example directory to populate the cache before running +analytics commands. diff --git a/examples/post-retirement/metadata.srf b/examples/post-retirement/metadata.srf new file mode 100644 index 0000000..c748ce6 --- /dev/null +++ b/examples/post-retirement/metadata.srf @@ -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 diff --git a/examples/pre-retirement-age/metadata.srf b/examples/pre-retirement-age/metadata.srf new file mode 100644 index 0000000..b70bc48 --- /dev/null +++ b/examples/pre-retirement-age/metadata.srf @@ -0,0 +1,14 @@ +#!srfv1 +# Symbol classification metadata for the pre-retirement example. +# Each line: symbol::,sector::,geo::,asset_class:: + +# 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 diff --git a/examples/pre-retirement-both/metadata.srf b/examples/pre-retirement-both/metadata.srf new file mode 100644 index 0000000..b70bc48 --- /dev/null +++ b/examples/pre-retirement-both/metadata.srf @@ -0,0 +1,14 @@ +#!srfv1 +# Symbol classification metadata for the pre-retirement example. +# Each line: symbol::,sector::,geo::,asset_class:: + +# 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 diff --git a/examples/pre-retirement-spending-target/metadata.srf b/examples/pre-retirement-spending-target/metadata.srf new file mode 100644 index 0000000..b70bc48 --- /dev/null +++ b/examples/pre-retirement-spending-target/metadata.srf @@ -0,0 +1,14 @@ +#!srfv1 +# Symbol classification metadata for the pre-retirement example. +# Each line: symbol::,sector::,geo::,asset_class:: + +# 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 diff --git a/examples/pre-retirement-spending/metadata.srf b/examples/pre-retirement-spending/metadata.srf new file mode 100644 index 0000000..b70bc48 --- /dev/null +++ b/examples/pre-retirement-spending/metadata.srf @@ -0,0 +1,14 @@ +#!srfv1 +# Symbol classification metadata for the pre-retirement example. +# Each line: symbol::,sector::,geo::,asset_class:: + +# 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