From 74fc219afda9688eef6cc0f5ea5ec9b9e2e3a7ae Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 22 Jun 2026 14:53:53 -0700 Subject: [PATCH] add docs/guides --- README.md | 876 +------------------ TODO.md | 26 + docs/README.md | 103 +++ docs/explanation/caching.md | 121 +++ docs/explanation/concepts.md | 126 +++ docs/explanation/data-providers.md | 72 ++ docs/explanation/faq-troubleshooting.md | 105 +++ docs/explanation/projections-model.md | 141 +++ docs/explanation/returns-and-performance.md | 127 +++ docs/getting-started.md | 243 +++++ docs/guides/audit-against-brokerage.md | 244 ++++++ docs/guides/classify-holdings.md | 114 +++ docs/guides/customize-the-tui.md | 73 ++ docs/guides/offline-and-refresh.md | 80 ++ docs/guides/periodic-review.md | 177 ++++ docs/guides/plan-retirement.md | 310 +++++++ docs/guides/read-your-portfolio.md | 190 ++++ docs/guides/set-up-accounts.md | 101 +++ docs/guides/set-up-your-portfolio.md | 193 ++++ docs/guides/snapshots-and-history.md | 149 ++++ docs/guides/track-contributions.md | 117 +++ docs/images/fidelity-positions-download.png | Bin 0 -> 69243 bytes docs/reference/cli/analysis.md | 51 ++ docs/reference/cli/audit.md | 58 ++ docs/reference/cli/cache.md | 35 + docs/reference/cli/compare.md | 50 ++ docs/reference/cli/contributions.md | 55 ++ docs/reference/cli/divs.md | 37 + docs/reference/cli/doctor.md | 102 +++ docs/reference/cli/earnings.md | 45 + docs/reference/cli/enrich.md | 48 + docs/reference/cli/etf.md | 47 + docs/reference/cli/exposure.md | 44 + docs/reference/cli/history.md | 57 ++ docs/reference/cli/import.md | 51 ++ docs/reference/cli/index.md | 88 ++ docs/reference/cli/interactive.md | 43 + docs/reference/cli/lookup.md | 36 + docs/reference/cli/milestones.md | 52 ++ docs/reference/cli/options.md | 62 ++ docs/reference/cli/perf.md | 52 ++ docs/reference/cli/portfolio.md | 49 ++ docs/reference/cli/projections.md | 64 ++ docs/reference/cli/quote.md | 46 + docs/reference/cli/review.md | 52 ++ docs/reference/cli/snapshot.md | 49 ++ docs/reference/cli/splits.md | 41 + docs/reference/cli/version.md | 36 + docs/reference/config/accounts-srf.md | 106 +++ docs/reference/config/environment.md | 68 ++ docs/reference/config/keys-srf.md | 72 ++ docs/reference/config/metadata-srf.md | 87 ++ docs/reference/config/portfolio-srf.md | 159 ++++ docs/reference/config/projections-srf.md | 142 +++ docs/reference/config/theme-srf.md | 60 ++ docs/reference/config/transaction-log-srf.md | 68 ++ docs/reference/config/watchlist-srf.md | 45 + docs/reference/providers.md | 70 ++ docs/reference/tui.md | 76 ++ 59 files changed, 5154 insertions(+), 837 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/explanation/caching.md create mode 100644 docs/explanation/concepts.md create mode 100644 docs/explanation/data-providers.md create mode 100644 docs/explanation/faq-troubleshooting.md create mode 100644 docs/explanation/projections-model.md create mode 100644 docs/explanation/returns-and-performance.md create mode 100644 docs/getting-started.md create mode 100644 docs/guides/audit-against-brokerage.md create mode 100644 docs/guides/classify-holdings.md create mode 100644 docs/guides/customize-the-tui.md create mode 100644 docs/guides/offline-and-refresh.md create mode 100644 docs/guides/periodic-review.md create mode 100644 docs/guides/plan-retirement.md create mode 100644 docs/guides/read-your-portfolio.md create mode 100644 docs/guides/set-up-accounts.md create mode 100644 docs/guides/set-up-your-portfolio.md create mode 100644 docs/guides/snapshots-and-history.md create mode 100644 docs/guides/track-contributions.md create mode 100644 docs/images/fidelity-positions-download.png create mode 100644 docs/reference/cli/analysis.md create mode 100644 docs/reference/cli/audit.md create mode 100644 docs/reference/cli/cache.md create mode 100644 docs/reference/cli/compare.md create mode 100644 docs/reference/cli/contributions.md create mode 100644 docs/reference/cli/divs.md create mode 100644 docs/reference/cli/doctor.md create mode 100644 docs/reference/cli/earnings.md create mode 100644 docs/reference/cli/enrich.md create mode 100644 docs/reference/cli/etf.md create mode 100644 docs/reference/cli/exposure.md create mode 100644 docs/reference/cli/history.md create mode 100644 docs/reference/cli/import.md create mode 100644 docs/reference/cli/index.md create mode 100644 docs/reference/cli/interactive.md create mode 100644 docs/reference/cli/lookup.md create mode 100644 docs/reference/cli/milestones.md create mode 100644 docs/reference/cli/options.md create mode 100644 docs/reference/cli/perf.md create mode 100644 docs/reference/cli/portfolio.md create mode 100644 docs/reference/cli/projections.md create mode 100644 docs/reference/cli/quote.md create mode 100644 docs/reference/cli/review.md create mode 100644 docs/reference/cli/snapshot.md create mode 100644 docs/reference/cli/splits.md create mode 100644 docs/reference/cli/version.md create mode 100644 docs/reference/config/accounts-srf.md create mode 100644 docs/reference/config/environment.md create mode 100644 docs/reference/config/keys-srf.md create mode 100644 docs/reference/config/metadata-srf.md create mode 100644 docs/reference/config/portfolio-srf.md create mode 100644 docs/reference/config/projections-srf.md create mode 100644 docs/reference/config/theme-srf.md create mode 100644 docs/reference/config/transaction-log-srf.md create mode 100644 docs/reference/config/watchlist-srf.md create mode 100644 docs/reference/providers.md create mode 100644 docs/reference/tui.md diff --git a/README.md b/README.md index 66680ef..d42248e 100644 --- a/README.md +++ b/README.md @@ -51,15 +51,37 @@ zfin i -s AAPL # start with a symbol, no portfolio Building from source requires Zig 0.16.0. +## Documentation + +Full user documentation lives in [`docs/`](docs/README.md) and is built +around the runnable example portfolios in [`examples/`](examples/). + +- **New here?** [Getting started](docs/getting-started.md) -- install, + configure, and run your first commands. +- **Learn the model:** [Core concepts](docs/explanation/concepts.md). +- **Do a task (the user manual):** + - [Build your portfolio](docs/guides/set-up-your-portfolio.md) + - [Classify your holdings](docs/guides/classify-holdings.md) and [map your accounts](docs/guides/set-up-accounts.md) + - [Read your portfolio](docs/guides/read-your-portfolio.md) + - [Plan for retirement](docs/guides/plan-retirement.md) + - [Snapshots and history](docs/guides/snapshots-and-history.md), [track contributions](docs/guides/track-contributions.md) + - [Audit against your brokerage](docs/guides/audit-against-brokerage.md) + - [Customize the TUI](docs/guides/customize-the-tui.md), [offline use and refreshing data](docs/guides/offline-and-refresh.md) +- **Look something up (reference):** + - [CLI commands](docs/reference/cli/index.md) + - Config files: [`portfolio.srf`](docs/reference/config/portfolio-srf.md), [`accounts.srf`](docs/reference/config/accounts-srf.md), [`metadata.srf`](docs/reference/config/metadata-srf.md), [`projections.srf`](docs/reference/config/projections-srf.md), and [more](docs/reference/config/environment.md) + - [The interactive TUI](docs/reference/tui.md) + - [Data providers and API keys](docs/reference/providers.md) +- **Understand the why (explanation):** [caching](docs/explanation/caching.md), [data providers](docs/explanation/data-providers.md), [returns](docs/explanation/returns-and-performance.md), [the projection model](docs/explanation/projections-model.md), [FAQ](docs/explanation/faq-troubleshooting.md). + ## Data providers -zfin aggregates data from multiple free-tier APIs. Each provider is used for the data it does best, and aggressive caching keeps usage well within free-tier limits. - -### Provider summary - primary providers +zfin aggregates data from multiple free-tier APIs, using each for what +it does best, with aggressive caching to stay within free-tier limits. | Data type | Provider | Auth | Free-tier limit | Cache TTL | |-----------------------|------------------|----------------------|----------------------------|--------------| -| Daily candles (OHLCV) | Tiingo | `TIINGO_API_KEY` | 1,000 req/day | 23h45m | +| Daily candles (OHLCV) | Tiingo | `TIINGO_API_KEY` | 1,000 req/day, 50 req/hour | 23h45m | | Real-time quotes | Yahoo Finance | None required | Unofficial | Never cached | | Quote fallback | TwelveData | `TWELVEDATA_API_KEY` | 8 req/min, 800/day | Never cached | | Dividends | Polygon | `POLYGON_API_KEY` | 5 req/min | 14 days | @@ -69,832 +91,12 @@ zfin aggregates data from multiple free-tier APIs. Each provider is used for the | ETF profiles | SEC EDGAR | `ZFIN_USER_EMAIL` | 10 req/sec | ~90 days | | Classification | Wikidata + EDGAR | `ZFIN_USER_EMAIL` | No per-day quota | Long-lived | -### Tiingo - -**Used for:** daily candles (primary provider for all symbols), supplementary dividend and split data. - -- Endpoint: `https://api.tiingo.com/tiingo/daily/{symbol}/prices` -- Free tier: 1,000 requests per day, no per-minute restriction. -- Covers stocks, ETFs, and mutual funds. Mutual fund NAVs are available after midnight ET. -- Candles are fetched with a fixed 2000-01-01 start date so the cache supports `--as-of` projections back to the earliest imported portfolio data (typically 2014) with full 10Y trailing-return windows. -- The same response carries per-row `divCash` and `splitFactor`. We extract these as a free side benefit and merge them into the dividend/split caches alongside Polygon's primary view -- this rescues entries Polygon's reference endpoints miss (e.g. SPYM's 2017-10-16 4:1 split). - -### TwelveData - -**Used for:** real-time quote fallback (after Yahoo). - -- Endpoint: `https://api.twelvedata.com/quote` -- Free tier: 8 API credits per minute, 800 per day. -- TwelveData was previously used for candles but is no longer in the candle pipeline -- its `adj_close` values were unreliable for split-adjustment math. Yahoo is the candle fallback now. - -### Polygon - -**Used for:** dividend and stock split data, both historical and forward-looking. - -- Endpoints: `https://api.polygon.io/v3/reference/dividends` and `/v3/reference/splits` -- Free tier: 5 requests per minute, unlimited daily. Full historical data. -- Dividend endpoint uses cursor-based pagination (automatically followed). -- Provides dividend type classification (regular, special, supplemental) and richer metadata than Tiingo (`pay_date`, `record_date`, `currency`). -- Polygon is **primary** for dividends and splits because it carries forward-looking declared events (e.g. ARCC's next ex-dividend date several months out) that Tiingo's price-series response cannot provide. Tiingo merges in supplementary entries for historical events Polygon's reference endpoints occasionally miss. - -### CBOE - -**Used for:** options chains. - -- Endpoint: `https://cdn.cboe.com/api/global/delayed_quotes/options/{SYMBOL}.json` -- No API key required. Data is 15-minute delayed during market hours. -- Returns all expirations with full chains including greeks (delta, gamma, theta, vega), bid/ask, volume, open interest, and implied volatility. -- OCC option symbols are parsed to extract expiration, strike, and contract type. - -### FMP (Financial Modeling Prep) - -**Used for:** earnings history (historical actuals + analyst consensus estimates + upcoming). - -- Endpoint: `https://financialmodelingprep.com/stable/earnings?symbol={SYMBOL}` -- Free tier: 250 requests per day. With the 30-day cache TTL, a 50-symbol portfolio averages ~2 requests/day. -- History depth: full — often back to the 1980s for long-listed tickers. -- Coverage: US stocks with real earnings. ETFs, mutual funds, CUSIPs, and some dual-class shares (BRK.B, GOOG) return 402 on the free tier and show up as "no earnings data" in the UI — a documented limitation, not a bug. - -### SEC EDGAR + Wikidata - -**Used for:** ETF profiles (NPORT-P holdings, sector weights, AUM, inception -dates) and the `enrich` flow that bootstraps `metadata.srf` (sector / industry -/ country / asset-class classification). - -- Endpoints: - - `https://data.sec.gov/...` -- XBRL company facts, NPORT-P primary - documents, mutual-fund ticker map. - - `https://www.wikidata.org/sparql` -- sector / industry / country - statements. -- Free, but the SEC requires a contact email in the User-Agent header. zfin - reads this from `ZFIN_USER_EMAIL` (`.env` or environment). Without it, ETF - profiles and `enrich` are unavailable; everything else still works. -- The SEC caps requests at 10/sec; the rate limiter respects this. Wikidata - has no per-day quota. -- The `enrich` command queries Wikidata first (rich classification metadata); - when Wikidata has no entry for a symbol (common for managed funds, UITs), - EDGAR's mutual-fund ticker map is the fallback. Symbols that miss both fall - through as TODO entries the user fills in by hand. - -## API keys - -Set keys as environment variables or in a `.env` file (searched in the executable's parent directory, then cwd): - -```bash -TIINGO_API_KEY=your_key # Required for candles (primary provider) -TWELVEDATA_API_KEY=your_key # Quote fallback (after Yahoo) -POLYGON_API_KEY=your_key # Required for dividends/splits (total returns) -FMP_API_KEY=your_key # Required for earnings data -ZFIN_USER_EMAIL=you@example.com # Required for ETF profiles + `enrich` - # (SEC EDGAR mandates a User-Agent contact) -``` - -The cache directory defaults to `~/.cache/zfin` and can be overridden with `ZFIN_CACHE_DIR`. - -Not all keys are required. Without a key, the corresponding data simply won't be available: - -| Key | Without it | -|----------------------|-------------------------------------------------------------------------------------------| -| `TIINGO_API_KEY` | Candles fall back to Yahoo only; some symbols (especially mutual funds) won't work | -| `TWELVEDATA_API_KEY` | No quote fallback after Yahoo | -| `POLYGON_API_KEY` | No forward-looking dividends; trailing total returns may use only Tiingo's view | -| `FMP_API_KEY` | No earnings data (tab disabled) | -| `ZFIN_USER_EMAIL` | No ETF profiles; `enrich` cannot bootstrap metadata. (Used as the EDGAR User-Agent value) | - -CBOE options require no API key. - -## Caching strategy - -Every data fetch follows the same pattern: - -1. Check local cache (`~/.cache/zfin/{SYMBOL}/{data_type}.srf`) -2. If cached file exists and is within TTL -- deserialize and return (no network) -3. Otherwise -- fetch from provider -- serialize to cache -- return - -Cache files use [SRF](https://github.com/lobo/srf) (Simple Record Format), a line-oriented key-value format. Freshness is determined by file modification time vs. the TTL for that data type. - -| Data type | TTL | Rationale | -|---------------|--------------|-----------------------------------------------------| -| Daily candles | 23h45m | Slightly under 24h for cron jitter tolerance | -| Dividends | 14 days | Declared well in advance | -| Splits | 14 days | Rare corporate events | -| Options | 1 hour | Prices change continuously during market hours | -| Earnings | 30 days* | Quarterly events; smart refresh after announcements | -| ETF profiles | 30 days | Holdings/weights change slowly | -| Quotes | Never cached | Intended for live price checks | - -**Earnings smart refresh:** Even within the 30-day TTL, cached earnings are automatically re-fetched when an earnings date has passed but the cache still has no actual results for it. This ensures results appear promptly after an announcement without wasteful daily polling. - -Manual refresh (`r` / `F5` in TUI) invalidates the cache for the current tab's data before re-fetching. - -### Rate limiting - -Each provider has a client-side token-bucket rate limiter that prevents exceeding free-tier limits: - -| Provider | Rate limit | -|---------------|---------------------| -| Tiingo | 1,000/day | -| TwelveData | 8/minute | -| Polygon | 5/minute | -| FMP | 250/day | -| CBOE | 30/minute | -| SEC EDGAR | 10/second | -| Wikidata | (no enforced limit) | - -The limiter blocks until a token is available, spreading bursts of requests automatically rather than failing with 429 errors. - -## CLI commands - -``` -zfin [args] - -Commands: - perf Trailing returns (1yr/3yr/5yr/10yr, price + total) - quote Real-time quote - history Last 30 days price history - divs Dividend history with TTM yield - splits Split history - options Options chains (all expirations) - earnings Earnings history and upcoming events - etf ETF profile (expense ratio, holdings, sectors) - portfolio Portfolio analysis from .srf file - snapshot [opts] Write a daily portfolio snapshot to history/ - compare [] - Compare portfolio state across two dates - cache stats Show cached symbols - cache clear Delete all cached data - interactive, i Launch interactive TUI - help Show usage -``` - -### compare - -Compare the portfolio at two points in time. Useful for answering -"how am I doing since X" without the noise of the full portfolio -display. - -``` -zfin compare Compare snapshot at DATE vs current live portfolio -zfin compare Compare two historical snapshots -``` - -Arguments can be given in any order — the command always displays -older → newer. Dates are `YYYY-MM-DD`. Snapshots come from -`history/YYYY-MM-DD-portfolio.srf` files produced by -`zfin snapshot` (typically run via cron). - -**Output:** - -- **Liquid:** raw value change — includes any contributions or - withdrawals made between the two dates (adjusting for flows is - out of scope). -- **Per-symbol price change:** for symbols held on *both* dates. - Sorted by % change descending (biggest winners first). The dollar - column uses the shares-held-throughout floor (`min(shares_then, - shares_now)`) so newly-added shares don't inflate it and sold - shares don't deflate it. -- **Hidden count:** positions added or removed between the two dates - are counted but not rendered. - -On a missing snapshot date, the command prints the nearest earlier -and later available dates to stderr and exits 1 — no silent -snapping. - -Example output shape (values illustrative): - -``` -$ zfin compare 2024-01-15 2024-03-15 -Portfolio comparison: 2024-01-15 → 2024-03-15 (60 days) - -Liquid: $100,000.00 → $105,000.00 +$5,000.00 +5.00% - -Per-symbol price change (5 held throughout) - FOO $40.00 → $44.00 +10.00% +$400.00 - BAR $100.00 → $105.00 +5.00% +$250.00 - ... - BAZ $50.00 → $48.00 -4.00% -$80.00 - -(1 added, 1 removed since 2024-01-15 — hidden) -``` - - -### Chart export (`--export-chart `) - -The `quote` and `projections` commands support `--export-chart ` to render their charts as PNG files (1920x1080) instead of emitting text. Useful for write-ups, sharing, or capturing a back-dated projection without screenshot-and-crop. - -``` -zfin quote AAPL --export-chart aapl.png -zfin projections --as-of 1Y --overlay-actuals --export-chart proj.png -``` - -The exported image uses the TUI's default theme. When `--export-chart` is set, no other text output is emitted — the command exits after writing the file. Only the default `projections` mode is supported; `--convergence`, `--return-backtest`, and `--vs` reject the flag (their charts still need PNG plumbing). The `history` command's portfolio-value chart is also not yet exportable — it uses a single-series braille format that doesn't share the z2d pipeline used by `quote` and `projections`. - -### Interactive TUI flags - -``` -zfin i [options] - - -p, --portfolio Portfolio file (.srf format) - -w, --watchlist Watchlist file (default: watchlist.srf if present) - -s, --symbol Start with a specific symbol - --default-keys Print default keybindings config to stdout - --default-theme Print default theme config to stdout -``` - -If no portfolio or symbol is specified and `portfolio.srf` exists in the current directory, it is loaded automatically. - -## Interactive TUI - -The TUI has eight tabs: Portfolio, Analysis, Projections, History, Quote, Performance, Earnings, and Options. - -### Tabs - -**Portfolio** -- navigable list of positions with market value, gain/loss, weight, and purchase date. Multi-lot positions can be expanded to show individual lots with per-lot gain/loss, capital gains indicator (ST/LT), and account name. Press `a` to open an account-filter picker (with `/` search). - -**Analysis** -- portfolio breakdown by asset class, sector, geographic region, account, and tax type. Uses classification data from `metadata.srf` and account tax types from `accounts.srf`. Displays horizontal bar charts with sub-character precision using Unicode block elements. - -**Projections** -- Monte Carlo retirement projection with percentile bands. Press `d` to set an as-of date (back-date the projection to a historical snapshot), `o` to overlay realized actuals from snapshots / `imported_values.srf` on top of the bands, `z` to toggle auto-zoom on the overlay (chart x-axis defaults to roughly `[as_of, today + actuals_span]` so a short actuals line isn't squashed into the start of a 50-year horizon), `v` to toggle the chart vs the text-only report, and `e` to toggle simulated lifecycle events (RMDs, lump-sum withdrawals). Esc clears an active as-of override. - -**History** -- portfolio value over time, sourced from snapshot files in `/history/` plus optional `imported_values.srf`. Cycle the metric column with `m` (liquid / total / contributions / etc.) and the time-bucket resolution with `t` (week / month / quarter / year). Press `s` (or space) to mark a row for compare; mark a second row, then `c` to commit a side-by-side compare against the live portfolio. Esc cancels an in-flight compare. - -**Quote** -- current price, OHLCV, daily change, and a 60-day ASCII chart with recent history table. - -**Performance** -- trailing returns using two methodologies (as-of-date and month-end), matching Morningstar's "Trailing Returns" and "Performance" pages respectively. Shows price-only and total return (with dividend reinvestment) using whichever dividend data is available -- Polygon (richer metadata, forward-looking entries) and Tiingo (extracted from candle responses, historical only) are merged. Also shows risk metrics (volatility, Sharpe ratio, max drawdown). - -**Earnings** -- historical and upcoming earnings events with EPS estimate/actual, surprise amount and percentage. Future events are dimmed. Tab is disabled for ETFs. - -**Options** -- all expirations in a navigable list. Expand any expiration to see calls and puts inline. Calls and puts sections are independently collapsible. Near-the-money filter limits strikes shown (default +/- 8, adjustable with Ctrl+1-9). ITM strikes are marked with `|`. Monthly expirations display in normal color, weeklies are dimmed. - -### Keybindings - -All keybindings are configurable via `~/.config/zfin/keys.srf`. Generate the default config: - -```bash -zfin i --default-keys > ~/.config/zfin/keys.srf -``` - -The generated file has two parts: a global section (keys that work in every tab) and a per-tab section (keys that only fire when that tab is active). Tab-local bindings cannot override globally-bound keys — zfin refuses to start if your config creates that conflict. - -Default global keybindings: - -| Key | Action | -|------------------------|------------------------------------------------------| -| `q`, `Ctrl+c` | Quit | -| `r`, `F5` | Refresh current tab (invalidates cache) | -| `R` | Reload portfolio from disk (no network) | -| `h`, Left, Shift+Tab | Previous tab | -| `l`, Right, Tab | Next tab | -| `1`-`8` | Jump to tab N | -| `j`, Down | Select next row | -| `k`, Up | Select previous row | -| `g` | Scroll to top | -| `G` | Scroll to bottom | -| `Ctrl+d` | Half-page down | -| `Ctrl+u` | Half-page up | -| `PageDown`, `Ctrl+f` | Page down | -| `PageUp`, `Ctrl+b` | Page up | -| `/` | Symbol input prompt | -| `?` | Help screen | - -Default tab-local keybindings (only active on the matching tab): - -| Tab | Key | Action | -|-------------|--------------------|----------------------------------------------------------| -| Portfolio | `Enter` | Expand/collapse position | -| Portfolio | `>` / `<` | Sort by next / previous column | -| Portfolio | `o` | Reverse sort direction | -| Portfolio | `a` | Open account-filter picker (`/` to search inside picker) | -| Portfolio | `Esc` | Clear active account filter | -| Portfolio | `s`, `Space` | Select symbol (sets active symbol for other tabs) | -| Quote | `[` / `]` | Previous / next chart timeframe | -| Options | `Enter` | Expand/collapse expiration or section | -| Options | `c` / `p` | Toggle all calls / puts collapsed | -| Options | `Ctrl+1`-`Ctrl+9` | Set near-the-money filter to +/- N strikes | -| History | `Enter` | Expand/collapse tier | -| History | `m` | Cycle metric column | -| History | `t` | Cycle time-bucket resolution | -| History | `s`, `Space` | Mark / unmark row for compare | -| History | `c` | Commit compare (after two rows marked) | -| History | `Esc` | Cancel in-flight compare selection | -| Projections | `d` | Set as-of date prompt | -| Projections | `Esc` | Clear as-of date | -| Projections | `o` | Toggle realized-actuals overlay | -| Projections | `z` | Toggle overlay auto-zoom (clamp x-axis to overlay span) | -| Projections | `v` | Toggle chart vs text-only report | -| Projections | `e` | Toggle simulated lifecycle events | - -Mouse: scroll wheel navigates, left-click selects rows and switches tabs. - -### Theme - -The TUI uses a dark theme inspired by Monokai/opencode. Customize via `~/.config/zfin/theme.srf`: - -```bash -zfin i --default-theme > ~/.config/zfin/theme.srf -``` - -Colors are specified as `#rrggbb` hex values. The theme uses RGB colors (not terminal color indices) to work correctly with transparent terminal backgrounds. - -## Portfolio format - -Portfolios are [SRF](https://github.com/lobo/srf) files with one lot per line. Each lot is a comma-separated list of `key::value` pairs (numbers use `key:num:value`). - -``` -#!srfv1 -# Stocks/ETFs -symbol::VTI,shares:num:100,open_date::2024-01-15,open_price:num:220.50,account::Brokerage -symbol::AAPL,shares:num:50,open_date::2024-03-01,open_price:num:170.00,account::Roth IRA -symbol::AAPL,shares:num:25,open_date::2023-06-15,open_price:num:155.00,account::Roth IRA - -# Closed lot (sold) -symbol::AMZN,shares:num:10,open_date::2022-03-15,open_price:num:150.25,close_date::2024-01-15,close_price:num:185.50 - -# DRIP lots (summarized as ST/LT groups in the UI) -symbol::VTI,shares:num:0.234,open_date::2024-06-15,open_price:num:267.50,drip::true,account::Brokerage - -# CUSIP with ticker alias (401k CIT share class) -symbol::02315N600,shares:num:1200,open_date::2022-01-01,open_price:num:140.00,ticker::VTTHX,account::Fidelity 401k,note::VANGUARD TARGET 2035 - -# Manual price override (for securities without API coverage) -symbol::NON40OR52,shares:num:500,open_date::2023-01-01,open_price:num:155.00,price:num:163.636,price_date::2026-02-27,account::Fidelity 401k,note::CIT SHARE CLASS - -# Options -security_type::option,symbol::AAPL 250321C00200000,shares:num:-2,open_date::2025-01-15,open_price:num:12.50,account::Brokerage - -# CDs -security_type::cd,symbol::912797KR0,shares:num:10000,open_date::2024-06-01,open_price:num:10000,maturity_date::2025-06-01,rate:num:5.25,account::Brokerage,note::6-Month T-Bill - -# Cash -security_type::cash,shares:num:15000,account::Brokerage -security_type::cash,shares:num:5200.50,account::Roth IRA,note::Money market settlement - -# Illiquid assets -security_type::illiquid,symbol::HOME,shares:num:450000,open_date::2020-06-01,open_price:num:350000,note::Primary residence (Zillow est.) - -# Watchlist (track price only, no position) -security_type::watch,symbol::NVDA -security_type::watch,symbol::TSLA -``` - -### Lot fields - -| Field | Type | Required | Description | -|-----------------|--------|----------|--------------------------------------------------------------------------| -| `symbol` | string | Yes* | Ticker symbol or CUSIP. *Optional for `cash` lots. | -| `shares` | number | Yes | Number of shares (or face value for cash/CDs) | -| `open_date` | string | Yes** | Purchase date (YYYY-MM-DD). **Not required for cash/watch. | -| `open_price` | number | Yes** | Purchase price per share. **Not required for cash/watch. | -| `close_date` | string | No | Sale date (null = open lot) | -| `close_price` | number | No | Sale price per share | -| `security_type` | string | No | `stock` (default), `option`, `cd`, `cash`, `illiquid`, `watch` | -| `account` | string | No | Account name (e.g. "Roth IRA", "Brokerage") | -| `note` | string | No | Descriptive note (shown in cash/CD/illiquid tables) | -| `ticker` | string | No | Ticker alias for price fetching (overrides `symbol` for API calls) | -| `price` | number | No | Manual price override (fallback when API has no coverage) | -| `price_date` | string | No | Date of the manual price (YYYY-MM-DD, for staleness display) | -| `drip` | string | No | `true` if lot is from dividend reinvestment (summarized as ST/LT groups) | -| `maturity_date` | string | No | CD maturity date (YYYY-MM-DD) | -| `rate` | number | No | Interest rate for CDs (e.g. 5.25 = 5.25%) | - -### Security types - -- **stock** (default) -- Stocks, ETFs, and mutual funds. Prices are fetched from Tiingo (primary) or Yahoo (candle fallback). Positions are aggregated by symbol and shown with gain/loss. -- **option** -- Option contracts. Shown in a separate "Options" section. Shares can be negative for short positions. -- **cd** -- Certificates of deposit. Shown sorted by maturity date with rate and face value. -- **cash** -- Cash, money market, and settlement balances. Shown grouped by account with optional notes. -- **illiquid** -- Illiquid assets (real estate, vehicles, etc.). Shown in a separate section. Not included in the liquid portfolio total; contributes to Net Worth. -- **watch** -- Watchlist items. No position, just tracks the price. Shown at the bottom of the portfolio tab. - -### Price resolution - -For stock lots, prices are resolved in this order: - -1. **Live API** -- Latest close from cached candles (Tiingo, with Yahoo as candle fallback) -2. **Manual price** -- `price::` field on the lot (for securities without API coverage, e.g. 401k CIT share classes) -3. **Average cost** -- Falls back to the position's `open_price` as a last resort - -Manual-priced rows are shown in warning color (yellow) so you know the price may be stale. The `price_date::` field helps you track when the price was last updated. - -### Ticker aliases - -Some securities (like 401k CIT share classes) use CUSIPs as identifiers but have a retail equivalent ticker for price fetching. Use `ticker::` to specify the API ticker: - -``` -symbol::02315N600,ticker::VTTHX,... -``` - -The `symbol::` is used as the display identifier and for classification lookups. The `ticker::` is used for API price fetching. If the CUSIP and retail ticker have different NAVs (common for CIT vs retail fund), use `price::` instead. - -### CUSIP lookup - -Use the `lookup` command to resolve CUSIPs to tickers via OpenFIGI: - -```bash -zfin lookup 459200101 # -> IWM (iShares Russell 2000 ETF) -``` - -### DRIP lots - -Lots marked with `drip::true` are summarized as ST (short-term) and LT (long-term) groups in the position detail view, rather than listing every small reinvestment lot individually. The grouping is based on the 1-year capital gains threshold. - -### Watchlist - -Watchlist symbols can be defined as `security_type::watch` lots in the portfolio file, or in a separate watchlist file (`-w` flag). They appear at the bottom of the portfolio tab showing the cached price. - -## Classification metadata (metadata.srf) - -The `metadata.srf` file provides classification data for portfolio analysis. It maps symbols to asset class, sector, and geographic region. Place it in the same directory as the portfolio file. - -``` -#!srfv1 -# Individual stock: single classification at 100% -symbol::AMZN,sector::Technology,geo::US,asset_class::US Large Cap - -# ETF: inherits sector from holdings, but classified by asset class -symbol::VTI,asset_class::US Large Cap,geo::US - -# International ETF -symbol::VXUS,asset_class::International Developed,geo::International Developed - -# Target date fund: blended allocation (percentages should sum to ~100) -symbol::02315N600,asset_class::US Large Cap,pct:num:55 -symbol::02315N600,asset_class::International Developed,pct:num:20 -symbol::02315N600,asset_class::Bonds,pct:num:15 -symbol::02315N600,asset_class::Emerging Markets,pct:num:10 - -# BDC / REIT / specialty -symbol::ARCC,sector::Financials,geo::US,asset_class::US Large Cap -``` - -### Classification fields - -| Field | Type | Required | Description | -|---------------|--------|----------|---------------------------------------------------------------------------| -| `symbol` | string | Yes | Ticker symbol or CUSIP (must match `symbol::` or `ticker::` in portfolio) | -| `asset_class` | string | No | e.g. "US Large Cap", "Bonds", "Cash & CDs", "Emerging Markets" | -| `sector` | string | No | e.g. "Technology", "Healthcare", "Financials" | -| `geo` | string | No | e.g. "US", "International Developed", "Emerging Markets" | -| `pct` | number | No | Percentage weight for this entry (default 100). Use for blended funds. | - -For single-asset-class securities (individual stocks, single-focus ETFs), one line at the default 100% is sufficient. For multi-asset-class funds (target date, balanced), add multiple lines for the same symbol with `pct:num:` values that sum to approximately 100. - -Cash and CD lots are automatically classified as "Cash & CDs" without needing metadata entries. - -### Bootstrapping metadata - -Use the `enrich` command to generate a starting `metadata.srf` from Wikidata -+ SEC EDGAR data. Requires `ZFIN_USER_EMAIL` set so EDGAR will accept the -request. - -```bash -# Enrich an entire portfolio (generates full metadata.srf) -zfin enrich portfolio.srf > metadata.srf - -# Enrich a single symbol and append to existing metadata.srf -zfin enrich SCHD >> metadata.srf -``` - -When given a file path, it fetches all stock symbols and outputs a complete -SRF file with headers. When given a symbol, it outputs just the -classification lines (no header), so you can append directly with `>>`. -Wikidata is queried first; symbols not found in Wikidata fall through to -EDGAR's mutual-fund ticker map. Anything that misses both shows up as a -TODO entry to fill in by hand. - -## Account metadata (accounts.srf) - -The `accounts.srf` file maps account names to tax types for the tax type breakdown in portfolio analysis. Place it in the same directory as the portfolio file. - -``` -#!srfv1 -account::Brokerage,tax_type::taxable -account::Roth IRA,tax_type::roth -account::Traditional IRA,tax_type::traditional -account::Fidelity 401k,tax_type::traditional -account::HSA,tax_type::hsa -``` - -### Account fields - -| Field | Type | Required | Description | -|------------|--------|----------|-----------------------------------------------------------------| -| `account` | string | Yes | Account name (must match `account::` in portfolio lots exactly) | -| `tax_type` | string | Yes | Tax classification (see below) | - -### Tax types - -| Value | Display label | -|---------------|-----------------------| -| `taxable` | Taxable | -| `roth` | Roth (Post-Tax) | -| `traditional` | Traditional (Pre-Tax) | -| `hsa` | HSA (Triple Tax-Free) | -| (other) | Shown as-is | - -Accounts not listed in `accounts.srf` appear as "Unknown" in the tax type breakdown. - -## Projections configuration (projections.srf) - -The `projections` command runs Monte-Carlo-style historical -simulations of your retirement portfolio over the Shiller dataset -(1871–present). Configuration lives in `projections.srf` next to -`portfolio.srf`. The file is optional — without it, the command -runs with sensible defaults (20/30/45-year horizons, 90/95/99% -confidence levels, no accumulation phase). - -```srf -#!srfv1 -# Asset allocation target (drives sim stock/bond blend) -type::config,target_stock_pct:num:80 - -# Distribution-phase horizons (years to project past retirement) -type::config,horizon:num:25 -type::config,horizon:num:35 - -# Or: horizon-by-age, resolves to "years until oldest hits this age" -type::config,horizon_age:num:95 - -# Birthdates (drive horizon_age + life-event timing + retirement_age) -type::birthdate,date::1981-04-12 -type::birthdate,date::1983-09-08,person:num:2 - -# Life events (positive = income, negative = expense). See below. -type::event,name::Social Security (Pat),start_age:num:70,person:num:1,amount:num:38400 -type::event,name::College Tuition,start_age:num:50,person:num:1,duration:num:4,amount:num:-55000 -``` - -### How the simulation runs - -The simulation always operates in two phases: - -- **Accumulation phase** — contributions added each year, no - spending. Length is determined by your retirement-date input - (see below). When you have no input configured, this phase has - zero years (already-retired view). -- **Distribution phase** — annual spending withdrawn (CPI-adjusted - by default), no contributions. Length is the configured - `horizon`. - -Life events apply to both phases. - -### Two ways to ask the question - -`projections.srf` accepts two retirement-planning inputs that can -be set independently or together. Each shapes a different display -block: - -#### Target retirement date - -Set either `retirement_age` (resolved against the oldest -birthdate) or `retirement_at` (an absolute calendar date) to -anchor a retirement boundary. The display answers -"given my retirement date, what can I spend?" - -```srf -type::config,retirement_age:num:65 # oldest person hits 65 -# or -type::config,retirement_at::2046-04-12 - -type::config,annual_contribution:num:80000 -type::config,contribution_inflation_adjusted:bool:true -``` - -When both are set, `retirement_at` wins. Output renders the -**Accumulation phase** block: median portfolio at retirement, -p10–p90 range, and the dated headline "Years until possible -retirement: N (DATE, ages A/B)" line. - -#### Target spending - -Set `target_spending` to anchor a desired retirement income. The -display answers "given my desired spending, when can I retire?" - -```srf -type::config,target_spending:num:80000 -type::config,target_spending_inflation_adjusted:bool:true -``` - -Output renders an **Earliest retirement** grid (one cell per -horizon × confidence) showing the earliest year that supports the -target spending at that confidence over that distribution -horizon. One cell from the grid is **promoted** into the -Accumulation phase block as the headline retirement line. - -The default promotion rule walks horizons longest → shortest at -99% confidence (most conservative), preferring the longest -horizon whose end year keeps the oldest configured person under -age 100. Override the default with a per-horizon annotation: - -```srf -type::config,horizon:num:35,retirement_target:num:95 -``` - -This forces "use the 35yr × 95% cell as the headline." Allowed -values are `90`, `95`, `99`. At most one horizon may carry the -annotation; configuring more than one drops them all and falls -back to the default rule. - -When the promoted cell is infeasible (no value of -`accumulation_years` ≤ 50 sustains the target spending), the -headline renders "Years until possible retirement: not feasible" -and the contribution / median lines are suppressed. Cells in the -grid that hit the same wall render "infeasible" in red. - -#### Both inputs configured - -When both a target retirement date and a target spending are -configured, both display blocks render back-to-back. The -configured retirement date wins for the headline; the Earliest -retirement grid is rendered below for comparison ("you set 2046; -at 95% confidence over 30 years you could retire as early as -YYYY"). - -#### Neither configured - -Distribution-only mode — appropriate for already-retired users. -The Accumulation phase block reduces to a single soft -"Years until possible retirement: none" line; everything else -behaves like the legacy projection display. - -### Realized actuals overlay (`--overlay-actuals`) - -Run `zfin projections --as-of --overlay-actuals` to plot the -**realized portfolio trajectory** from `` up to today on top of -the projected percentile bands. Answers the question "how accurate -were my past projections compared to reality?" - -The TUI is the high-fidelity surface — open the projections tab, -press `d` to set the as-of date, then press `o` to toggle the -overlay. The CLI prints a tip pointing at the TUI; the braille chart -itself doesn't render the overlay (the resolution doesn't do justice -to a 12+ year actuals trajectory laid against percentile bands). - -**The overlay reads from two sources, snapshot-precedence:** - -1. **Native snapshots** in `/history/*-portfolio.srf` - — produced by `zfin snapshot`. Highest fidelity (full lot-level - state, exact totals). -2. **Imported values** in `/history/imported_values.srf` - — a hand-maintained back-history file with one `liquid::` total - per date. Useful for backfilling a historical record from - spreadsheet data, statements, etc. Snapshots win on overlapping - dates. - -**As-of resolution against either source.** `--as-of ` -resolves first against the snapshot directory (nearest-earlier -`*-portfolio.srf`); if no snapshot exists at or before the date, -it falls back to `imported_values.srf`. This means you can run -`zfin projections --as-of 2018-06-01` even with zero snapshot -files, as long as the imported back-history covers that date. - -When the resolution lands on an imported value, the projection -bands are computed using **today's allocations scaled to the -imported liquid total** — we can't reconstruct the historical lot -composition from just a `liquid::` row, so today's stock/bond -split is substituted as the best-available approximation. The -header line reads `Projections (as of YYYY-MM-DD, imported value)` -and a muted note flags the scaling. - -**How to read the chart:** - -- **Actuals line stays inside the bands** → the model was - directionally honest. -- **Actuals line punches through the top band** → the model was - conservative (good problem to have). -- **Actuals line punches through the bottom band** → the model was - optimistic (bands need wider envelope, or a bear scenario was - underweighted). -- **A thin vertical "today" line** marks where the actuals end and - the projected future begins. - -**Critical caveat (must be loud, by design):** - -> This overlay shows whether the model was **directionally honest**, -> not whether the SWR claim was **accurate**. The SWR claim is a -> 30-year claim. We have at most ~12 years of weekly history -> (post-import) and 1+ years of native snapshots. The overlay tells -> you "did my actual trajectory fall within the bands the model would -> have drawn." It does **not** tell you "did the safe withdrawal rate -> hold up over a full retirement horizon." We will not have data to -> answer that within either of our lifetimes. - -The TUI surfaces this caveat on a status line whenever the overlay is -active. - -### Forecast-vs-actual evaluation (`--convergence`, `--return-backtest`) - -Two evaluation views over the spreadsheet's historical forecasts in -`imported_values.srf`. They complement `--overlay-actuals`: - -- **Overlay** answers "did the actual trajectory fall inside the - model's bands?" -- **Convergence** answers "did the model converge on a retirement - date as data accumulated?" -- **Return back-test** answers "was the model's expected-return - assumption honest, in hindsight?" - -```bash -zfin projections --convergence -zfin projections --return-backtest [--real] -``` - -`--convergence` reads each historical row's `projected_retirement` -field — the date the spreadsheet predicted you could retire — and -emits a table of `(observation_date, projected_date, years-until)`. -A flat downward slope of ~1y/year means the model was honestly -counting down. A flat-line at constant N means each year passes with -no progress (the prediction always says "N years away"). A `reached` -sentinel marks rows where the model said "you're already -retirement-ready." - -`--return-backtest` reads each row's `expected_return` claim and -compares it to the realized 1y/3y/5y forward CAGR of the `liquid` -series. Useful for gut-checking whether the spreadsheet's -`min(1y,3y,5y,10y)`-weighted formula systematically over- or -under-predicted. Pair with `--real` to compare against -inflation-deflated realized returns (the expected column stays -nominal — it's a return rate the source spreadsheet captured as -nominal). - -The CLI emits sampled tables (every Nth observation/anchor) for -quick scanning. The TUI projections tab renders the same data as -high-fidelity Kitty-graphics line charts; press `c` (convergence) -or `r` (return back-test) on the projections tab to switch views. - -Both views are also subject to the "directional honesty, not SWR -validity" caveat: they evaluate the model's inputs and outputs over -time, not whether the underlying SWR claim will hold up over a -30-year retirement. - -### Life events - -Life events modify the simulation's annual cash flow. Positive -amounts are income (offset withdrawals); negative amounts are -expenses (added to withdrawals). Events are CPI-adjusted by -default; set `inflation_adjusted:bool:false` for nominal events. - -```srf -# Permanent income starting at age 70 -type::event,name::Social Security,start_age:num:70,amount:num:38400 - -# 4-year expense starting at age 50 -type::event,name::College Tuition,start_age:num:50,duration:num:4,amount:num:-55000 - -# Per-person events (defaults to person 1) -type::event,name::Pension,start_age:num:65,person:num:2,amount:num:24000 -``` - -### Configuration field reference - -| Field | Type | Description | -| -------------------------------------- | ---- | --------------------------------------------------------------------------------- | -| `target_stock_pct` | num | Asset-allocation target (0–100). Used for sim stock/bond blend. | -| `horizon` | num | Distribution-phase length in years. Repeat for multiple horizons. | -| `horizon_age` | num | Resolves to `target_age − oldest_current_age`. Repeat for multiple. | -| `retirement_target` (on `horizon[_age]`) | num | Override the default earliest-retirement promotion. Allowed: 90, 95, 99. | -| `retirement_age` | num | Years old the OLDEST configured person must be to retire. | -| `retirement_at` | date | Absolute calendar date for retirement. Wins over `retirement_age` if both set. | -| `annual_contribution` | num | Yearly contribution during accumulation, in today's dollars. | -| `contribution_inflation_adjusted` | bool | If true (default), contributions grow with CPI year-over-year. | -| `target_spending` | num | Target retirement spending in today's dollars. | -| `target_spending_inflation_adjusted` | bool | If true (default), target spending grows with CPI in the distribution phase. | - -See `examples/pre-retirement-{age,spending,spending-target,both}/` -and `examples/post-retirement/` for fully-configured walkthroughs -of each combination. - -## CLI commands - -``` -zfin [args] - -Commands: - perf Trailing returns (1yr/3yr/5yr/10yr, price + total) - quote Real-time quote with chart - history Last 30 days price history - divs Dividend history with TTM yield - splits Split history - options Options chains (all expirations) - earnings Earnings history and upcoming events - etf ETF profile (expense ratio, holdings, sectors) - portfolio [FILE] Portfolio summary (default: portfolio.srf) - analysis [FILE] Portfolio analysis breakdowns (default: portfolio.srf) - snapshot [opts] Write a daily portfolio snapshot to history/ - compare [] Compare portfolio state across two dates - enrich Generate metadata.srf from Wikidata + SEC EDGAR - lookup CUSIP to ticker lookup via OpenFIGI - cache stats Show cached symbols - cache clear Delete all cached data - interactive, i Launch interactive TUI - help Show usage - -Global options: - --no-color Disable colored output (also respects NO_COLOR env) - -Portfolio options: - --refresh Force re-fetch all prices (ignore cache) - -w, --watchlist Watchlist file -``` +Not all keys are required; without a given key, that data type is +simply unavailable. For per-provider notes, signup links, the full +caching/TTL model, and the complete environment-variable list, see +[Data providers and API keys](docs/reference/providers.md), +[Caching and data freshness](docs/explanation/caching.md), and +[Environment variables](docs/reference/config/environment.md). ## Architecture @@ -958,11 +160,11 @@ accounts.srf Account to tax type mapping for analysis ### Dependencies -| Dependency | Source | Purpose | -|----------------------------------------------------|----------------|---------------------------------------------------| -| [SRF](https://git.lerch.org/lobo/srf) | Git | Cache file format and portfolio/watchlist parsing | -| [libvaxis](https://github.com/rockorager/libvaxis) | Git (v0.6.0) | Terminal UI rendering | -| [z2d](https://github.com/vancluever/z2d) | Git (v0.11.0) | Pixel chart rendering (Kitty graphics protocol) | +| Dependency | Source | Purpose | +|----------------------------------------------------|---------------|---------------------------------------------------| +| [SRF](https://git.lerch.org/lobo/srf) | Git | Cache file format and portfolio/watchlist parsing | +| [libvaxis](https://github.com/rockorager/libvaxis) | Git (v0.6.0) | Terminal UI rendering | +| [z2d](https://github.com/vancluever/z2d) | Git (v0.11.0) | Pixel chart rendering (Kitty graphics protocol) | ## Building @@ -986,8 +188,8 @@ A small amount of third-party source is vendored directly into the tree (rather than added as a Zig package dependency) where the upstream is small, stable, and not packaged for `build.zig.zon`: -| File | Source | Purpose | -|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------| +| File | Source | Purpose | +|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| | `src/providers/xml.zig` | [Snektron/vulkan-zig](https://github.com/Snektron/vulkan-zig/blob/797ae8af88e84753af9640266de61a985b76b580/generator/xml.zig), via [aws-zig](https://github.com/elerch/aws-sdk-for-zig) | XML DOM parser used by the EDGAR provider for NPORT-P primary documents. | Each vendored file carries a `// VENDORED - see README.md` header diff --git a/TODO.md b/TODO.md index d60b97a..28b87ec 100644 --- a/TODO.md +++ b/TODO.md @@ -99,6 +99,32 @@ ranking; unlabeled items are "someday, if the mood strikes." faithfulness one notch. Pick whichever has the highest payoff vs. complexity when this gets revisited. +## FIRECalc parity audit (priority LOW) + +`analytics/projections.zig` re-implements the FIRECalc algorithm over +the Shiller dataset (`data/shiller.zig`, 1871-present). In practice the +outputs land close to FIRECalc.com, but there is no formal cross-check, +and the user docs only claim results "track FIRECalc closely" +(`docs/guides/plan-retirement.md`). Stand up a parity audit so that +claim is backed by evidence. + +Do it: + +- Pick a handful of representative inputs (portfolio value, allocation, + horizon, spending, with and without contributions) and run each + through both FIRECalc.com and `zfin projections`. +- Compare success rate, safe-withdrawal dollars, and terminal-value + percentiles; record the deltas and an acceptable tolerance. +- Where they diverge, pin down why. Usual suspects: withdrawal timing + (start- vs end-of-year), inflation / CPI handling, rebalancing + assumptions, fees, and how a partial final year is treated. + +There is already a single FIRECalc reference assertion in the tests +(~$305K at 99% / 45yr on $7.7M, around `projections.zig:1696`). Extend +that into a small documented parity suite rather than a lone magic +number, and note any known, accepted differences in +`docs/explanation/projections-model.md`. + ## `--export-chart` follow-ups — priority LOW V1 of `--export-chart ` shipped for `quote` and `projections` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4ed2763 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,103 @@ +# zfin documentation + +zfin is a financial-data CLI and terminal UI for tracking a real +portfolio, analyzing trailing returns and risk, and running +retirement projections -- all from the terminal. + +These docs are **task-first**. Almost every example here is runnable +against the fictional example portfolios that ship in the repo under +[`examples/`](../examples/), so you can follow along verbatim and see +the exact output shown. Set `ZFIN_HOME` to an example directory and +any command reads that portfolio: + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin portfolio +ZFIN_HOME=examples/post-retirement zfin projections +``` + +If you are reading these on Forgejo, every link below is a relative +path you can click. + +## Start here + +New to zfin? Read these in order: + +1. [Getting started](getting-started.md) -- install, set up API keys, + create your first portfolio, run your first commands, open the TUI. +2. [Core concepts](explanation/concepts.md) -- the handful of ideas + (the `.srf` files, `ZFIN_HOME`, live vs. snapshot data) that make + everything else click. + +## The user manual (workflows) + +Goal-oriented guides. Each one walks an end-to-end task against a real +example portfolio and links to the reference for exhaustive detail. + +| Guide | What you'll accomplish | +|-------------------------------------------------------------------|--------------------------------------------------------------------| +| [Build your portfolio](guides/set-up-your-portfolio.md) | Write a `portfolio.srf` from scratch: lots, cash, options, CDs | +| [Classify your holdings](guides/classify-holdings.md) | Create `metadata.srf` (by hand or with `enrich`) so analysis works | +| [Map your accounts](guides/set-up-accounts.md) | Tag accounts with tax type and institution in `accounts.srf` | +| [Read your portfolio](guides/read-your-portfolio.md) | Interpret `portfolio`, `analysis`, `exposure`, `review`, `perf` | +| [Track contributions](guides/track-contributions.md) | See money added over time and tag internal transfers | +| [Snapshots and history](guides/snapshots-and-history.md) | Record daily snapshots and compare your portfolio over time | +| [Plan for retirement](guides/plan-retirement.md) | Configure `projections.srf` for accumulation and drawdown | +| [Audit against your brokerage](guides/audit-against-brokerage.md) | Reconcile zfin against Fidelity/Schwab/Wells exports | +| [A periodic review](guides/periodic-review.md) | Reconcile, see what changed, then commit the new baseline | +| [Customize the TUI](guides/customize-the-tui.md) | Rebind keys and recolor the interface | +| [Offline use and refreshing data](guides/offline-and-refresh.md) | Control caching and provider calls | + +## Reference + +Look-it-up material. Exhaustive, terse, and kept in step with the +shipped binary. + +- [CLI command reference](reference/cli/index.md) -- every command, + its flags, and sample output. +- Configuration files: + - [`portfolio.srf`](reference/config/portfolio-srf.md) -- positions, lots, cash, options, CDs + - [`metadata.srf`](reference/config/metadata-srf.md) -- sector / geography / asset-class classification + - [`accounts.srf`](reference/config/accounts-srf.md) -- tax type and institution per account + - [`projections.srf`](reference/config/projections-srf.md) -- retirement projection inputs + - [`watchlist.srf`](reference/config/watchlist-srf.md) -- price-only symbols + - [`transaction_log.srf`](reference/config/transaction-log-srf.md) -- declared transfers + - [`keys.srf`](reference/config/keys-srf.md) -- TUI keybindings + - [`theme.srf`](reference/config/theme-srf.md) -- TUI colors + - [Environment variables](reference/config/environment.md) -- API keys, `ZFIN_HOME`, `ZFIN_CACHE_DIR`, and more +- [The interactive TUI](reference/tui.md) -- tabs, keybindings, theming. +- [Data providers and API keys](reference/providers.md) -- who supplies + what, free-tier limits, and where to get keys. + +## Explanation + +Background and the "why" behind zfin's behavior. + +- [Core concepts](explanation/concepts.md) +- [Caching and data freshness](explanation/caching.md) +- [Why multiple data providers](explanation/data-providers.md) +- [Returns and performance](explanation/returns-and-performance.md) +- [The retirement projection model](explanation/projections-model.md) +- [FAQ and troubleshooting](explanation/faq-troubleshooting.md) + +## The example portfolios + +The guides lean on five bundled, fictional households. They are safe +to read, copy, and run against: + +| `ZFIN_HOME=examples/...` | Household | Demonstrates | +|----------------------------------|------------------------------------------|---------------------------------------------------------------| +| `pre-retirement-both` | Pat & Sam, ~45, ~$1.3M, contributing | A target retirement date **and** a target spending level | +| `pre-retirement-age` | same household | A target retirement **date** only | +| `pre-retirement-spending` | same household | A target **spending** level only (earliest-retirement search) | +| `pre-retirement-spending-target` | same household | An explicit, deliberately infeasible planning anchor | +| `post-retirement` | Robin & Jamie, ~68, ~$2.5M, drawing down | Distribution-only planning, with snapshot history | + +See [`examples/README.md`](../examples/README.md) for the full tour of +what differs between them. + +## A note on the examples and your real data + +All names, share counts, account numbers, and prices in `examples/` +are fictional. Nothing in these docs is financial advice. When you +point zfin at your own data, keep it outside the repository (set +`ZFIN_HOME` to a private directory) so you never commit real holdings. diff --git a/docs/explanation/caching.md b/docs/explanation/caching.md new file mode 100644 index 0000000..1a48af5 --- /dev/null +++ b/docs/explanation/caching.md @@ -0,0 +1,121 @@ +# Caching and data freshness + +zfin makes a lot of API calls on your behalf -- prices, dividends, +earnings, ETF holdings -- against providers with strict free-tier +limits. Aggressive caching is what keeps it fast and keeps you well +under those limits. This page explains how it works so the +[`--refresh-data`](../guides/offline-and-refresh.md) flag makes sense. + +## The fetch path + +Every data request walks the same tiers, stopping at the first one that +can satisfy it: + +1. **Local cache.** Look for `~/.cache/zfin//.srf`. If the + file exists and is within its TTL, deserialize and return -- no + network at all. +2. **Shared server** *(optional)*. On a miss or stale entry, if + `ZFIN_SERVER` is set, zfin asks that server before any provider; a + hit is written into your local cache and served from there, so no + provider call happens. See [Server sync](#server-sync-zfin_server). +3. **Provider.** Otherwise zfin fetches from the upstream provider, + writes the result to the cache, and returns it. + +Freshness is decided by the cache file's modification time versus the +TTL for that data type. The cache directory defaults to `~/.cache/zfin` +and is set with `ZFIN_CACHE_DIR`. + +The `--refresh-data` policy decides which tiers run: + +- `auto` (default) walks all three. +- `force` skips the local cache and the server, going straight to the + provider, then re-caches the result. +- `never` stops at the local cache: it returns cached data even if + stale, and never touches the server or a provider. + +## Time-to-live by data type + +Different data ages at different rates, so each type has its own TTL: + +| Data type | TTL | Why | +|---------------|---------------|-------------------------------------------------------------| +| Daily candles | ~24h (23h45m) | One bar per trading day; slightly under 24h for cron jitter | +| Dividends | 14 days | Declared well in advance | +| Splits | 14 days | Rare corporate events | +| Options | 1 hour | Prices move continuously when markets are open | +| Earnings | 30 days\* | Quarterly; smart-refreshed around announcements | +| ETF profiles | ~30 days | Holdings and weights change slowly | +| Quotes | never cached | Meant to be a live price check | + +\* **Earnings smart refresh:** even inside the 30-day window, cached +earnings re-fetch automatically once an earnings date has passed but +the cache still lacks the actual result -- so numbers appear promptly +after an announcement without daily polling. + +## Quotes are never cached + +Because quotes exist to give you a live price, they're never served +from cache. The practical consequence: in offline mode +(`--refresh-data=never`) the [`quote`](../reference/cli/quote.md) +command has nothing to serve, while candle-based commands like +[`perf`](../reference/cli/perf.md) work fine from cached history. + +## Incremental candle updates + +Price history isn't re-downloaded wholesale. On a cache miss, zfin +fetches only candles newer than the last cached date and appends them, +using a small `candles_meta.srf` companion file to track the last date +and source provider. A ten-year history costs one big fetch the first +time and tiny top-ups thereafter. + +## Negative caching + +When a provider permanently fails for a symbol -- a nonexistent +ticker, say -- zfin records a negative cache entry so it doesn't retry +the same dead lookup on every run. (Transient failures like rate limits +are not cached this way; they're retried.) + +## Rate limiting + +Each provider has a client-side token-bucket limiter sized to its +free-tier ceiling (e.g. Polygon 5/min, FMP 250/day). When you'd exceed +the rate, zfin blocks until a token is available rather than firing a +request that would 429. This is why a `--refresh-data=force` run across +many symbols can pace itself instead of failing. Limits are listed in +[Data providers and API keys](../reference/providers.md). + +## Server sync (`ZFIN_SERVER`) + +`ZFIN_SERVER` points zfin at an optional +[zfin-server](https://git.lerch.org/lobo/zfin-server) instance -- a +shared cache that sits between your local cache and the upstream +providers, and is the second tier of [the fetch path](#the-fetch-path). +On a local miss, zfin requests `GET {ZFIN_SERVER}//` +(candles, dividends, splits, options, earnings, classification, ETF +metrics, and EDGAR entity facts), and a hit is written straight into +your local cache. + +Why bother: the server is warmed once -- say by a cron job on one +machine -- and then every client draws from it instead of each spending +its own provider quota, so a household or a fleet of machines shares one +set of API-key budgets and gets faster cold starts. For the portfolio +price load, the server is queried in parallel across symbols, with +per-symbol provider fallback only for what it can't supply. + +It is entirely optional: when `ZFIN_SERVER` is unset, every server-sync +path silently no-ops and zfin runs local-cache-then-provider. Live +quotes are never served by the server (they aren't cached anywhere), and +`--refresh-data=force` bypasses the server to re-fetch from the provider. + +## Controlling it + +You rarely need to intervene -- `auto` does the right thing. When you +do: + +- `--refresh-data=force` re-fetches everything (after a close, or to + clear suspected bad data). +- `--refresh-data=never` goes fully offline. +- [`zfin cache stats`](../reference/cli/cache.md) shows what's cached; + `zfin cache clear` wipes it (everything re-fetches next run). + +See [Offline use and refreshing data](../guides/offline-and-refresh.md). diff --git a/docs/explanation/concepts.md b/docs/explanation/concepts.md new file mode 100644 index 0000000..6154301 --- /dev/null +++ b/docs/explanation/concepts.md @@ -0,0 +1,126 @@ +# Core concepts + +A handful of ideas explain how zfin is put together. Once they click, +the rest of the tool is predictable. + +## zfin is a reader, not a database + +zfin doesn't store your portfolio. You own a few plain-text files; zfin +reads them, fetches market data, and computes. There's no hidden state, +no import step, no lock-in -- your data is text you can read, diff, and +version-control. + +This is why almost everything is grounded in files you edit directly, +and why the docs can ship runnable [examples](../../examples/). + +## The `.srf` files + +You don't need any of these to look up a quote, trailing returns, or +earnings for a symbol -- those commands need only an API key. The files +matter only when you want zfin to track *your* portfolio, and you add +them one at a time as you go. In practice you start with **one** file, +`portfolio.srf`; everything else is optional, and zfin degrades +gracefully when a file is absent (the last column says what you give +up). + +They all share one format: [SRF](https://git.lerch.org/lobo/srf) +(Simple Record Format) -- line-oriented, comma-separated `key::value` +pairs (`key:num:` and `key:bool:` for typed values), `#` comments, and +a `#!srfv1` header. + +| File | Holds | When you need it | +|---------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| [`portfolio.srf`](../reference/config/portfolio-srf.md) | Your lots (positions, cash, options, CDs) | The one file to start with: required for any portfolio, analysis, or projection view. | +| [`accounts.srf`](../reference/config/accounts-srf.md) | Tax type / institution / account number / update cadence per account | Optional. Without it, accounts show as "Unknown" in the tax-type breakdown; everything else works. | +| [`metadata.srf`](../reference/config/metadata-srf.md) | Sector / geo / asset-class per symbol | Optional. Without it, the asset-class / sector / geography breakdowns have nothing to group by; valuation still works. | +| [`projections.srf`](../reference/config/projections-srf.md) | Retirement projection inputs | Only for `zfin projections`, which otherwise runs with sensible defaults. | +| [`watchlist.srf`](../reference/config/watchlist-srf.md) | Price-only symbols | Only if you want a watchlist. | +| [`transaction_log.srf`](../reference/config/transaction-log-srf.md) | Declared transfers | Only to refine contribution attribution; missing means it's simply not applied. | + +A few more files show up only when you opt into a feature: +`history/*-portfolio.srf` snapshots are written for you by +[`zfin snapshot`](../reference/cli/snapshot.md), and +`~/.config/zfin/keys.srf` / `theme.srf` customize the TUI. The same SRF +format is used internally for the cache and snapshots, so those are +inspectable too. + +## `ZFIN_HOME`: where your data lives + +zfin resolves your files one of two ways -- never a mix of both: + +1. **`ZFIN_HOME` is set** -- zfin reads from that directory, and only + there. The current directory is never consulted, so a stray file in + cwd can't silently shadow your real data. +2. **`ZFIN_HOME` is unset** -- zfin reads from the current directory. + +To make a single run read the current directory while `ZFIN_HOME` is +set, unset it just for that command: `env -u ZFIN_HOME zfin portfolio`. + +`accounts.srf` and `metadata.srf` are always loaded from the same +directory as the resolved `portfolio.srf`. Pointing `ZFIN_HOME` at an +example directory is what makes the guides runnable: + +```bash +ZFIN_HOME=examples/post-retirement zfin portfolio +``` + +Keep your real data in a private `ZFIN_HOME` outside any repo so you +never commit holdings. See +[environment variables](../reference/config/environment.md). + +## Live data vs. snapshots + +zfin deals with two kinds of "your portfolio," and the distinction +matters: + +- **Live (current) portfolio** -- computed from `portfolio.srf` plus + the latest prices. This is what `portfolio`, `analysis`, `review`, + and friends show. It always reflects what you hold *now*. +- **Snapshots** -- immutable records of your totals on a past date, + written by [`zfin snapshot`](../reference/cli/snapshot.md) into + `history/-portfolio.srf`. These are the time series that + `history` and `compare` read. + +The live portfolio is mutable (you edit it); snapshots are historical +facts (you don't). See +[Snapshots and history](../guides/snapshots-and-history.md). + +## The data service and caching + +All market data flows through one path: check the local cache, and if +the entry is fresh enough, use it; otherwise fetch from a provider, +write it to the cache, and return it. You rarely think about providers +directly -- you think about *freshness*, controlled by the +`--refresh-data` flag. See +[Caching and data freshness](caching.md) and +[Why multiple data providers](data-providers.md). + +## CLI and TUI are two faces of one engine + +The CLI and the interactive TUI (`zfin i`) sit on the same engine: +they read the same files, share the same cache, and run the same +analytics, so a figure you see in one matches the other. Reach for the +CLI for quick checks and scripting; reach for the TUI for browsing and +high-fidelity charts. + +They overlap heavily but aren't a one-to-one mirror -- and aren't +trying to be: + +- **CLI-only.** Several commands have no TUI tab: the data-hygiene and + journaling ones (`audit`, `snapshot`, `import`, `enrich`, `compare`, + `contributions`), plus a few like `exposure` and `milestones`. +- **TUI-only.** Interactive touches have no CLI counterpart: live + charts, in-place refresh, the `?` keybinding overlay, and + conveniences like the overlay that shows a holding's full name behind + its ticker. + +The nine TUI tabs -- Portfolio, Analysis, Review, Projections, History, +Quote, Performance, Earnings, Options -- cover the browsing and +analysis surfaces; the rest lives on the CLI. See +[The interactive TUI](../reference/tui.md). + +## Where to go next + +- [Getting started](../getting-started.md) if you haven't installed yet. +- [Build your portfolio](../guides/set-up-your-portfolio.md) to create your first file. +- [Caching and data freshness](caching.md) for the fetch model. diff --git a/docs/explanation/data-providers.md b/docs/explanation/data-providers.md new file mode 100644 index 0000000..0e0670c --- /dev/null +++ b/docs/explanation/data-providers.md @@ -0,0 +1,72 @@ +# Why multiple data providers + +No single free data source does everything well. zfin aggregates +several, using each for what it's best at, and falls back gracefully +when one is unavailable. This page explains the design; for signup +links, limits, and the full table, see +[Data providers and API keys](../reference/providers.md). + +## One source per job + +Each data type has a primary provider chosen for coverage and quality: + +| Data | Primary | Notes | +|-------------------------------|----------------------|-----------------------------------------------| +| Daily candles | Tiingo | Deep history; stocks, ETFs, mutual funds | +| Real-time quotes | Yahoo | No key required | +| Dividends / splits | Polygon | Carries forward-looking declared events | +| Options chains | CBOE | No key; 15-minute delayed | +| Earnings | FMP | Actuals + analyst estimates | +| ETF profiles / classification | SEC EDGAR + Wikidata | Authoritative holdings; needs a contact email | + +The [data service](concepts.md#the-data-service-and-caching) hides this +behind one interface -- commands ask for "candles for VTI," not "call +Tiingo." That's also what makes the cache and rate limiting uniform +across providers. + +## Fallback, not single-point-of-failure + +Where a second source can stand in, zfin uses it: + +- **Candles:** Tiingo is primary; **Yahoo** is the fallback if Tiingo + is unavailable or lacks the symbol. +- **Quotes:** Yahoo is primary; **TwelveData** is the fallback. +- **Dividends/splits:** Polygon is primary, but Tiingo's price-series + response carries per-row dividend and split data that zfin merges in + to rescue events Polygon's reference endpoints occasionally miss. + +How a failure propagates depends on its kind. **Transient** errors +(server 5xx, connection drops) stop a refresh so you don't get a +half-updated view. **Permanent** errors (not-found, parse failures) +fall through to the next provider, and a rate-limit hit triggers a +single backoff-and-retry. + +## Why some data needs a key and some doesn't + +Quotes (Yahoo) and options (CBOE) are unauthenticated. The rest want a +free API key, and SEC EDGAR wants a contact email in its `User-Agent` +(set via `ZFIN_USER_EMAIL`) rather than a key. None are individually +required -- missing a key just removes that one data type, and +everything else keeps working. See +[which key unlocks what](../reference/config/environment.md#api-keys). + +## A few deliberate quirks + +- **Yahoo is unofficial.** Yahoo Finance has no public API; zfin reads + an undocumented endpoint that needs no key but carries no guarantees + -- it can change shape or stop responding at any time. Quotes (and + the candle fallback) ride on it, so keeping `TIINGO_API_KEY` set for + candles and `TWELVEDATA_API_KEY` for quote fallback cushions a Yahoo + outage. +- **TwelveData is no longer used for candles.** Its split-adjusted + closes proved unreliable for the return math, so it's quote-fallback + only now. Yahoo is the candle fallback. +- **Mutual-fund and dual-class coverage varies.** Some symbols (ETFs, + CUSIPs, a few dual-class shares like BRK.B) have no earnings on the + free FMP tier and show "no earnings data" -- a documented limitation, + not a bug. See the [FAQ](faq-troubleshooting.md). + +## See also + +- [Data providers and API keys](../reference/providers.md) -- the reference table. +- [Caching and data freshness](caching.md) -- how fetched data is reused. diff --git a/docs/explanation/faq-troubleshooting.md b/docs/explanation/faq-troubleshooting.md new file mode 100644 index 0000000..a4133f7 --- /dev/null +++ b/docs/explanation/faq-troubleshooting.md @@ -0,0 +1,105 @@ +# FAQ and troubleshooting + +Common questions and the quick fixes. If something here doesn't cover +your case, the relevant reference page usually does. + +## Setup and keys + +**"Error: No API key set."** +zfin needs at least `TIINGO_API_KEY` to fetch price history. Set it in +your environment or `.env`. See [Getting started](../getting-started.md) +and [environment variables](../reference/config/environment.md#api-keys). + +**Total-return columns are blank or match price-only.** +Total return needs dividend data from Polygon -- set `POLYGON_API_KEY`. +Without it you get price-only returns. See +[Returns and performance](returns-and-performance.md#price-only-vs-total-return). + +**ETF profiles or `enrich` don't work.** +SEC EDGAR requires a contact email. Set `ZFIN_USER_EMAIL` to your +address (it's not a key). See +[`zfin enrich`](../reference/cli/enrich.md). + +## Missing or odd data + +**A holding shows up as "Unclassified."** +It has no [`metadata.srf`](../reference/config/metadata-srf.md) entry, +or the entry's `symbol::` doesn't match the lot's `symbol::`/`ticker::`. +Add a classification line; see +[Classify your holdings](../guides/classify-holdings.md). + +**An account shows as "Unknown" in the tax-type breakdown.** +It's missing from [`accounts.srf`](../reference/config/accounts-srf.md), +or the `account::` name doesn't match the lot's `account::` exactly +(names must match character-for-character). See +[Map your accounts](../guides/set-up-accounts.md). + +**A stock or ETF shows "no earnings data."** +ETFs, mutual funds, CUSIPs, and some dual-class shares (e.g. BRK.B) +return no earnings on FMP's free tier. This is an expected limitation, +not a bug. See [Why multiple data providers](data-providers.md). + +**A mutual fund price looks like yesterday's.** +Mutual-fund NAVs publish after market close (after midnight ET for some +funds), so intraday you'll see the prior NAV until the new one posts. + +**`quote` says the symbol is unavailable, but `perf` works.** +You're probably in offline mode (`--refresh-data=never`). Quotes are +never cached, so offline there's nothing to serve; candle-based +commands still work from cache. See +[Caching and data freshness](caching.md#quotes-are-never-cached). + +**A row is shown in warning (yellow) color.** +That's a manual-priced lot -- its price came from the lot's `price::` +field, not a live feed, so it may be stale. Update `price::` / +`price_date::`, or run [`zfin audit`](../reference/cli/audit.md) to find +stale ones. + +## Behavior + +**A run printed "(using cached data)."** +zfin served fresh-enough cached data instead of hitting the network -- +normal and fast. Force a refetch with `--refresh-data=force`. See +[Offline use and refreshing data](../guides/offline-and-refresh.md). + +**A refresh seems slow / pauses.** +The rate limiter is spacing requests to stay under a provider's +free-tier limit (e.g. Polygon 5/min). It blocks rather than failing. +See [rate limiting](caching.md#rate-limiting). + +**zfin can't find my portfolio.** +It looks in `ZFIN_HOME` if set, otherwise the current directory. Set +`ZFIN_HOME` to your data directory, or `cd` into it. `accounts.srf` and +`metadata.srf` load from the same directory as the resolved +`portfolio.srf`. See +[core concepts](concepts.md#zfin_home-where-your-data-lives). + +**CLI and TUI show different totals.** +They shouldn't -- both union-merge every `portfolio*.srf` in +`ZFIN_HOME`. If they differ, check whether you passed a narrowing `-p` +pattern to one but not the other, or whether one is using a stale +cache vs. live prices. + +**`contributions` reports "No changes detected."** +It diffs git revisions of your portfolio. With a clean tree it compares +`HEAD~1..HEAD`; if your last commit didn't change holdings, there's +nothing to show. Use `--since` to widen the window. See +[Track contributions](../guides/track-contributions.md). + +## Data hygiene + +**Lots of "overdue for update" accounts in `audit`.** +The default cadence is weekly and nags until satisfied. Set +`update_cadence::monthly|quarterly|none` per account in +[`accounts.srf`](../reference/config/accounts-srf.md#update_cadence-and-the-audit-nag). + +**A transfer between my accounts inflated my contributions.** +Declare it in +[`transaction_log.srf`](../reference/config/transaction-log-srf.md) so +it isn't counted as new money. See +[Track contributions](../guides/track-contributions.md#dont-double-count-transfers). + +## See also + +- [Core concepts](concepts.md) -- the mental model. +- [Documentation home](../README.md) -- everything else. diff --git a/docs/explanation/projections-model.md b/docs/explanation/projections-model.md new file mode 100644 index 0000000..acb28f0 --- /dev/null +++ b/docs/explanation/projections-model.md @@ -0,0 +1,141 @@ +# The retirement projection model + +[`zfin projections`](../reference/cli/projections.md) simulates your +retirement portfolio against real market history. This page explains +the model so you can trust -- and correctly distrust -- its output. For +how to configure it, see the +[`projections.srf` reference](../reference/config/projections-srf.md) +and [Plan for retirement](../guides/plan-retirement.md). + +## Historical simulation, not a formula + +Rather than assume a single average return, zfin replays your portfolio +through actual historical sequences drawn from the **Shiller dataset** +(US equity total returns and CPI back to 1871). Each simulated run uses +a real historical path of returns and inflation, so the spread of +outcomes reflects real sequences -- including bad-timing sequences like +retiring into 1929, 1973, or 2000. This is the same family of method +as FIRECalc. + +## Two phases + +Every projection runs the same two phases in order: + +1. **Accumulation** -- contributions added each year, no spending. Its + length comes from your retirement-date input. With no input, it's + zero years (an already-retired view). +2. **Distribution** -- annual spending withdrawn (CPI-adjusted by + default), no contributions. Its length is the configured `horizon`. + +[Life events](../reference/config/projections-srf.md#event-fields) +(Social Security, pensions, tuition, healthcare) adjust the cash flow +in both phases. + +## How inflation is handled + +Inflation isn't a fixed assumption. Each historical cycle uses that +start year's **actual CPI sequence** alongside its actual returns (the +Shiller dataset carries both), so a cycle beginning in 1966 replays +1966's stagflation while one beginning in 2009 replays low-inflation +years. + +The simulation runs in **nominal dollars**, which means the output +mixes two units -- and knowing which is which is the difference between +a sensible plan and a badly misread one: + +- **Flows are entered in today's dollars and inflated forward.** Your + `annual_contribution`, `target_spending`, and inflation-adjusted life + events are amounts in *today's* dollars; each simulated year the model + multiplies them by that cycle's cumulative CPI, holding their + purchasing power constant. Set `contribution_inflation_adjusted`, + `target_spending_inflation_adjusted`, or an event's + `inflation_adjusted` to `false` to pin a flow at a flat nominal amount + instead (e.g. a fixed pension with no COLA). +- **Safe-withdrawal figures are in today's dollars.** "You could spend + ~$264k/yr at 99%" means ~$264k of *today's* purchasing power, with the + actual dollar amount rising each retirement year to keep pace with + inflation. +- **Portfolio and terminal values are nominal (future dollars).** The + "Median portfolio at retirement" and the + `Terminal Portfolio Value (nominal, ...)` percentiles are **not** + inflation-adjusted. A ~$244M median balance 50 years out is heavily + inflated dollars, not $244M of today's purchasing power -- judge it + against the inflated spending it has to support, never against today's + prices. + +This split is deliberate and matches FIRECalc: you plan spending in real +(today's) terms while the balance compounds in nominal terms. + +## Percentile bands + +Across all the historical runs, zfin reports the distribution of +outcomes rather than a single number: + +- **p10 (pessimistic)** -- only 10% of histories did worse. +- **p50 (median)** -- the middle outcome. +- **p90 (optimistic)** -- only 10% did better. + +``` +Terminal Portfolio Value (nominal, at 99% withdrawal rate) + 25 Year 35 Year +Pessimistic (p10) $6,739,560.02 $11,597,557.94 +Median (p50) $30,023,255.68 $66,794,741.87 +Optimistic (p90) $103,184,321.05 $279,372,182.75 +``` + +The wide spread is the point: it shows sequence-of-returns risk +honestly instead of hiding it behind an average. + +## Confidence and safe withdrawal + +The **Safe Withdrawal** table answers "how much could I spend and still +not run out?" at chosen confidence levels (90/95/99%). A 99% safe +withdrawal is the spending level that survived 99% of historical +sequences over that horizon -- the most conservative. Higher confidence +and longer horizons both lower the safe number. + +## The earliest-retirement search + +When you set a `target_spending` instead of a date, zfin inverts the +question: for each (horizon x confidence) cell it searches for the +**earliest** accumulation length (up to 50 years) that sustains your +spending, and renders the grid of answers. One cell is promoted to the +headline (see +[promotion rules](../reference/config/projections-srf.md#the-two-retirement-planning-inputs)). +If no length within the cap works, the cell is **infeasible** -- shown +honestly rather than fudged. + +## The caveat that matters most + +zfin states this loudly by design, and so does this page: + +> The actuals overlay and evaluation views +> (`--overlay-actuals`, `--convergence`, `--return-backtest`) tell you +> whether the model was **directionally honest** -- did your real +> trajectory fall within the bands it would have drawn. They do **not** +> tell you whether a safe-withdrawal claim is **accurate**. An SWR +> claim is a 30-year claim; there is at most ~12 years of weekly +> history and a year or two of native snapshots to check it against. +> No one will have data to validate a full-retirement SWR within our +> lifetimes. + +Treat the projection as a disciplined way to compare scenarios and +visualize sequence risk -- not as a promise about your specific future. + +## Assumptions to keep in mind + +- **Allocation** is a single stock/bond blend (`target_stock_pct`), not + your exact holdings. +- **Inflation** comes from each historical cycle's own CPI; flows are + real (today's-dollar) and balances are nominal. See + [How inflation is handled](#how-inflation-is-handled). +- **Taxes** are not modeled. Withdrawal figures are pre-tax. +- **Imported-value overlays** scale today's allocation to a historical + total when lot-level history isn't available, because a `liquid::` + row can't reconstruct past composition. + +## See also + +- [Plan for retirement](../guides/plan-retirement.md) -- the guided walkthrough. +- [`projections.srf` reference](../reference/config/projections-srf.md) -- every input. +- [`zfin projections`](../reference/cli/projections.md) -- flags and evaluation views. diff --git a/docs/explanation/returns-and-performance.md b/docs/explanation/returns-and-performance.md new file mode 100644 index 0000000..76f3a3d --- /dev/null +++ b/docs/explanation/returns-and-performance.md @@ -0,0 +1,127 @@ +# Returns and performance + +zfin reports returns in a few different forms. This page explains what +each means so you can read [`perf`](../reference/cli/perf.md), +[`review`](../reference/cli/review.md), and the portfolio summary +correctly. + +## Price-only vs. total return + +Two columns show up throughout zfin: + +- **Price only** -- the change in share price alone. +- **Total return** -- price change **plus** reinvested dividends. This + is the number that reflects what you actually earned. + +``` + Price Only Total Return + 1-Year Return: 27.29% 28.79% + 3-Year Return: 20.75% 22.22% ann. +``` + +Total return needs dividend history, which comes from Polygon -- so it +requires `POLYGON_API_KEY`. Without it, you still get price-only +returns. For a dividend payer like SCHD the gap between the two columns +is large; for a non-payer it's near zero. + +## Annualized (CAGR) + +Multi-year returns are **annualized** -- expressed as a compound annual +growth rate (the constant yearly rate that produces the same total +growth), marked `ann.` in the output. A "3-Year Return: 22.22% ann." +means the holding grew as if by 22.22% every year for three years, not +22.22% total. One-year figures are already annual, so they carry no +`ann.` tag. + +## As-of vs. month-end + +[`perf`](../reference/cli/perf.md) prints two tables: + +- **As-of** -- returns through the latest available close. The most + current view. +- **Month-end** -- returns through the most recent calendar month-end. + This matches how mutual funds and Morningstar quote their trailing + numbers, so it's the apples-to-apples figure for comparison. + +``` +As-of 2026-06-04: ... +Month-end (2026-05-31): ... +``` + +## Risk metrics + +[`perf`](../reference/cli/perf.md) and +[`review`](../reference/cli/review.md) also report risk, computed from +periodic returns: + +- **Volatility** -- how much returns vary; higher means a bumpier ride. +- **Sharpe ratio** -- return earned per unit of volatility (risk- + adjusted return). Higher is better; a negative Sharpe means the + asset underperformed cash on a risk-adjusted basis over the window. +- **Max drawdown** -- the largest peak-to-trough decline over the + period; a worst-case-loss gut check. + +``` + 3Y-Vol 10Y-Vol 3Y-SR 10Y-SR 5Y-MaxDD + 13.4% 15.8% 1.29 0.90 24.8% +``` + +### Reading the Sharpe ratio + +Sharpe divides an asset's *excess* return -- its annualized return +**minus the risk-free rate** -- by its volatility. It answers "how much +extra return did I earn for the bumpiness I took on, versus just parking +the money risk-free?" + +zfin uses the average 3-month US T-bill rate (FRED series DTB3) over the +same window as the return. That rate table is **hand-maintained**: it's +refreshed about once a year, and when it falls behind zfin prints a +`T-bill risk-free rate table is overdue for refresh` warning on stderr +(also shown by [`zfin doctor`](../reference/cli/doctor.md)). It blocks +nothing -- it's a nudge. Because the same rate feeds every holding, a +slightly stale rate barely shifts comparisons between them; refresh it +when the nag appears. + +**Rough guide to what's "good":** + +| Sharpe | Read | +|-----------|-----------------------------------------------------------------------| +| `> 1.0` | Standout -- strong reward for the risk taken | +| `0.5-1.0` | Healthy -- where most solid long-term holdings sit | +| `0-0.5` | Mediocre -- barely beating cash once risk is accounted for | +| `< 0` | Lost to cash on a risk-adjusted basis (the negative case noted above) | + +These are the bands zfin uses to color the [`review`](../reference/cli/review.md) +dashboard: green above 0.5, yellow from 0 to 0.5, red below 0. + +**Prefer the longer window.** A Sharpe measured over a single year is +noisy -- one strong or weak quarter swings it. The 10-year figure +averages across market regimes and is the steadier read, which is why +zfin reports Sharpe at **3Y and 10Y** rather than 1Y. When the two +disagree, lean on the longer window to judge a holding's risk-adjusted +quality. + +## Portfolio-level returns + +The portfolio summary's **Historical** line and the +[`projections`](../reference/cli/projections.md) benchmark block show +your holdings' *blended* return -- each position's return weighted by +its market value. It's a quick "how have my holdings done together" +read, and it includes the effect of your allocation (a bond-heavy +portfolio will trail an all-equity one, as you'd expect). + +> These returns describe the securities, not your personal money- +> weighted return. They don't account for the timing of your +> contributions. To separate new money from market movement over a +> period, use [contributions](../guides/track-contributions.md). + +The market value that weights these returns reflects zfin's +covered-call cap: an in-the-money written call holds its covered shares +at the strike rather than the live price, so it doesn't price the +option contract itself. See +[covered-call valuation](../guides/read-your-portfolio.md#covered-calls). + +## See also + +- [`zfin perf`](../reference/cli/perf.md) / [`zfin review`](../reference/cli/review.md) +- [Read your portfolio](../guides/read-your-portfolio.md) -- interpreting the dashboards. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..6ba63e0 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,243 @@ +# Getting started + +This guide takes you from nothing to a working zfin in about ten +minutes: install the binary, set one API key, run your first commands +on any ticker, then create your own portfolio. + +By the end you will have: + +- the `zfin` binary on your `PATH`, +- a free Tiingo API key configured, +- pulled live quotes and trailing returns for real tickers, and +- created a minimal portfolio of your own. + +## 1. Install + +### Option A: pre-built binary (macOS, Apple Silicon) + +```bash +curl -L -o zfin \ + https://git.lerch.org/api/packages/lobo/generic/zfin-aarch64-macos/latest/zfin-aarch64-macos +chmod +x zfin +sudo mv zfin /usr/local/bin/ # or anywhere on your PATH +``` + +### Option B: build from source (Linux, macOS) + +Building requires **Zig 0.16.0**. If you use [mise](https://mise.jdx.dev/), +the pinned toolchain installs itself: + +```bash +git clone https://git.lerch.org/lobo/zfin.git +cd zfin +mise install # installs Zig 0.16.0 from .mise.toml; skip if you have it +zig build # binary lands at zig-out/bin/zfin +``` + +Put `zig-out/bin/zfin` on your `PATH`, or run it by full path. + +### Verify + +```bash +zfin version +``` + +``` +zfin e246d1e (built 2026-06-19) +``` + +## 2. Get an API key + +zfin pulls data from several free-tier providers, but you only need +**one** key to get started: + +- **Tiingo** (`TIINGO_API_KEY`) -- daily price history (candles). This + is the primary price source and the one key worth setting first. + Sign up free at . + +A couple more are worth adding once you are up and running. None are +required for your first run: + +| Key | Unlocks | Free signup | +|-------------------|-----------------------------------------------------------------------------|-------------------------------------| +| `POLYGON_API_KEY` | Dividends and splits (enables **total** return) | | +| `FMP_API_KEY` | Earnings history and estimates | | +| `ZFIN_USER_EMAIL` | ETF profiles and `enrich` (SEC EDGAR requires a contact email -- not a key) | your own email | + +Quotes (Yahoo) and options chains (CBOE) need no key at all. For the +full breakdown of who supplies what, see +[Data providers and API keys](reference/providers.md). + +## 3. Configure + +zfin reads keys from the environment or from a `.env` file. The `.env` +file is searched first in the binary's parent directory, then in the +current directory. + +Create a `.env` (or export the variables in your shell): + +```bash +TIINGO_API_KEY=your_key_here +ZFIN_USER_EMAIL=you@example.com # optional, enables ETF profiles +``` + +Cached data lands in `~/.cache/zfin` by default; override it with +`ZFIN_CACHE_DIR`. See [Environment variables](reference/config/environment.md) +for every setting zfin understands. + +Not sure a key took, or which features you've unlocked? Run +[`zfin doctor`](reference/cli/doctor.md) -- it reports which files and +keys it found and what each one enables, and changes nothing. + +## 4. Take it for a spin + +You don't need a portfolio to use zfin -- point it at any ticker and it +fetches what it needs. Start with a live quote (quotes need no API key): + +```bash +zfin quote SPY +``` + +``` +SPY $746.74 (close) +======================================== + Date: 2026-06-18 + Open: $747.76 + High: $748.23 + Low: $743.86 + Volume: 80,875,657 + Change: +$5.78 (+0.78%) +``` + +(A short price chart prints below the quote. Your figures will differ +-- prices move every day.) + +The per-symbol commands all work on any ticker, no portfolio required. +The first price-history fetch for a symbol populates the cache (a few +seconds); later runs are instant: + +```bash +zfin perf VTI # 1y/3y/5y/10y trailing returns (needs TIINGO_API_KEY) +zfin history VTI # last 30 days of prices +zfin divs SCHD # dividend history (needs POLYGON_API_KEY) +zfin earnings MSFT # earnings + estimates (needs FMP_API_KEY) +zfin etf QQQ # ETF holdings + sectors (needs ZFIN_USER_EMAIL) +zfin options AAPL # options chain (no key) +``` + +`zfin perf VTI` prints Morningstar-style trailing returns: + +``` +Trailing Returns for VTI +======================================== +Data points: 6290 (2001-05-31 to 2026-06-04) +Latest close: $373.38 + +As-of 2026-06-04: + Price Only Total Return +---------------------- -------------- -------------- + 1-Year Return: 27.29% 28.79% + 3-Year Return: 20.75% 22.22% ann. + 5-Year Return: 11.22% 12.80% ann. + 10-Year Return: 13.19% 15.10% ann. +``` + +A command that needs a key you haven't set will say so and name the +key -- see the key table in [step 2](#2-get-an-api-key) or +[Data providers and API keys](reference/providers.md). Curious what the +columns mean? See +[Returns and performance](explanation/returns-and-performance.md). + +### Explore the example portfolios (optional) + +Portfolio features -- summaries, allocation breakdowns, retirement +projections -- need a portfolio file. To try them before building your +own, zfin ships five fictional households under `examples/`. If you +built from source you already have them; with the pre-built binary, +clone the repo to get a copy: + +```bash +git clone https://git.lerch.org/lobo/zfin.git && cd zfin +``` + +Point `ZFIN_HOME` at any example and zfin treats it as your data: + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin portfolio +ZFIN_HOME=examples/pre-retirement-both zfin analysis +ZFIN_HOME=examples/pre-retirement-both zfin projections +``` + +``` +Portfolio Summary (examples/pre-retirement-both/portfolio.srf) +======================================== + Value: $1,383,137.81 Cost: $658,837.01 Gain/Loss: +$724,300.80 (109.9%) + Lots: 13 open, 0 closed Positions: 5 symbols + Historical: 1M: +3.2% 3M: +13.3% 1Y: +24.5% 3Y: +56.4% 5Y: +56.1% 10Y: +182.6% + + Symbol Shares Avg Cost Price Market Value Gain/Loss Weight ... + ------- -------- ---------- ---------- ---------------- -------------- -------- ... + AGG 980.0 $115.23 $98.90 $96,922.00 - $16,002.00 7.0% + ... +``` + +The five households each demonstrate a different planning scenario. To +interpret this output see +[Read your portfolio](guides/read-your-portfolio.md); for the scenarios +themselves see [the examples tour](../examples/README.md) and +[Plan for retirement](guides/plan-retirement.md). + +## 5. Create your own portfolio + +A portfolio is a plain-text [`.srf`](reference/config/portfolio-srf.md) +file: one line per lot (a batch of shares bought on a date). Keep it +**outside** the repository so you never commit real holdings. Make a +private directory and a file: + +```bash +mkdir -p ~/finance +$EDITOR ~/finance/portfolio.srf +``` + +A minimal portfolio with two positions and some cash: + +```srf +#!srfv1 +symbol::VTI,shares:num:100,open_date::2020-01-15,open_price:num:170.00,account::My Brokerage +symbol::SCHD,shares:num:200,open_date::2021-06-01,open_price:num:75.00,account::My Brokerage +security_type::cash,shares:num:5000.00,open_date::2026-01-01,open_price:num:1.00,account::My Brokerage +``` + +Point zfin at it: + +```bash +ZFIN_HOME=~/finance zfin portfolio +``` + +zfin also auto-detects `portfolio.srf` in the current directory, so +from inside `~/finance` you can just run `zfin portfolio`. + +To go further -- options, CDs, illiquid assets, manual prices, ticker +aliases, DRIP lots -- see +[Build your portfolio](guides/set-up-your-portfolio.md) and the +[`portfolio.srf` reference](reference/config/portfolio-srf.md). + +## 6. Open the interactive TUI + +Everything the CLI shows is also available in a multi-tab terminal UI: + +```bash +ZFIN_HOME=~/finance zfin i +``` + +Navigate tabs with `h`/`l` (or arrow keys), move the cursor with +`j`/`k`, press `?` for the keybinding overlay, and `q` to quit. See +[The interactive TUI](reference/tui.md) for the full tour. + +## Next steps + +- **Understand the model:** [Core concepts](explanation/concepts.md) +- **Build out your data:** [Classify your holdings](guides/classify-holdings.md) + and [Map your accounts](guides/set-up-accounts.md) +- **Plan ahead:** [Plan for retirement](guides/plan-retirement.md) +- **Browse everything:** the [documentation home](README.md) diff --git a/docs/guides/audit-against-brokerage.md b/docs/guides/audit-against-brokerage.md new file mode 100644 index 0000000..3cc24cc --- /dev/null +++ b/docs/guides/audit-against-brokerage.md @@ -0,0 +1,244 @@ +# Audit against your brokerage + +**Goal:** catch drift between what zfin thinks you hold and what your +brokerage actually reports -- wrong share counts, sales you forgot to +record, missing lots, stale manual prices -- by reconciling +`portfolio.srf` against a positions export. + +**You'll need:** a portfolio whose [`accounts.srf`](set-up-accounts.md) +entries carry `institution::` and `account_number::` (that's how zfin +ties an export back to your accounts -- see +[How accounts are matched](#how-accounts-are-matched)), plus an export +from a supported broker. + +> Heads up: this is the most heuristic corner of zfin. The brokerage +> parsers are format-specific, account matching depends on metadata you +> maintain, and the comparison uses deliberate tolerances. It's the best +> way to keep your records honest, but expect a little setup and the +> occasional "why didn't that match?" -- this guide covers the gotchas, +> not just the happy path. + +## Supported brokers and how to export + +`zfin audit` reconciles against **Fidelity** and **Schwab**. (Wells +Fargo is handled by [`import`](#what-about-wells-fargo), not audit.) + +| Broker | How to export | Flag | +|--------------------------|-------------------------------------------------------------------------------------------------------------|--------------------| +| **Fidelity** | *Positions* tab -> the three-dot (**⋮**) menu -> **Download** (a CSV) | `--fidelity ` | +| **Schwab** (per-account) | *Accounts -> Positions* -> **Export** (one CSV per account) | `--schwab ` | +| **Schwab** (summary) | *Accounts -> Summary*: select the accounts table and copy it ([what to copy](#schwab-summary-what-to-copy)) | `--schwab-summary` | + +The two Schwab inputs differ in detail: the **per-account CSV** has full +per-position data (shares, price, value); the **summary paste** carries +only each account's cash and total value, so it reconciles *totals*, not +individual holdings. Use the summary for a quick "are my account totals +right?", the CSV for position-level checks. (Fidelity money-market rows +and Schwab "Cash & Cash Investments" rows are recognized as cash.) + +The Fidelity **Download** isn't a top-level button -- it's behind the +three-dot (**⋮**) menu at the top-right of the positions panel: + +![Fidelity Positions tab with the three-dot menu open, showing the Download item](../images/fidelity-positions-download.png) + +*(The account list down the left side is blanked out above.)* + +### Schwab summary: what to copy + +The summary paste comes from Schwab's **Accounts -> Summary** page. +Scroll to the **Accounts** table, drag-select from the first account's +name through the last row, and copy. Then either save it as a `.txt` +file in your `audit/` folder (where auto-discovery will find it) or pipe +it straight in: + +```bash +zfin audit --schwab-summary # paste, then Ctrl-D +``` + +A good paste is repeating three-line blocks -- the account name, the +"ending in" line, then a values line -- and looks about like this +(figures fictional): + +``` +Sample Roth IRA +Account number ending in 1234 ...1234 +Type IRA $46.44 $227,058.15 +$1,072.88 +0.47% +Sample Brokerage +Account number ending in 5678 ...5678 +Type Brokerage $12,500.00 $980,000.00 +$3,200.00 +0.33% +Sample Trust +Account number ending in 9012 ...9012 +Type $2,000.00 $415,300.00 +$1,150.00 +0.28% +``` + +zfin anchors on each **"Account number ending in"** line (its trailing +digits are the account number) and reads the **first two dollar figures** +on the line below as **cash** then **total value** -- everything else on +that line is ignored, and the account-type word is optional. If your copy +looks nothing like this -- no "ending in" lines, or no dollar figures -- +you grabbed the wrong region. Because it carries only cash and totals, +the summary reconciles **account totals**, not individual positions. + +## Run it + +**Point it at a file:** + +```bash +ZFIN_HOME=~/finance zfin audit --fidelity ~/Downloads/Portfolio_Positions.csv +ZFIN_HOME=~/finance zfin audit --schwab ~/Downloads/Positions-Individual.csv +ZFIN_HOME=~/finance zfin audit --schwab-summary # then paste the page, Ctrl-D +``` + +**Or run it with no flags** -- `zfin audit` does a portfolio hygiene +check *and* auto-discovers and reconciles any recent exports it finds +(next two sections). + +## The hygiene check + +With no flags, `zfin audit` first prints a health report: + +- **Stale manual prices** -- lots with a manual `price::` older than + `--stale-days` (default 3). +- **Accounts overdue for update** -- accounts past their + `update_cadence` (see [accounts.srf](set-up-accounts.md#3-tune-the-maintenance-cadence)). +- **Brokerage files** it discovered, which it then reconciles. + +``` + Portfolio hygiene + + Stale manual prices (>3 days — --stale-days to configure) + (none) + + Accounts overdue for update (weekly default — set update_cadence in accounts.srf) + Sample IRA weekly no update history found + Sample Brokerage weekly no update history found +``` + +"No update history found" is a nudge, not an error -- silence accounts +you don't actively track with `update_cadence::none`. + +## Auto-discovery (and your download folder) + +With no `--fidelity`/`--schwab` flag, zfin looks for exports in two +places: + +1. **`$ZFIN_AUDIT_FILES`** -- a directory *you* set. Point it at wherever + your browser saves downloads (e.g. `~/Downloads`) so a + just-downloaded export is found with no copying or renaming. (zfin + does **not** scan it on its own -- you opt in by setting this.) +2. **`/audit/`** -- a dedicated subfolder next to your + `portfolio.srf`, for exports you want to keep around. + +What it considers: + +- **Only files modified in the last 24 hours.** Your browser saves the + export to its download folder with names like + `Portfolio_Positions_Jun-19.csv`; the + recency window keeps zfin reconciling *the one you just pulled*, not + last quarter's. +- **Detected by content, not filename.** zfin sniffs the first lines -- + Fidelity begins `Account Number`/`Account Name`, a Schwab CSV begins + `"Positions for ...`, a Schwab summary contains `Account number ending + in`. A renamed file still works; an unrelated CSV is skipped. + +So with `ZFIN_AUDIT_FILES=~/Downloads`, the workflow collapses to +"download from your broker, run `zfin audit`, done." + +## How accounts are matched + +This is the part that trips people up. An export covers one or more +**accounts**, and zfin has to tie each one to an account in your +portfolio. It does that through +[`accounts.srf`](set-up-accounts.md#2-add-institution-and-account-number-for-auditing): + +- The export carries an account number -- Schwab's from the "Positions + for account ...1234" title, Fidelity's from the Account Number column, + the summary's from "...ending in 1234". +- zfin finds the `accounts.srf` entry whose `institution::` (`fidelity`, + `schwab`) **and** `account_number::` match, and compares against that + account's lots. +- **No match -> the account is shown as `unmapped`** and flagged as a + discrepancy. Fix it by adding `institution::` and `account_number::` + to that account in `accounts.srf` (a placeholder number you recognize + is fine -- it just has to match what the export shows). + +## Reading the report + +zfin treats the **brokerage as the source of truth** and shows, per +account, your portfolio (PF) against the broker (BR). Figures below are +illustrative and fictional: + +``` +Portfolio Audit (brokerage is source of truth) +======================================== + + Sample Brokerage *1234 + Symbol PF Shares BR Shares PF Price BR Price + VTI 100.000 100.000 373.38 373.38 ok + SCHD 200.000 210.000 31.86 31.86 brokerage +10.000 + AGG 50.000 0.000 portfolio only +``` + +- **Portfolio-only** rows are lots the broker no longer shows -- a sale + you forgot to remove, or a mistyped symbol. +- **Brokerage-only** rows are holdings missing from your portfolio. +- A share or value **delta** flags a count or price mismatch. + +`--verbose` prints the full comparison even when everything reconciles. + +### Why "close" counts as a match + +- **Cash matches to the penny.** It's an exact figure on both sides, so + any gap is real (e.g. money-market dividend accrual between updates) + and worth surfacing. +- **Securities get ~$1 of slack.** A sub-cent NAV-rounding difference on + a six-figure fund position can exceed a dollar without being + actionable, so small *value* deltas are tolerated -- but *share-count* + mismatches never are. + +### Institutional share classes + +If a lot is priced through a retail-ticker `ticker::` alias while the +account actually holds an institutional class (a different NAV), audit +compares against the broker's NAV and can **suggest a `price_ratio`** to +bridge the gap. Accounts flagged `direct_indexing::true` get the same +treatment to track drift. See +[price resolution](../reference/config/portfolio-srf.md#advanced-and-option-fields). + +## Why it's finicky + +- The parsers are **broker-specific and hardcode each export's column + layout** -- if Fidelity or Schwab changes their format, parsing can + break (Fidelity's header is validated to catch this; Schwab's is not). + They are not full RFC-4180 CSV parsers (no escaped quotes or + multi-line fields) -- fine for the real exports, not for arbitrary + CSVs. +- Matching is only as good as the `institution::` / `account_number::` + entries you keep in `accounts.srf`. +- Options and cash are reconciled separately from share counts. + +None of this is a reason to skip it -- it's the single best way to keep +your records honest -- just know it expects some setup and an occasional +manual nudge. + +### What about Wells Fargo? + +Wells Fargo's portal has no clean positions export, so it isn't an +`audit` target. Instead, [`zfin import --wells-fargo`](../reference/cli/import.md) +rebuilds a portfolio file from a paste of the WF positions table (copy +the rendered table from the brokerage portal and save it to a file). +Fidelity and Schwab exports can be imported the same way. + +> **Keep brokerage exports private.** They contain real account numbers +> and holdings. Store them outside any git repository and delete them +> when you're done reconciling. + +## Next steps + +- [`zfin audit` reference](../reference/cli/audit.md) -- every flag. +- [Map your accounts](set-up-accounts.md) -- the `institution` / `account_number` matching keys. +- [`zfin import`](../reference/cli/import.md) -- build a portfolio file *from* an export (including Wells Fargo). + +--- + +[Previous: Plan for retirement](plan-retirement.md) | [Next: A periodic review](periodic-review.md) | [Documentation home](../README.md) diff --git a/docs/guides/classify-holdings.md b/docs/guides/classify-holdings.md new file mode 100644 index 0000000..a763614 --- /dev/null +++ b/docs/guides/classify-holdings.md @@ -0,0 +1,114 @@ +# Classify your holdings + +**Goal:** create a `metadata.srf` that tells zfin the asset class, +sector, and geography of each symbol, so +[`zfin analysis`](../reference/cli/analysis.md) and +[`zfin review`](../reference/cli/review.md) -- and their TUI tabs -- +can group your holdings by category and sector. + +**You'll need:** a `portfolio.srf` ([build one first](set-up-your-portfolio.md)). +For the automatic path, set `ZFIN_USER_EMAIL` (SEC EDGAR requires a +contact address). Full field list: +[`metadata.srf` reference](../reference/config/metadata-srf.md). + +## Why classify? + +Without `metadata.srf`, zfin can value your portfolio but can't group +it. Classification feeds the Asset Category / Sector / Geographic +breakdowns in [`zfin analysis`](../reference/cli/analysis.md), and the +per-holding Sector column and grouping in +[`zfin review`](../reference/cli/review.md) -- each as a CLI command +and as its tab in the TUI. A few lines unlock all of them: + +``` + Asset Category + Equity ██████████████████████████▋ 89.2% $1,233,151.30 + Fixed Income ██ 7.0% $96,922.00 + Cash █▏ 3.8% $53,064.51 +``` + +## Option A: bootstrap with `enrich` (recommended) + +[`zfin enrich`](../reference/cli/enrich.md) queries Wikidata and SEC +EDGAR to generate classification lines for you. Point it at your +portfolio and redirect to `metadata.srf`: + +```bash +ZFIN_HOME=~/finance zfin enrich portfolio.srf > ~/finance/metadata.srf +``` + +It writes a complete SRF file (header included) with one entry per +stock symbol. Symbols Wikidata doesn't know fall back to EDGAR's +mutual-fund map; anything that misses both is emitted as a `TODO` +line for you to fill in by hand. + +To add a single symbol to an existing file, give `enrich` a symbol +instead of a file -- it prints just the classification lines (no +header), so you can append: + +```bash +zfin enrich SCHD >> ~/finance/metadata.srf +``` + +## Option B: write it by hand + +For a small portfolio, hand-writing is quick. One line per symbol: + +```srf +#!srfv1 +symbol::VTI,sector::Diversified,geo::US,asset_class::US Large Cap +symbol::AGG,sector::Bonds,geo::US,asset_class::Bonds +symbol::QQQ,sector::Technology,geo::US,asset_class::US Large Cap +``` + +The `symbol::` must match the `symbol::` (or `ticker::`) used in your +portfolio. Cash and CDs are classified as "Cash & CDs" automatically. + +## Blended funds + +A target-date or balanced fund spans several asset classes. Add one +line per slice with `pct:num:` weights that sum to ~100: + +```srf +symbol::02315N600,asset_class::US Large Cap,pct:num:55 +symbol::02315N600,asset_class::International Developed,pct:num:20 +symbol::02315N600,asset_class::Bonds,pct:num:15 +symbol::02315N600,asset_class::Emerging Markets,pct:num:10 +``` + +## Fixing uninformative sectors + +ETF holdings data sometimes tags everything as the generic +"Equity / Corporate," which collapses distinct holdings into one +meaningless group. When that happens, set a `bucket::` label yourself +to a grouping that actually distinguishes them -- it overrides the +auto-derived sector for concentration and dominance analysis. See the +[`bucket` field](../reference/config/metadata-srf.md#the-bucket-field). + +For example, two broad funds that would both auto-bucket as +"Diversified" can be split into meaningful groups so concentration and +dominance analysis treat them separately: + +```srf +#!srfv1 +symbol::VTI,sector::Diversified,geo::US,asset_class::US Large Cap,bucket::US Total Market +symbol::SCHD,sector::Diversified,geo::US,asset_class::US Large Cap,bucket::US Dividend +``` + +## Verify + +```bash +ZFIN_HOME=~/finance zfin analysis +``` + +If a symbol shows up under "Unclassified," it's missing a metadata +entry (or the symbol doesn't match). Add a line and re-run. + +## Next steps + +- [Map your accounts](set-up-accounts.md) for the tax-type breakdown. +- [Read your portfolio](read-your-portfolio.md) to interpret the analysis. + +--- + +[Previous: Build your portfolio](set-up-your-portfolio.md) | [Next: Map your accounts](set-up-accounts.md) | [Documentation home](../README.md) diff --git a/docs/guides/customize-the-tui.md b/docs/guides/customize-the-tui.md new file mode 100644 index 0000000..1ba3d8b --- /dev/null +++ b/docs/guides/customize-the-tui.md @@ -0,0 +1,73 @@ +# Customize the TUI + +**Goal:** rebind the interactive TUI's keys and recolor its interface +to your taste. + +The TUI reads two optional config files from `~/.config/zfin/`: +[`keys.srf`](../reference/config/keys-srf.md) for keybindings and +[`theme.srf`](../reference/config/theme-srf.md) for colors. When a file +is absent, built-in defaults apply; when present, it is the **sole** +source for that setting. + +## Rebind keys + +Generate a fully-commented starting file from the current defaults, +then edit it: + +```bash +mkdir -p ~/.config/zfin +zfin interactive --default-keys > ~/.config/zfin/keys.srf +$EDITOR ~/.config/zfin/keys.srf +``` + +Each line binds one action to one key: + +```srf +action::quit,key::q +action::next_tab,key::l +action::next_tab,key::right +``` + +- Add modifiers with `ctrl+`, `alt+`, `shift+` (e.g. `ctrl+d`). +- Bind several keys to an action by repeating the line. +- Scope a binding to one tab with `scope::` (e.g. + `scope::options`); the `action::` then names that tab's local + action. A tab-local binding may not reuse a globally-bound key -- + zfin refuses to start if you create that conflict. + +Full key vocabulary and the default bindings: +[`keys.srf` reference](../reference/config/keys-srf.md). + +## Recolor the interface + +Same pattern for the theme: + +```bash +zfin interactive --default-theme > ~/.config/zfin/theme.srf +$EDITOR ~/.config/zfin/theme.srf +``` + +Every value is a hex RGB string: + +```srf +#!srfv1 +bg::#0a0a0a +text::#eeeeee +accent::#9d7cd8 +positive::#7fd88f +negative::#e06c75 +``` + +The keys cover backgrounds, tabs, text, status line, the modal input, +gains/losses, warnings, selection, and borders -- see the +[`theme.srf` reference](../reference/config/theme-srf.md) for the full +list. + +## See also + +- [The interactive TUI](../reference/tui.md) -- tabs, actions, and the `?` help overlay. +- [`keys.srf`](../reference/config/keys-srf.md) / [`theme.srf`](../reference/config/theme-srf.md) references. + +--- + +[Previous: A periodic review](periodic-review.md) | [Next: Offline use and refreshing data](offline-and-refresh.md) | [Documentation home](../README.md) diff --git a/docs/guides/offline-and-refresh.md b/docs/guides/offline-and-refresh.md new file mode 100644 index 0000000..1572029 --- /dev/null +++ b/docs/guides/offline-and-refresh.md @@ -0,0 +1,80 @@ +# Offline use and refreshing data + +**Goal:** control when zfin talks to the network -- force a full +refresh, work entirely offline, or rely on normal cache freshness. + +zfin caches every fetch under `~/.cache/zfin` (override with +`ZFIN_CACHE_DIR`) and reuses it until it goes stale. The global +`--refresh-data` flag overrides that policy for a single run. + +## The three policies + +`--refresh-data` must appear **before** the subcommand: + +```bash +zfin --refresh-data=auto portfolio # default +zfin --refresh-data=force perf VTI +zfin --refresh-data=never analysis +``` + +| Value | Behavior | +|------------------|---------------------------------------------------------------------------------------------------------------| +| `auto` (default) | Respect each data type's cache TTL; fetch only what's stale. | +| `force` | Re-fetch every symbol regardless of freshness. Use after a market close, or when you suspect bad cached data. | +| `never` | Serve cache contents only; make no network calls. True offline mode. | + +## Working offline + +`--refresh-data=never` is the way to run on a plane or to get +deterministic output from already-cached data. Anything not in the +cache simply isn't shown: + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin --refresh-data=never analysis +``` + +Note that **quotes are never cached** (they're meant to be live), so +in `never` mode the [`quote`](../reference/cli/quote.md) command has +nothing to serve and reports the symbol as unavailable. Price-history +commands like [`perf`](../reference/cli/perf.md) and +[`portfolio`](../reference/cli/portfolio.md) work fine offline once +their candles are cached. + +## How freshness works + +In `auto` mode, each data type has its own time-to-live: + +| Data | TTL | +|--------------------|------------------------------------------------| +| Daily candles | ~24 hours | +| Dividends / splits | 14 days | +| Options | 1 hour | +| Earnings | 30 days (refreshed early once a result is due) | +| ETF profiles | ~30 days | +| Quotes | never cached | + +So a second `portfolio` run the same day is instant and network-free +without any flag. For the full rationale, see +[Caching and data freshness](../explanation/caching.md). + +## Inspecting and clearing the cache + +```bash +zfin cache stats # what's cached, sizes, and ages +zfin cache clear # delete all cached data +``` + +`cache clear` is safe -- everything re-fetches on the next run (subject +to provider rate limits). Reach for it only when you suspect corrupt +cached data; normal staleness is handled by `auto`. See +[`zfin cache`](../reference/cli/cache.md). + +## See also + +- [Caching and data freshness](../explanation/caching.md) -- the why. +- [Data providers and API keys](../reference/providers.md) -- rate limits that shape `force` runs. +- [Environment variables](../reference/config/environment.md) -- `ZFIN_CACHE_DIR`, `ZFIN_SERVER`. + +--- + +[Previous: Customize the TUI](customize-the-tui.md) | [Documentation home](../README.md) diff --git a/docs/guides/periodic-review.md b/docs/guides/periodic-review.md new file mode 100644 index 0000000..dd9c425 --- /dev/null +++ b/docs/guides/periodic-review.md @@ -0,0 +1,177 @@ +# A periodic review + +**Goal:** on a regular cadence -- weekly works well -- make zfin agree +with your brokerages, see what changed since last time, and commit the +reconciled state so the *next* review has a clean baseline to compare +against. + +This is the loop that ties the other guides together. Each step has its +own guide for the details; this one is the routine you actually run. It +settles into well under an hour once it's habit. + +``` + reconcile ──► what changed? ──► (projections) ──► commit + (audit) (compare) (optional) (baseline) + ▲ │ + └──────────────── next review ◄────────────────────────┘ +``` + +## 1. Reconcile against your brokerages + +Pull a fresh export from each brokerage that offers one and drop it in +your `audit/` folder (or wherever you've pointed `$ZFIN_AUDIT_FILES` -- +see [Audit against your brokerage](audit-against-brokerage.md) for the +export steps and auto-discovery rules). Then: + +```bash +zfin audit +``` + +Fix any flagged share or cash discrepancy in `portfolio.srf` and re-run +until it reconciles. A tight edit loop helps: if you have +[`watchexec`](https://github.com/watchexec/watchexec) installed, + +```bash +watchexec -- zfin audit +``` + +re-runs the audit on every save, so the remaining-discrepancy list +shrinks live as you fix lots -- much faster than alt-tab, rerun, read, +alt-tab, edit. + +**Reconcile first, for a reason.** Step 2 splits your change in value +into **contributions vs. market gains**, and that split is only as +honest as your share counts. Reconcile before you read the headline or +the attribution will lie to you. + +### Blind spots the audit can't see + +- **Accounts with no export** (some insurers, some 401(k) + recordkeepers) -- check the latest statement and update + `portfolio.srf` by hand. +- **Payroll-driven cash** -- e.g. ESPP contributions that haven't + purchased yet won't appear in a positions export until the purchase + posts. Reconcile those against a paystub. +- **A small, expected standing discrepancy** -- some accounts just sit a + few dollars off every week. Note it and move on rather than chasing it + each time. +- **Lagging transaction views.** A brokerage's *positions* view can + update overnight before its *transaction* view posts the dividend, + interest, or option assignment that caused the change. Trust the + positions numbers and reconcile the totals; the cause-side record + catches up later and isn't needed to get today's counts right. + +## 2. The headline -- what changed since last time + +One command gives you the whole "since last review" picture: + +```bash +zfin compare 1W --projections --commit-before HEAD +``` + +- **`1W`** is the point of comparison -- the snapshot from one week ago. + Any [relative shortcut](#relative-dates) or an explicit `YYYY-MM-DD` + works. +- **`--projections`** folds in projected-return and safe-withdrawal + (SWR@99%) deltas, then vs. now. (Costs ~1-2s per endpoint for the + Monte Carlo search; add `--no-events` to exclude life events.) +- **`--commit-before HEAD`** pins the contributions/gains attribution to + your latest reconciliation commit. This matters -- see + [Attribution and commit timing](#attribution-and-commit-timing). + +Read off the liquid-total delta, the contributions-vs-gains split, the +per-symbol winners and losers, and the projection deltas. For most +weeks, this single command *is* the review. See +[Snapshots and history](snapshots-and-history.md) for a full walk +through `compare` output. + +## 3. (Optional) Full projections + +`compare --projections` gives you the deltas but not the full benchmark +table or every scenario row. When you want the complete picture: + +```bash +zfin projections # default: with life events (SS, college, ...) +zfin projections --no-events # baseline: life events excluded +zfin projections --as-of 1W # the same table as of last review +zfin projections --vs 1W # both ends of the window in one run +``` + +See [Plan for retirement](plan-retirement.md) for what these rows mean. + +## 4. Commit -- the baseline for next time + +```bash +git add portfolio.srf metadata.srf history/ +git commit -m "review 2026-06-20" +``` + +Committing does double duty: + +- It snapshots the reconciled `portfolio.srf`, and the day's snapshot + file in `history/` rides along, so future `--as-of` runs can read it. +- zfin walks the git history of `portfolio.srf` for + [contributions](track-contributions.md) analysis, treating each commit + as a reconciliation point. **Commit timing sets next review's + baseline** -- a same-day commit keeps a weekly cadence clean. + +If you keep your portfolio directory in git (recommended), this is also +your backup and your audit trail. + +## Attribution and commit timing + +`compare` and `contributions` work out "contributions vs. gains" by +walking the git history of `portfolio.srf`. The positional date (`1W`) +picks the *snapshot* whose prices you compare against; **`--commit-before`** +picks which *commit* anchors the attribution. Those two can drift apart. + +If you reconcile on Saturday but don't commit until Monday, next +Saturday's `1W` snapshot lands *before* your last commit -- so a bare +`compare 1W` would attribute **two weeks** of contributions to one week. +`--commit-before HEAD` sidesteps this by pinning attribution to your +most recent reconciliation commit regardless of the snapshot date. When +the dates line up anyway, the flag is harmless -- which is why it's +worth making a habit. + +## Relative dates + +Every date-accepting command takes the same shorthand, so you rarely +type a full date: + +| Shortcut | Means | +|----------|-----------------| +| `1W` | one week ago | +| `3W` | three weeks ago | +| `1M` | one month ago | +| `1Q` | one quarter ago | +| `1Y` | one year ago | + +`compare`, `contributions --since/--until`, `projections --as-of/--vs`, +`snapshot --as-of`, and `history --since/--until` all accept it (plus an +explicit `YYYY-MM-DD`). + +## If there's no snapshot yet + +The comparison needs a snapshot to compare against. If your daily +snapshot didn't run, or you're reviewing off your normal cadence, make +one first: + +```bash +zfin portfolio --refresh # fresh close prices for tracked symbols +zfin snapshot # writes history/-portfolio.srf +``` + +See [Snapshots and history](snapshots-and-history.md) for the cron setup +that automates daily snapshots. + +## Next steps + +- [Audit against your brokerage](audit-against-brokerage.md) -- step 1 in depth. +- [Snapshots and history](snapshots-and-history.md) -- `compare`, snapshots, and the timeline. +- [Track contributions](track-contributions.md) -- the contributions / gains attribution. +- [Plan for retirement](plan-retirement.md) -- the projection rows. +- [`compare`](../reference/cli/compare.md) and [`projections`](../reference/cli/projections.md) reference -- every flag. + +--- + +[Previous: Audit against your brokerage](audit-against-brokerage.md) | [Next: Customize the TUI](customize-the-tui.md) | [Documentation home](../README.md) diff --git a/docs/guides/plan-retirement.md b/docs/guides/plan-retirement.md new file mode 100644 index 0000000..7332f26 --- /dev/null +++ b/docs/guides/plan-retirement.md @@ -0,0 +1,310 @@ +# Plan for retirement + +**Goal:** configure a `projections.srf` and read the output to answer +the two questions that matter -- "given my retirement date, what can I +spend?" and "given my desired spending, when can I retire?" + +This guide uses the five bundled households. Each is fully configured; +run them and compare: + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin projections +``` + +For the field-by-field file format, see the +[`projections.srf` reference](../reference/config/projections-srf.md); +for how the simulation works under the hood, see +[The retirement projection model](../explanation/projections-model.md). + +## Why this is the payoff + +Everything you've recorded -- lots, accounts, contributions -- feeds the +question most tools answer badly: *can I actually retire, and on what?* +zfin answers it by **replaying history**. The engine implements the +[FIRECalc](https://firecalc.com/) algorithm over Robert Shiller's market +dataset going back to **1871**: it runs your portfolio through every +historical starting year as a separate retirement -- 1871, 1872, ... -- +including the cohorts who retired into 1929, 1966, or 2000. The spread +of those real outcomes is exactly what becomes the percentile bands and +safe-withdrawal numbers below. + +Because it's the same method over the same dataset, results should track +[FIRECalc.com](https://firecalc.com/) closely. For the full method and +its assumptions, see +[The retirement projection model](../explanation/projections-model.md). + +## Keeping the Shiller data current + +That historical dataset is **compiled into the binary** (from +`src/data/ie_data.csv`), not fetched at runtime -- so it's one of the +few things in zfin that needs a periodic refresh. Once each year's +final market and CPI numbers are published, the dataset should be +updated to add that year. zfin tracks this for you: once the dataset is +overdue, **every command** (CLI and TUI) prints a one-line reminder to +stderr, above its normal output -- so you don't have to go looking for +it. [`zfin doctor`](../reference/cli/doctor.md) also reports it in its +Environment section (a `WARN` instead of `OK`, with the date it was +last updated). + +Because the data is embedded, refreshing it means updating the +**binary**, not clearing a cache: + +- **Built from source:** pull the latest repo and `zig build` -- the + refreshed dataset recompiles in. +- **Pre-built binary:** install a newer release. + +It's not urgent -- stale data just means you're missing the most recent +year of history, and projections still run -- so treat it as "refresh +when convenient." + +## The two questions + +zfin runs a two-phase historical Monte Carlo: an **accumulation** phase +(contributions in, no spending) followed by a **distribution** phase +(spending out, no contributions). What you put in `projections.srf` +decides which question the display answers: + +| You configure | zfin answers | Try the example | +|----------------------------------------------------------|-------------------------------|---------------------------| +| A retirement **date** (`retirement_age`/`retirement_at`) | "What can I spend?" | `pre-retirement-age` | +| A target **spending** (`target_spending`) | "When can I retire?" | `pre-retirement-spending` | +| **Both** | both, side by side | `pre-retirement-both` | +| **Neither** | already-retired drawdown view | `post-retirement` | + +Every projection also opens with a benchmark comparison and a +`Projected return` line -- your holdings' blended expected return, +which feeds the simulation. + +## Question 1: "What can I spend?" (target date) + +`pre-retirement-age` sets `retirement_age:num:65` and an +`annual_contribution`, but no target spending: + +```bash +ZFIN_HOME=examples/pre-retirement-age zfin projections +``` + +The **Accumulation phase** block gives the dated headline and the +projected portfolio at retirement: + +``` +Accumulation phase: + Years until possible retirement: 19 (2046-04-12, ages 65/62) + Annual contributions: $80,000 (CPI-adjusted) + Median portfolio at retirement: $7,871,732.10 + Range (10th–90th percentile): $5,807,693.45 to $18,240,675.15 +``` + +Below it, the **Safe Withdrawal** table shows the sustainable annual +spend at each horizon and confidence level (FIRECalc-style historical +simulation): + +``` +Safe Withdrawal (FIRECalc historical simulation) + 25 Year 35 Year 50 Year +90% safe withdrawal $347,601 $311,857 $308,728 +99% safe withdrawal $314,920 $293,374 $264,002 +``` + +Read it as: "retiring in 2046 with this portfolio, I could withdraw +~$264k/yr and be 99% confident it lasts 50 years (historically)." + +## Question 2: "When can I retire?" (target spending) + +`pre-retirement-spending` sets `target_spending:num:80000` but no date. +zfin searches for the earliest year that sustains that spending and +renders the **Earliest retirement** grid: + +```bash +ZFIN_HOME=examples/pre-retirement-spending zfin projections +``` + +``` +Earliest retirement (target spending: $80,000/yr CPI-adjusted) + 25 Year 35 Year 50 Year + 90% confidence 2030-06-19 2030-06-19 2030-06-19 + 95% confidence 2030-06-19 2030-06-19 2030-06-19 + 99% confidence 2031-06-19 2031-06-19 2031-06-19 +``` + +One cell is **promoted** to the Accumulation-phase headline. The +default rule picks the longest horizon at 99% confidence that keeps the +oldest person under age 100. Override it by annotating one horizon line +in `projections.srf`: + +```srf +type::config,horizon:num:35,retirement_target:num:95 +``` + +## Both questions at once + +`pre-retirement-both` sets a date **and** a spending target, so both +blocks render back to back -- "I planned to retire in 2046; at these +confidence levels I could actually retire as early as 2030." The +configured date wins the headline; the grid is the comparison. + +## When a plan isn't feasible + +`pre-retirement-spending-target` sets an aggressive +`target_spending:num:2400000` and pins the headline to the +longest-horizon, highest-confidence cell -- which turns out to be +unreachable inside the 50-year search: + +```bash +ZFIN_HOME=examples/pre-retirement-spending-target zfin projections +``` + +``` +Accumulation phase: + Years until possible retirement: not feasible + +Earliest retirement (target spending: $2,400,000/yr CPI-adjusted) + 25 Year 35 Year 50 Year + 99% confidence 2075-06-19 infeasible infeasible +``` + +The headline reports "not feasible" honestly, and the grid still shows +which cells *do* work so you can choose a reachable anchor. + +## Already retired: the drawdown view + +`post-retirement` configures neither input -- it's a distribution-only +household: + +```bash +ZFIN_HOME=examples/post-retirement zfin projections +``` + +The accumulation block collapses to a single line, confirming no +pre-retirement growth is being modeled: + +``` +Accumulation phase: + Years until possible retirement: none +``` + +Everything else -- the median-value chart, terminal-value percentiles, +and safe-withdrawal table over the configured horizons -- behaves as a +pure drawdown projection. + +## Life events + +Social Security, pensions, tuition, and late-life healthcare are +modeled as `type::event` lines (positive = income, negative = +expense). They appear in the Life Events block and shift the cash-flow +math in both phases: + +``` +Life Events + Social Security (Pat) +$38,400/yr age 70 (in 25yr) + College Tuition -$55,000/yr age 50 (in 5yr), 4yr +``` + +See [event fields](../reference/config/projections-srf.md#event-fields). + +## Check the model against reality + +Once you have [snapshot history](snapshots-and-history.md) (or imported +back-values), zfin can grade its own past projections three ways: + +- **Actuals overlay** -- plot your realized trajectory on top of the + bands the model *would have drawn* from a past date. Did reality stay + inside the envelope? + + ```bash + zfin projections --as-of 1Y --overlay-actuals + ``` + +- **Convergence** (`--convergence`) -- as data accumulated, did the + model's predicted retirement date settle down, or keep drifting? + +- **Return back-test** (`--return-backtest`) -- was the + expected-return assumption honest next to the realized forward + returns? + +The CLI prints these as text and braille; the TUI draws them as real +charts (next section). + +> A caveat zfin states loudly: these show whether the model was +> **directionally honest** -- did your actual path fall within the +> bands it drew -- **not** whether a safe-withdrawal claim holds over a +> full 30-year retirement. There isn't enough history to answer the +> latter, and won't be within our lifetimes. + +## In the interactive TUI + +The CLI gives you the numbers; the **Projections tab** in the TUI +(`zfin i`, then tab over to Projections) is where it comes alive, with +high-fidelity charts the plain terminal can't draw. Press `?` for the +full keymap; the projections-specific keys: + +| Key | Does | +|-------|----------------------------------------------------------------------------------------------------------------| +| `v` | Show/hide the **percentile-band chart** -- the median line with the p10-p90 envelope across the horizon. | +| `d` | Set an **as-of date** -- back-date the whole projection to any past date (auto-snaps to the nearest snapshot). | +| `o` | Overlay your **realized actuals** on the bands (needs an as-of date plus snapshot/imported history). | +| `z` | Zoom the overlay's x-axis to roughly `[as-of, today + horizon]`. | +| `c` | **Convergence** chart -- the model's predicted retirement date over time. | +| `b` | **Return back-test** chart -- expected vs. realized forward returns. | +| `e` | Show/hide the life-events annotations. | +| `Esc` | Clear the as-of date, back to the live view. | + +A typical what-if loop: open the Projections tab, press `d` and enter a +date a few years back, then `o` to drop your real trajectory onto the +bands the model would have drawn then -- a visual, honest check of how +the projection has held up. `c` and `b` then grade the model's +retirement-date and return assumptions over time. + +Charts render as crisp Kitty graphics when your terminal supports it, +and fall back to braille otherwise (see +[`--chart`](../reference/cli/interactive.md) and +[The interactive TUI](../reference/tui.md)). + +## Example: a complete `projections.srf` + +This is the +[`pre-retirement-both`](../../examples/pre-retirement-both/projections.srf) +household: Pat (born 1981) and Sam (born 1983), retiring at 65 and +targeting $80k/yr, with both an accumulation and a distribution phase. +Copy it as a starting point and change the numbers to yours. + +```srf +#!srfv1 + +# Accumulation phase (while still working) +type::config,retirement_age:num:65 +type::config,annual_contribution:num:80000 +type::config,contribution_inflation_adjusted:bool:true + +# Distribution phase (in retirement) +type::config,target_stock_pct:num:80 +type::config,target_spending:num:80000 +type::config,target_spending_inflation_adjusted:bool:true +type::config,horizon:num:25 +type::config,horizon:num:35 +type::config,horizon_age:num:95 + +# The two people (drive ages, retirement_age, and life-event timing) +type::birthdate,date::1981-04-12 +type::birthdate,date::1983-09-08,person:num:2 + +# Life events (positive = income, negative = expense) +type::event,name::Social Security (Pat),start_age:num:70,person:num:1,amount:num:38400 +type::event,name::Social Security (Sam),start_age:num:70,person:num:2,amount:num:36000 +type::event,name::College Tuition,start_age:num:50,person:num:1,duration:num:4,amount:num:-55000 +``` + +Run it with `ZFIN_HOME=examples/pre-retirement-both zfin projections`; +every field is documented in the +[`projections.srf` reference](../reference/config/projections-srf.md). + +## Next steps + +- [`projections.srf` reference](../reference/config/projections-srf.md) -- every field. +- [The retirement projection model](../explanation/projections-model.md) -- the math and assumptions. +- [The interactive TUI](../reference/tui.md) -- the Projections tab and its charts. +- [Snapshots and history](snapshots-and-history.md) -- build the actuals the overlay needs. + +--- + +[Previous: Snapshots and history](snapshots-and-history.md) | [Next: Audit against your brokerage](audit-against-brokerage.md) | [Documentation home](../README.md) diff --git a/docs/guides/read-your-portfolio.md b/docs/guides/read-your-portfolio.md new file mode 100644 index 0000000..38c62d9 --- /dev/null +++ b/docs/guides/read-your-portfolio.md @@ -0,0 +1,190 @@ +# Read your portfolio + +**Goal:** make sense of what zfin shows you. This guide walks the five +commands you'll reach for most -- `portfolio`, `analysis`, `review`, +`exposure`, and `perf` -- and explains how to read each one. + +Every example below runs against the bundled +[`pre-retirement-both`](../../examples/pre-retirement-both/) household, +so you can follow along verbatim: + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin portfolio +``` + +(Dollar figures depend on live prices, so yours will differ.) + +## `portfolio`: positions and value + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin portfolio +``` + +``` +Portfolio Summary (examples/pre-retirement-both/portfolio.srf) +======================================== + Value: $1,383,137.81 Cost: $658,837.01 Gain/Loss: +$724,300.80 (109.9%) + Lots: 13 open, 0 closed Positions: 5 symbols + Historical: 1M: +3.2% 3M: +13.3% 1Y: +24.5% 3Y: +56.4% 5Y: +56.1% 10Y: +182.6% + + Symbol Shares Avg Cost Price Market Value Gain/Loss Weight ... + VTI 2480.0 $138.35 $373.38 $925,982.40 + $582,874.40 66.9% + open 1100.0 $140.00 $410,718.00 + $256,718.00 2018-06-15 LT Pat 401k + ... +``` + +How to read it: + +- **The header line** is your liquid total, total cost, and aggregate + gain/loss. +- **Historical** is the portfolio's blended price return over trailing + windows -- a quick "how have my holdings done" gut check. +- **Each position** shows aggregated shares, average cost, current + price, market value, gain/loss, and weight. Indented `open` rows are + the individual lots, tagged **LT**/**ST** (long/short-term holding + period) and the account. +- **Cash** is summarized by account at the bottom. + +Manual-priced rows render in warning color so you know the price may be +stale. Full output shape: [`zfin portfolio`](../reference/cli/portfolio.md). + +### Covered calls + +zfin values a written (short) call by **capping the covered shares at +the strike**, rather than pricing the option contract -- it never looks +up a live option quote. When the call is in-the-money (the stock trades +above the strike), the covered shares are valued at the strike, since +that's the price you'd be assigned at; an out-of-the-money call gets no +adjustment, and an expired or closed call stops capping. + +This differs from your brokerage, which tracks the call as its own line +with its own mark-to-market gain/loss. Example: you hold 100 MSFT and +wrote a $500 call. If MSFT trades at $510, zfin values those shares at +**$50,000** (100 x the $500 strike), not $51,000 (100 x $510) -- the +$1,000 of upside above the strike now belongs to the call holder. A +brokerage would instead show $51,000 of stock plus a separate, +losing short-call position. + +This is deliberate, and for a covered call used as an exit strategy +it's the more useful number: the cap is the value you'll actually +realize when the call assigns. Mark-to-market would understate it, +because an in-the-money call also carries time value you'd only pay to +*buy it back* -- which a let-it-assign writer never does. (At $510 the +call might mark around $14, so a brokerage nets $496/share even though +you'll realize the full $500 at assignment.) The flip side: zfin is +built for covered-call and buy-write investors, not active option +trading -- it deliberately doesn't track the live mark-to-market swings +of contracts you intend to trade rather than hold to assignment. + +## `analysis`: allocation breakdowns + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin analysis +``` + +`analysis` answers "how is my money allocated?" along five axes: +**Asset Category**, **Sector**, **Geographic**, **By Account**, and +**By Tax Type**. Each bar is a share of your liquid total. + +``` + By Tax Type + Traditional (Pre-Tax) █████████████████▋ 58.9% $815,290.06 + Taxable ██████▍ 21.6% $299,010.60 + Roth (Post-Tax) █████ 16.9% $233,732.95 + HSA (Triple Tax-Free) ▊ 2.5% $35,104.20 +``` + +The Sector and Asset Category axes need [`metadata.srf`](classify-holdings.md); +the Tax Type axis needs [`accounts.srf`](set-up-accounts.md). Anything +missing classification lands under "Unclassified" / "Unknown." + +### Umbrella exposure + +A personal **umbrella insurance** policy covers liability -- lawsuits +and judgments -- above your auto/home limits, and you size it to the +assets you'd need to protect. The last block estimates that target: how +much of your liquid net worth is **exposed** to a civil judgment +because it sits outside judgment-protected retirement accounts: + +``` + Umbrella exposure + Total liquid: $1,383,137.81 + Shielded (retirement accounts): $1,084,127.21 + Exposed (taxable + non-shielded pre-tax): $299,010.60 (21.6%) + ↑ approximate umbrella target +``` + +The default rule treats anything that isn't `taxable` as shielded. +Override per account with `shielded:bool:false` in +[`accounts.srf`](set-up-accounts.md#4-advanced-flags) -- IRA protection +varies by state and isn't modeled automatically. + +## `review`: per-holding performance and risk + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin review +``` + +`review` is a dashboard: one row per holding with trailing returns, +volatility, Sharpe ratio, max drawdown, and a taxable-percentage +column, plus automated **findings** at the bottom. + +``` + Symbol Sector Wt% 1Y 3Y ... 3Y-SR 10Y-SR 5Y-MaxDD Tax% + VTI Diversified 66.9% +30.2% +23.3% ... 1.29 0.90 24.8% 10.9% + QQQ Technology 3.5% +43.1% +29.3% ... 1.41 1.16 32.6% 0.0% + + Findings (2 active, 0 acked, 0 resolved) + ⚠️ VTI at 66.9% of liquid (warn at 50.0%, flag at 70.0%) + ❌️ Diversified sector at 85.7% (warn at 60.0%, flag at 75.0%) +``` + +The findings flag concentration, sector dominance, volatility +outliers, and tiny positions against configurable thresholds. The +status icons at the top summarize which checks fired. See +[`zfin review`](../reference/cli/review.md). + +## `exposure`: look-through to a single symbol + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin exposure SPY +``` + +`exposure` answers "how much of *X* do I really own?" -- combining +direct holdings with look-through into the ETFs you hold (matched by +CUSIP against each ETF's latest reported holdings): + +``` +Exposure to SPY (examples/pre-retirement-both/portfolio.srf) +======================================== + Total exposure 17.3% $238,956.80 + Direct 17.3% $238,956.80 + Look-through 0.0% $0.00 +``` + +This is most interesting for an individual stock you also hold inside +broad-market ETFs (e.g. checking your true NVDA exposure across VTI, +SPY, and QQQ). See [`zfin exposure`](../reference/cli/exposure.md). + +## `perf`: trailing returns for one symbol + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin perf VTI +``` + +`perf` shows Morningstar-style trailing returns (price-only **and** +total return), as-of the latest close and as-of the most recent +month-end, plus risk metrics. For what "total return," "annualized," +and "month-end" mean, see +[Returns and performance](../explanation/returns-and-performance.md). + +## Where to go next + +- Track change over time: [Snapshots and history](snapshots-and-history.md) +- See money added: [Track contributions](track-contributions.md) +- Look ahead: [Plan for retirement](plan-retirement.md) +- Command details: the [CLI reference](../reference/cli/index.md) + +--- + +[Previous: Map your accounts](set-up-accounts.md) | [Next: Track contributions](track-contributions.md) | [Documentation home](../README.md) diff --git a/docs/guides/set-up-accounts.md b/docs/guides/set-up-accounts.md new file mode 100644 index 0000000..f0ae3ee --- /dev/null +++ b/docs/guides/set-up-accounts.md @@ -0,0 +1,101 @@ +# Map your accounts + +**Goal:** create an `accounts.srf` that tags each account with its tax +treatment (and, optionally, its institution and maintenance cadence). +This unlocks the **By Tax Type** breakdown, an umbrella-insurance +exposure estimate, and broker reconciliation. + +**You'll need:** a `portfolio.srf` whose lots use `account::` labels +([build one first](set-up-your-portfolio.md)). Full field list: +[`accounts.srf` reference](../reference/config/accounts-srf.md). + +## 1. List your accounts with a tax type + +One record per account. The `account::` name must match the +`account::` value on your lots **exactly**. The minimum is a tax type: + +```srf +#!srfv1 +account::Pat 401k,tax_type::traditional +account::Pat Roth,tax_type::roth +account::Joint taxable,tax_type::taxable +account::Family HSA,tax_type::hsa +``` + +The four recognized tax types are `taxable`, `roth`, `traditional`, +and `hsa`. Run analysis to see the breakdown: + +```bash +ZFIN_HOME=~/finance zfin analysis +``` + +``` + By Tax Type + Traditional (Pre-Tax) █████████████████▋ 58.9% $815,290.06 + Taxable ██████▍ 21.6% $299,010.60 + Roth (Post-Tax) █████ 16.9% $233,732.95 + HSA (Triple Tax-Free) ▊ 2.5% $35,104.20 +``` + +Accounts you don't list show up as "Unknown." + +## 2. Add institution and account number (for auditing) + +If you plan to reconcile against brokerage exports +([audit guide](audit-against-brokerage.md)), add the institution and a +(placeholder) account number so zfin can match export files to +accounts: + +```srf +account::Pat 401k,tax_type::traditional,institution::fidelity,account_number::P401 +account::Joint taxable,tax_type::taxable,institution::schwab,account_number::JT01 +``` + +Recognized institution keys include `fidelity`, `schwab`, `vanguard`, +and `wells_fargo`. + +## 3. Tune the maintenance cadence + +[`zfin audit`](../reference/cli/audit.md) (run with no flags) nags you +about accounts you haven't refreshed recently. The default cadence is +`weekly`; relax or silence it per account: + +```srf +account::Family HSA,tax_type::hsa,update_cadence::quarterly +account::Old Rollover,tax_type::traditional,update_cadence::none +``` + +## 4. Advanced flags + +Two flags change how analysis treats an account. Both are optional -- +see the reference for details: + +- **`shielded:bool:false`** -- mark a pre-tax account that is *not* + judgment-protected (deferred comp, a weak-state IRA) so it counts + toward your [umbrella-insurance exposure](read-your-portfolio.md#umbrella-exposure) + -- the slice of net worth a personal umbrella liability policy is + meant to cover. +- **`cash_is_contribution:bool:true`** -- treat cash increases on this + account as real external contributions in + [`zfin contributions`](track-contributions.md), instead of internal + noise. + +## Example (from `examples/pre-retirement-both`) + +```srf +#!srfv1 +account::Pat 401k,tax_type::traditional,institution::fidelity,account_number::P401 +account::Pat Roth,tax_type::roth,institution::fidelity,account_number::PROTH +account::Sam 401k,tax_type::traditional,institution::vanguard,account_number::S401 +account::Joint taxable,tax_type::taxable,institution::schwab,account_number::JT01 +account::Family HSA,tax_type::hsa,institution::fidelity,account_number::HSA01 +``` + +## Next steps + +- [Read your portfolio](read-your-portfolio.md) -- the breakdowns this unlocks. +- [Audit against your brokerage](audit-against-brokerage.md) -- put `institution`/`account_number` to work. + +--- + +[Previous: Classify your holdings](classify-holdings.md) | [Next: Read your portfolio](read-your-portfolio.md) | [Documentation home](../README.md) diff --git a/docs/guides/set-up-your-portfolio.md b/docs/guides/set-up-your-portfolio.md new file mode 100644 index 0000000..5bb3e43 --- /dev/null +++ b/docs/guides/set-up-your-portfolio.md @@ -0,0 +1,193 @@ +# Build your portfolio + +**Goal:** create a `portfolio.srf` that captures your holdings -- the +shares you own, what you paid, and which account each lot lives in. +Everything else in zfin reads from this file. + +**You'll need:** a text editor and a private directory outside the +repo (so you never commit real holdings). This guide builds up a file +much like the one in +[`examples/pre-retirement-both`](../../examples/pre-retirement-both/portfolio.srf); +the full field list lives in the +[`portfolio.srf` reference](../reference/config/portfolio-srf.md). + +You only need this one file to begin. `accounts.srf`, `metadata.srf`, +`projections.srf`, and the rest are optional add-ons you layer on as +you need them -- zfin works without them, just with fewer breakdowns +(see [the `.srf` files overview](../explanation/concepts.md#the-srf-files)). + +## Key terms + +Three words show up throughout zfin, nesting from smallest to largest: + +- **Share** -- one unit of a security (a stock, ETF, or fund); the atom + of ownership. +- **Lot** -- one purchase: a batch of shares of a single security, + bought on one date at one price, in one account. A lot is the unit of + a `portfolio.srf` line. zfin tracks lots rather than running totals so + it can derive each lot's cost basis, holding period (short- vs + long-term), and gain/loss. +- **Position** (or **holding**) -- all the lots of the same security + rolled together **across every account**: total shares, average cost, + and market value. Your VTI shows as a single position even when it's + spread across a 401(k), a Roth IRA, and a taxable brokerage -- a + whole-household, cross-account rollup most brokerages won't show you, + and a core reason to run zfin. It's one row in the portfolio summary, + with its lots indented beneath. + +In short: you record **lots** (one per line) and zfin aggregates them +into **positions** for display. + +## 1. Start the file + +A portfolio is one lot per line -- one purchase. Create +`~/finance/portfolio.srf`: + +```srf +#!srfv1 +symbol::VTI,shares:num:1100,open_date::2018-06-15,open_price:num:140.00,account::Pat 401k +``` + +The `#!srfv1` header is required. Each line is comma-separated +`key::value` pairs; numbers use `key:num:value`. + +Check it: + +```bash +ZFIN_HOME=~/finance zfin portfolio +``` + +## 2. Add more lots + +Add one line per purchase. Multiple lots of the same symbol aggregate +into a single position automatically, so record each buy at its own +cost basis rather than averaging by hand: + +```srf +#!srfv1 +symbol::VTI,shares:num:1100,open_date::2018-06-15,open_price:num:140.00,account::Pat 401k +symbol::VTI,shares:num:240,open_date::2015-01-08,open_price:num:103.40,account::Pat Roth +symbol::AGG,shares:num:600,open_date::2020-01-10,open_price:num:115.50,account::Pat 401k +symbol::QQQ,shares:num:65,open_date::2019-11-22,open_price:num:200.10,account::Pat Roth +``` + +zfin shows each position with its lots, market value, and gain/loss: + +``` + VTI 2480.0 $138.35 $373.38 $925,982.40 + $582,874.40 66.9% + open 1100.0 $140.00 $410,718.00 + $256,718.00 2018-06-15 LT Pat 401k + open 240.0 $103.40 $89,611.20 + $64,795.20 2015-01-08 LT Pat Roth +``` + +## 3. Record cash + +Cash, money-market, and settlement balances are lots with +`security_type::cash`. They need no symbol, open date, or price: + +```srf +security_type::cash,shares:num:48000.00,open_date::2026-04-30,open_price:num:1.00,account::Joint taxable +``` + +Cash is grouped by account in its own section of the portfolio +summary. + +## 4. Use accounts consistently + +The `account::` value is just a label, but it has to match **exactly** +across lots (and later in [`accounts.srf`](set-up-accounts.md)). Pick +names and reuse them verbatim: `Pat 401k`, `Joint taxable`, +`Family HSA`. Account names drive the By-Account and (with +`accounts.srf`) the By-Tax-Type breakdowns in +[`zfin analysis`](read-your-portfolio.md). + +## 5. Special holdings + +zfin handles more than stocks and cash. Each is one line; see the +[reference](../reference/config/portfolio-srf.md) for full field +lists: + +- **Sold positions** -- add `close_date` and `close_price` to a lot. +- **Options** -- `security_type::option` with a readable symbol plus + explicit `option_type` / `underlying` / `strike` / `maturity_date` + fields; negative `shares` to write (sell) a contract. See the + covered-call example below. +- **CDs** -- `security_type::cd` with `maturity_date` and `rate`. +- **Illiquid assets** (home, vehicle) -- `security_type::illiquid`; + counted in Net Worth but not the liquid total. +- **Securities the providers don't cover** (e.g. a 401k CIT share + class) -- add a manual `price::` and `price_date::`, or a + `ticker::` alias for pricing. See + [price resolution](../reference/config/portfolio-srf.md#price-resolution). + +### Example: a covered call + +Options trip people up, so here's a worked one. A *covered call* is two +lots -- the shares you own, plus one call you write (sell) against them. +Say you hold 100 shares of MSFT and sell a call: + +```srf +#!srfv1 +# The 100 shares you own -- the "covered" part +symbol::MSFT,shares:num:100,open_date::2024-02-01,open_price:num:400.00,account::Joint taxable +# One call written against them +security_type::option,symbol::MSFT 06/19/2026 500.00 C,shares:num:-1,open_date::2026-01-15,open_price:num:6.68,option_type::call,underlying::MSFT,strike:num:500,maturity_date::2026-06-19,account::Joint taxable +``` + +Reading the option lot: + +- **`shares:num:-1`** -- you *wrote* one contract; negative means short + (sold). Each contract covers 100 shares. +- **`open_price:num:6.68`** -- the premium you received, per share + ($6.68 x 100 = $668 for the contract). +- **`option_type` / `underlying` / `strike` / `maturity_date`** define + the contract. `symbol` is just a human-readable label -- use whatever + your brokerage shows (here `MSFT 06/19/2026 500.00 C`). +- It's "covered" because the 100 MSFT shares in the same account back + the call. When it's closed or expires, add `close_date` and + `close_price` (use `0` if it expired worthless and you kept the + premium). + +**zfin values this differently from your brokerage.** Your brokerage +tracks the call as its own security with its own gain/loss. zfin +doesn't price the contract at all; instead it caps the covered shares +at the strike while the call is in-the-money. So if MSFT trades at $510 +here, zfin values these 100 shares at **$50,000** (the $500 strike), +not $51,000 -- the upside above the strike belongs to the call holder. +See [covered-call valuation](read-your-portfolio.md#covered-calls) for +the full rule. + +## 6. Optional: split across multiple files + +You can keep holdings in several files -- `portfolio.srf`, +`portfolio_401k.srf`, `portfolio_taxable.srf`. zfin union-merges every +`portfolio*.srf` in `ZFIN_HOME` by default, so the CLI and TUI both see +one combined view. Target a subset with `-p`: + +```bash +zfin -p 'portfolio_*.srf' portfolio # quote the glob so the shell doesn't expand it +zfin -p portfolio.srf -p portfolio_hsa.srf portfolio +``` + +**A good first split: closed positions.** When you sell, move the +closed (sold) lots into a `portfolio_closed_positions.srf`. Your main +`portfolio.srf` then shows only what you currently hold, while the sold +lots still merge in -- so realized gain/loss and back-dated +(`--as-of`) snapshots stay accurate. Because the filename matches +`portfolio*.srf`, it's picked up automatically -- no flag needed. + +```srf +#!srfv1 +# Sold lots live here so portfolio.srf stays focused on current holdings. +symbol::AMZN,shares:num:10,open_date::2022-03-15,open_price:num:150.25,close_date::2024-01-15,close_price:num:185.50,account::Joint taxable +``` + +## Next steps + +- [Classify your holdings](classify-holdings.md) so analysis can break + down your allocation. +- [Map your accounts](set-up-accounts.md) to unlock the tax-type view. +- [Read your portfolio](read-your-portfolio.md) to interpret the output. + +--- + +[Next: Classify your holdings](classify-holdings.md) | [Documentation home](../README.md) diff --git a/docs/guides/snapshots-and-history.md b/docs/guides/snapshots-and-history.md new file mode 100644 index 0000000..1a2c15d --- /dev/null +++ b/docs/guides/snapshots-and-history.md @@ -0,0 +1,149 @@ +# Snapshots and history + +**Goal:** record your portfolio's value over time and review how it has +changed -- day to day, year over year, and between any two dates. + +**You'll need:** a working portfolio. The +[`post-retirement`](../../examples/post-retirement/) example ships with +a `history/` folder of snapshots, so you can explore the read side +immediately: + +```bash +ZFIN_HOME=examples/post-retirement zfin history +``` + +## The idea + +zfin doesn't track your value automatically -- it reads what you have +*right now*. To build a time series, you write a **snapshot** each day +(or week), and the history/compare commands read those snapshots back. + +Snapshots live in `/history/-portfolio.srf`. Each +is an immutable record of totals, per-account values, and lot-level +state for one date. + +## 1. Write a snapshot + +The simplest way is to run it yourself. +[`zfin snapshot`](../reference/cli/snapshot.md) computes today's +snapshot and writes it under `history/`. Try a dry run first -- it +computes and prints the snapshot without writing anything: + +```bash +zfin snapshot --dry-run # compute + print, write nothing +zfin snapshot # write history/-portfolio.srf +``` + +Run it whenever you want a record on the books -- after a monthly +review, say. The snapshot is just a file in `history/`; nothing else is +required. + +**Back-filling a past date (optional, needs git).** If you keep your +portfolio file under [git](https://git-scm.com/) version control, +`--as-of` can reconstruct a snapshot for an earlier date: it recovers +your portfolio as it was then (from git) and prices it from the cached +price history. + +```bash +zfin snapshot --as-of 2025-01-02 +``` + +If you don't use git, skip this -- just snapshot going forward. + +## 2. Keep it current automatically (optional) + +Most people would rather not remember to run it every day. You can hand +that off to your operating system's **task scheduler** -- a built-in +service that runs a command on a set timetable, even while you're away. + +**When to schedule it.** Run it in the early morning, *after* the prior +trading day's closing prices have posted. ETF and mutual-fund values +update overnight, and while data providers say "after midnight," in +practice **3:30am US Eastern** is the first time yesterday's closing +NAVs reliably land -- schedule it earlier and you risk capturing stale +prices. + +**macOS and Linux** use `cron`. Run `crontab -e` and add a line like +this (3:30am, Monday-Friday -- adjust the hour for your machine's +timezone if it isn't US Eastern): + +```cron +30 3 * * 1-5 cd ~/finance && /usr/local/bin/zfin snapshot +``` + +The five fields are minute, hour, day-of-month, month, day-of-week +(`1-5` = Mon-Fri); the full path to `zfin` matters because cron runs +with a minimal `PATH`. macOS ships cron, though the first run may prompt +you to grant your terminal "Full Disk Access." + +**Windows** has no `cron` -- use Task Scheduler to run `zfin snapshot` +daily. (zfin should run on Windows, but it isn't regularly tested +there.) + +## 3. Review the timeline with `history` + +Run [`zfin history`](../reference/cli/history.md) with no symbol for the +portfolio-value timeline: rolling-window changes, a braille chart, and +a recent-snapshots table. + +```bash +ZFIN_HOME=examples/post-retirement zfin history +``` + +``` +Portfolio Timeline — Liquid +======================================== + Change Δ % % / yr + 1 year +$230,000.00 +9.79% +9.79% + 3 years +$459,059.08 +21.64% +6.72% + 5 years +$686,215.45 +36.24% +6.37% + All-time +$1,073,725.79 +71.28% +6.00% +``` + +Useful flags: `--metric liquid|illiquid|net_worth`, `--since` / +`--until` to bound the window, and `--resolution daily|weekly|monthly`. + +> The percentage change includes contributions and withdrawals, not +> just market movement. To separate new money from market gains, use +> [contributions](track-contributions.md). + +## 4. Compare two points with `compare` + +[`zfin compare`](../reference/cli/compare.md) diffs two dates: liquid +totals, per-symbol price moves, and -- when your portfolio is tracked +in git -- contribution attribution. Pass one date to compare against +the live portfolio, or two dates to compare historical snapshots: + +```bash +ZFIN_HOME=examples/post-retirement zfin compare 2024-04-01 2025-04-01 +``` + +``` +Portfolio comparison: 2024-04-01 → 2025-04-01 (365 days) + +Liquid: $2,350,000.00 → $2,580,000.00 +$230,000.00 +9.79% +``` + +Arguments can be given in any order; output always reads older -> +newer. On a missing snapshot date, `compare` prints the nearest +available dates and exits rather than silently snapping. Add +`--projections` to include projected-return and safe-withdrawal deltas. + +## Back-history without daily snapshots + +If you have historical totals from a spreadsheet but no per-day +snapshots, record them in +`history/imported_values.srf` (one `liquid::` total per date). The +history and projection-overlay tools read it as a lower-fidelity +fallback when no native snapshot covers a date. The post-retirement +example includes one spanning 2016-2024. + +## Next steps + +- [Track contributions](track-contributions.md) -- separate new money from gains. +- [Plan for retirement](plan-retirement.md) -- overlay actuals on projections. +- [`zfin snapshot`](../reference/cli/snapshot.md) / [`zfin history`](../reference/cli/history.md) / [`zfin compare`](../reference/cli/compare.md) + +--- + +[Previous: Track contributions](track-contributions.md) | [Next: Plan for retirement](plan-retirement.md) | [Documentation home](../README.md) diff --git a/docs/guides/track-contributions.md b/docs/guides/track-contributions.md new file mode 100644 index 0000000..306dcbe --- /dev/null +++ b/docs/guides/track-contributions.md @@ -0,0 +1,117 @@ +# Track contributions + +**Goal:** see how much new money you've added (or withdrawn) over a +period, separated from market movement -- and stop internal transfers +between your own accounts from being counted as contributions. + +**You'll need:** your portfolio under **git** version control, with +commits over time. `contributions` works by diffing two revisions of +your `portfolio.srf`, so it only sees money movement you've committed. + +## How it works + +Market gains change your portfolio's *value*; contributions change its +*shares and lots*. [`zfin contributions`](../reference/cli/contributions.md) +diffs your portfolio file between two git revisions, attributes the +share/lot changes to contributions vs. withdrawals, and ignores price +movement. + +This means the workflow is: **keep `portfolio.srf` in a git repo, and +commit it whenever you update it.** A natural cadence is a commit per +account update or per weekly review. + +> **Do this bookkeeping when the market is closed -- a weekend is +> ideal.** Prices and balances have settled, your brokerage statements +> are final, and zfin's cached closing prices won't shift under you +> mid-run, so the numbers are stable and reproducible. The same applies +> to [`audit`](audit-against-brokerage.md): reconciling against an +> export lines up cleanly when both sides reflect the same settled +> close, rather than a moving intraday price. + +## The default modes + +With no flags, the comparison depends on your working tree: + +```bash +zfin contributions +``` + +- **Clean working tree:** compares `HEAD~1` against `HEAD` -- i.e. + "what changed in my last commit." +- **Dirty working tree:** compares `HEAD` against the working copy -- + i.e. "what have I edited but not yet committed." + +Against a freshly-checked-out example (clean, nothing to diff) you'll +see: + +``` +Portfolio contributions report + Working tree clean — comparing HEAD~1 against HEAD + + No changes detected. +``` + +## Choosing a window + +Use `--since` (and optionally `--until`) to pick the endpoints. Dates +accept `YYYY-MM-DD` or relative shortcuts (`1W`, `1M`, `1Q`, `1Y`): + +```bash +zfin contributions --since 1Y # a year ago vs. now +zfin contributions --since 2025-01-01 --until 2025-12-31 +``` + +Each date resolves to the commit at or before it. When a review date +and its commit date diverge (you committed two days after your Sunday +review), pin commits directly with `--commit-before` / `--commit-after` +(which accept `HEAD`, `HEAD~N`, a SHA, or `working`). + +## Don't double-count transfers + +Moving money between two accounts you own isn't a contribution, but a +naive diff sees the receiving account gain lots and counts it as new +money. `transaction_log.srf` is how you tell zfin "this was a transfer, +not new money." + +Like the other sibling files, it's **optional and additive**: you only +need it if you move money between your own accounts and want clean +attribution. Without it nothing breaks -- those transfers just show up +as contributions, inflating the total by the amount moved. If you never +shuffle money between accounts, skip the file entirely. + +When you do, declare each move in +[`transaction_log.srf`](../reference/config/transaction-log-srf.md) so +it cancels out: + +```srf +#!srfv1 +transfer::2026-05-02,type::cash,amount:num:50000,from::Joint taxable,to::Pat Roth,dest_lot::cash +``` + +zfin matches the transfer against the diff and removes it from the +attribution total. Only `type::cash` is wired up today; `in_kind` +parses but isn't yet supported. + +## Cash that *is* a contribution + +By default, raw cash-balance increases are treated as internal noise +(interest, dividends, sweeps). For accounts whose cash movement is +dominated by real external deposits (payroll ESPP, direct 401k cash), +set `cash_is_contribution:bool:true` in +[`accounts.srf`](set-up-accounts.md#4-advanced-flags) so those increases +count. + +## Related: `compare` + +[`zfin compare`](../reference/cli/compare.md) shows the same +attribution alongside value and per-symbol price moves between two +dates. See [Snapshots and history](snapshots-and-history.md). + +## Next steps + +- [Snapshots and history](snapshots-and-history.md) -- record value over time. +- [`transaction_log.srf` reference](../reference/config/transaction-log-srf.md) + +--- + +[Previous: Read your portfolio](read-your-portfolio.md) | [Next: Snapshots and history](snapshots-and-history.md) | [Documentation home](../README.md) diff --git a/docs/images/fidelity-positions-download.png b/docs/images/fidelity-positions-download.png new file mode 100644 index 0000000000000000000000000000000000000000..5c2e632cf8708a40bb52d7565d16af2915b78d59 GIT binary patch literal 69243 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%0A693a7SgPh@%k+j3l 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 @?!}hw7i#C$I%NP7Brh66c<`Dp3)C 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 literal 0 HcmV?d00001 diff --git a/docs/reference/cli/analysis.md b/docs/reference/cli/analysis.md new file mode 100644 index 0000000..7a2d24b --- /dev/null +++ b/docs/reference/cli/analysis.md @@ -0,0 +1,51 @@ +# `zfin analysis` + +Break your portfolio down by asset class, sector, geography, account, +and tax type. + +``` +Usage: zfin analysis [FILE] +``` + +Renders a bar breakdown along five axes, plus an umbrella-exposure +estimate. The Asset Category / Sector / Geographic axes use +[`metadata.srf`](../config/metadata-srf.md); the Tax Type axis uses +[`accounts.srf`](../config/accounts-srf.md). Unclassified holdings and +unmapped accounts are shown as such. + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin analysis +``` + +``` + Asset Category + Equity ██████████████████████████▋ 89.2% $1,233,151.30 + Fixed Income ██ 7.0% $96,922.00 + Cash █▏ 3.8% $53,064.51 + + By Tax Type + Traditional (Pre-Tax) █████████████████▋ 58.9% $815,290.06 + Taxable ██████▍ 21.6% $299,010.60 + Roth (Post-Tax) █████ 16.9% $233,732.95 + HSA (Triple Tax-Free) ▊ 2.5% $35,104.20 + + Umbrella exposure + Total liquid: $1,383,137.81 + Shielded (retirement accounts): $1,084,127.21 + Exposed (taxable + non-shielded pre-tax): $299,010.60 (21.6%) +``` + +The umbrella block's shielded/exposed split can be overridden per +account with `shielded:bool:false` in `accounts.srf`. + +## See also + +- [Read your portfolio](../../guides/read-your-portfolio.md#analysis-allocation-breakdowns) +- [Classify your holdings](../../guides/classify-holdings.md) / [Map your accounts](../../guides/set-up-accounts.md) +- [`review`](review.md) / [`exposure`](exposure.md) + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/audit.md b/docs/reference/cli/audit.md new file mode 100644 index 0000000..4875e2c --- /dev/null +++ b/docs/reference/cli/audit.md @@ -0,0 +1,58 @@ +# `zfin audit` + +Two modes in one command: a portfolio hygiene check, and reconciliation +against a brokerage export. + +``` +Usage: zfin audit [opts] +``` + +**Flagless** runs the hygiene check -- stale manual prices, +accounts overdue for update, and auto-discovered brokerage-file +candidates. **With brokerage flags**, it reconciles your portfolio +against the export (treating the brokerage as source of truth) and +reports discrepancies. + +## Options + +| Flag | Effect | +|--------------------|----------------------------------------------------------------------------| +| `--verbose` | Show full reconciliation output even when clean. | +| `--stale-days ` | Manual-price staleness threshold (default 3). | +| `--fidelity ` | Fidelity positions CSV ("All accounts" -> Positions tab -> Download). | +| `--schwab ` | Schwab per-account positions CSV. | +| `--schwab-summary` | Schwab account summary: paste from the summary page to stdin, then Ctrl-D. | + +Reconciliation matches export accounts to yours via `institution::` and +`account_number::` in [`accounts.srf`](../config/accounts-srf.md); an +unmatched account is reported as "unmapped." + +## Example (hygiene check) + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin audit +``` + +``` + Portfolio hygiene + + Stale manual prices (>3 days — --stale-days to configure) + (none) + + Accounts overdue for update (weekly default — set update_cadence in accounts.srf) + Sam 401k weekly no update history found + Joint taxable weekly no update history found +``` + +> Brokerage exports contain real account numbers and holdings. Keep +> them out of any git repo and delete them after reconciling. + +## See also + +- [Audit against your brokerage](../../guides/audit-against-brokerage.md) -- the workflow. +- [`import`](import.md) -- build a portfolio file *from* an export instead. +- [`accounts.srf` reference](../config/accounts-srf.md) -- `update_cadence`, `institution`, `account_number`. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/cache.md b/docs/reference/cli/cache.md new file mode 100644 index 0000000..56b7ce9 --- /dev/null +++ b/docs/reference/cli/cache.md @@ -0,0 +1,35 @@ +# `zfin cache` + +Inspect or clear the local provider-data cache. + +``` +Usage: zfin cache +``` + +| Subcommand | Does | +|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `stats` | List every cached symbol with per-data-type size, age, and freshness state. Stale entries (past TTL) are flagged. Includes `cusip_tickers.srf` if present. | +| `clear` | Delete every file under the cache directory. No confirmation; the next provider call re-fetches everything. | + +The cache directory is `$ZFIN_CACHE_DIR` if set, otherwise +`~/.cache/zfin`. + +## Examples + +```bash +zfin cache stats +zfin cache clear # wipe; everything re-fetches on next use +``` + +`clear` is safe -- it only removes cached copies of public market data. +Reach for it when you suspect corrupt cached data; routine staleness is +handled automatically by the `auto` refresh policy. + +## See also + +- [Caching and data freshness](../../explanation/caching.md) -- TTLs and the fetch model. +- [Offline use and refreshing data](../../guides/offline-and-refresh.md) -- the `--refresh-data` flag. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/compare.md b/docs/reference/cli/compare.md new file mode 100644 index 0000000..9fcb39c --- /dev/null +++ b/docs/reference/cli/compare.md @@ -0,0 +1,50 @@ +# `zfin compare` + +Compare your portfolio at two points in time: liquid totals, +per-symbol price moves, and contribution attribution. + +``` +Usage: + zfin compare # compare DATE vs. live portfolio + zfin compare # compare two historical dates +``` + +Arguments can be given in any order; output always reads older -> +newer. Dates accept `YYYY-MM-DD` or relative shortcuts +(`1W`/`1M`/`1Q`/`1Y`). Historical dates resolve against your +`history/*-portfolio.srf` snapshots; on a missing date, `compare` +prints the nearest available dates and exits rather than snapping +silently. + +## Options + +| Flag | Effect | +|--------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `--projections` | Add projected-return and 99% safe-withdrawal deltas (adds ~1-2s per endpoint). | +| `--no-events` | With `--projections`, exclude life events. | +| `--snapshot-before ` / `--snapshot-after ` | Override a side's snapshot (`--snapshot-after live` for the current portfolio). | +| `--commit-before ` / `--commit-after ` | Pin the git commit for the attribution block (`HEAD`, `HEAD~N`, SHA, or `working`). Useful when a review date and its commit diverge. | + +## Example + +```bash +ZFIN_HOME=examples/post-retirement zfin compare 2024-04-01 2025-04-01 +``` + +``` +Portfolio comparison: 2024-04-01 → 2025-04-01 (365 days) + +Liquid: $2,350,000.00 → $2,580,000.00 +$230,000.00 +9.79% +``` + +With symbols held on both dates, a per-symbol price-change table +appears, sorted by percentage move. + +## See also + +- [Snapshots and history](../../guides/snapshots-and-history.md) +- [`history`](history.md) -- the full timeline. [`contributions`](contributions.md) -- the attribution detail. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/contributions.md b/docs/reference/cli/contributions.md new file mode 100644 index 0000000..5019852 --- /dev/null +++ b/docs/reference/cli/contributions.md @@ -0,0 +1,55 @@ +# `zfin contributions` + +Show contributions, withdrawals, and lot-level changes between two +points in your portfolio's **git history**. + +``` +Usage: zfin contributions [opts] +``` + +`contributions` diffs two git revisions of your `portfolio.srf` and +attributes the share/lot changes to new money vs. market movement. Your +portfolio must be under git with commits over time. + +## Modes + +| Invocation | Window | +|-----------------------------|-----------------------------------------------------------| +| (no flags), dirty tree | `HEAD` vs. working copy | +| (no flags), clean tree | `HEAD~1` vs. `HEAD` (review the last commit) | +| `--since ` | commit at/before DATE vs. HEAD (or working copy if dirty) | +| `--since --until ` | commit at/before D1 vs. commit at/before D2 | + +`--until` alone is rejected (the window is ambiguous). Dates accept +`YYYY-MM-DD` or `1W`/`1M`/`1Q`/`1Y`. + +## Options + +| Flag | Effect | +|--------------------------|-------------------------------------------------------------------------------------| +| `--since ` | Earliest side (resolves to commit at/before). | +| `--until ` | Latest side (pair with `--since`). | +| `--commit-before ` | Pin the before commit directly (same grammar as `--commit-after`, minus `working`). | +| `--commit-after ` | Pin the after commit: `YYYY-MM-DD`, relative, `HEAD`, `HEAD~N`, SHA, or `working`. | + +Pass at most one of `--since`/`--commit-before` (same axis), and at +most one of `--until`/`--commit-after`. + +## Example + +```bash +zfin contributions --since 1Y +``` + +Internal transfers between your own accounts are excluded from the +attribution total when declared in +[`transaction_log.srf`](../config/transaction-log-srf.md). + +## See also + +- [Track contributions](../../guides/track-contributions.md) -- the full workflow. +- [`compare`](compare.md) -- attribution alongside value and price moves. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/divs.md b/docs/reference/cli/divs.md new file mode 100644 index 0000000..c84a83a --- /dev/null +++ b/docs/reference/cli/divs.md @@ -0,0 +1,37 @@ +# `zfin divs` + +Show dividend history for a symbol, with trailing-twelve-month yield. + +``` +Usage: zfin divs +``` + +Lists each dividend's ex-date, amount, pay date, and type (regular, +special, etc.). Dividend data comes from Polygon (`POLYGON_API_KEY`), +merged with per-row dividend data from Tiingo's price series. + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin divs SCHD +``` + +``` +Dividend History for SCHD +======================================== + Ex-Date Amount Pay Date Type +------------ ---------- ------------ ---------- +2026-03-25 0.2569 2026-03-30 regular +2025-12-10 0.2782 2025-12-15 regular +2025-09-24 0.2604 2025-09-29 regular +... +``` + +## See also + +- [`splits`](splits.md) -- corporate splits for a symbol. +- [`perf`](perf.md) -- total return, which includes reinvested dividends. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/doctor.md b/docs/reference/cli/doctor.md new file mode 100644 index 0000000..3d29347 --- /dev/null +++ b/docs/reference/cli/doctor.md @@ -0,0 +1,102 @@ +# `zfin doctor` + +Health-check your zfin setup -- files and environment -- without +changing anything. + +``` +Usage: zfin doctor +``` + +`doctor` answers "is my setup sane?" It is **read-only**: no provider +fetches, no cache writes, no portfolio changes. The only network call +is an optional `GET {ZFIN_SERVER}/help` to confirm the server is +reachable, so it's safe to run in CI or cron. + +## What it checks + +Four sections, each line tagged `OK` / `INFO` / `WARN` / `FAIL`: + +- **Files** -- every config file: is it present, where was it resolved + from, and does it parse? +- **Cross-checks** -- do `accounts.srf` / `metadata.srf` / + `transaction_log.srf` reference entries that actually exist (e.g. + every account used by a lot has an `accounts.srf` entry)? +- **Environment** -- cache size, staleness of the hand-maintained data + tables, and `ZFIN_SERVER` reachability and version. +- **Capabilities** -- which API keys are set and what each enables (or + what you give up without it). + +## Exit code + +`0` unless a file that **exists** fails to parse (a `FAIL`), in which +case it exits `1`. Missing optional files, cross-reference gaps, stale +data, an unreachable server, and absent API keys are all non-fatal +(`INFO` / `WARN`) -- so a clean install with only `portfolio.srf` still +exits `0`. + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin doctor +``` + +``` +zfin doctor + +Files + [OK ] examples/pre-retirement-both/portfolio.srf: 20 lots + [OK ] accounts.srf: examples/pre-retirement-both/accounts.srf + [OK ] metadata.srf: examples/pre-retirement-both/metadata.srf + [INFO] transaction_log.srf: not present + [OK ] projections.srf: examples/pre-retirement-both/projections.srf + [INFO] history/imported_values.srf: not present + [INFO] history/ snapshots: no history/ directory + [INFO] keys.srf: not present; using built-in defaults + [INFO] theme.srf: not present; using built-in defaults + +Cross-checks + [OK ] accounts.srf coverage: all referenced entries present + [OK ] metadata.srf coverage: all referenced entries present + [INFO] transaction_log.srf references: skipped (transaction_log.srf not loaded) + +Environment + [OK ] Cache: 42 symbols, 168 files, 23.4 MB (~/.cache/zfin) + [OK ] T-bill risk-free rate table: current + [OK ] Shiller annual returns (ie_data.csv): current + [OK ] Review tab MaxDD color thresholds: current + [OK ] Observation engine thresholds: current + [OK ] ZFIN_SERVER: reachable: zfin-server abc1234 (https://zfin.example.com) + +Capabilities + [OK ] TIINGO_API_KEY: daily candles + [OK ] POLYGON_API_KEY: dividend/split history + dividend-reinvested total return + [OK ] FMP_API_KEY: earnings history and estimates + [OK ] TWELVEDATA_API_KEY: quote fallback after Yahoo + [OK ] ZFIN_USER_EMAIL: ETF profiles and `enrich` + [OK ] OPENFIGI_API_KEY: faster CUSIP lookups (higher rate limit) + [OK ] Quotes (Yahoo): always available, no key required + [OK ] Options (CBOE): always available, no key required + +Summary: 20 OK, 0 warning(s), 0 failure(s) +``` + +A key you haven't set is not an error -- it shows as `INFO` with what +you give up, for example: + +``` + [INFO] POLYGON_API_KEY: price-only returns; no dividend/split history + [INFO] FMP_API_KEY: no earnings data +``` + +With `ZFIN_SERVER` unset, the Environment section shows +`[INFO] ZFIN_SERVER: not set (provider fetch only; no server sync)`. + +## See also + +- [Environment variables](../config/environment.md) -- the keys and paths doctor inspects. +- [Data providers and API keys](../providers.md) -- where to get each key. +- [Caching and data freshness](../../explanation/caching.md) -- the cache and the `ZFIN_SERVER` tier doctor reports on. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/earnings.md b/docs/reference/cli/earnings.md new file mode 100644 index 0000000..6e2b7a4 --- /dev/null +++ b/docs/reference/cli/earnings.md @@ -0,0 +1,45 @@ +# `zfin earnings` + +Show earnings history (with EPS surprise) and upcoming events for a +symbol. + +``` +Usage: zfin earnings +``` + +Lists each quarter's report date, EPS estimate, actual EPS, and the +surprise (absolute and percent), plus upcoming scheduled dates. Data +comes from FMP (`FMP_API_KEY`). + +ETFs, mutual funds, CUSIPs, and some dual-class shares (e.g. BRK.B) +have no earnings on FMP's free tier and show as "no earnings data" -- +an expected limitation, not a bug. + +## Example + +```bash +zfin earnings AAPL +``` + +``` +Earnings History for AAPL +======================================== + Date Q EPS Est EPS Act Surprise Surprise % +------------ ---- ------------ ------------ ------------ ---------- + 2026-07-30 Q2 $1.75 -- -- -- + 2026-04-30 Q1 $1.95 $2.01 +$0.0600 +3.1% + 2026-01-29 Q4 $2.67 $2.85 +$0.1800 +6.7% + ... +``` + +A row with `--` in the actual column is a scheduled, not-yet-reported +quarter. + +## See also + +- [Why multiple data providers](../../explanation/data-providers.md) -- earnings coverage limits. +- [`etf`](etf.md) -- the profile view for funds (which have no earnings). + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/enrich.md b/docs/reference/cli/enrich.md new file mode 100644 index 0000000..eb72f9e --- /dev/null +++ b/docs/reference/cli/enrich.md @@ -0,0 +1,48 @@ +# `zfin enrich` + +Bootstrap a [`metadata.srf`](../config/metadata-srf.md) classification +file from public Wikidata + SEC EDGAR data. + +``` +Usage: zfin enrich [SYMBOL] +``` + +Requires `ZFIN_USER_EMAIL` (SEC EDGAR needs a contact address). Two +modes: + +- **Portfolio mode** (no argument) -- classify every stock symbol in + your portfolio and write a complete SRF file to stdout. Honors the + global `-p` flag for file selection. +- **Symbol mode** (one `SYMBOL`) -- emit one appendable line for a + single symbol. + +## Sources + +- **Wikidata SPARQL** -- sector / industry / country / asset class, + plus a CIK lookup for the EDGAR call. +- **EDGAR XBRL company facts** -- shares outstanding, combined with the + latest cached close to derive market-cap size buckets for US stocks. +- **EDGAR mutual-fund ticker map** -- fallback when Wikidata has no + entry (open-end funds aren't exchange-listed); fills in + `geo::US,asset_class::Fund`. + +## Examples + +```bash +zfin enrich > metadata.srf # whole portfolio (default file) +zfin -p sample enrich > metadata.srf # whole portfolio (named file) +zfin enrich AAPL >> metadata.srf # append a single symbol +zfin enrich fagix >> metadata.srf # symbol is auto-uppercased +``` + +Always review the output before saving -- symbols that miss both +sources come through as `TODO` lines to complete by hand. + +## See also + +- [Classify your holdings](../../guides/classify-holdings.md) -- the workflow. +- [`metadata.srf` reference](../config/metadata-srf.md) -- the file this produces. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/etf.md b/docs/reference/cli/etf.md new file mode 100644 index 0000000..2974999 --- /dev/null +++ b/docs/reference/cli/etf.md @@ -0,0 +1,47 @@ +# `zfin etf` + +Show an ETF's profile: net assets, holdings count, sector allocation, +and top holdings. + +``` +Usage: zfin etf +``` + +Profile data comes from SEC EDGAR's NPORT-P filings, so it requires +`ZFIN_USER_EMAIL` (EDGAR mandates a contact address in the request). + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin etf SPY +``` + +``` +ETF Profile: SPY +======================================== + Net Assets: $651.6B + Total Holdings: 20 + + Sector Allocation: + 98.1% Equity / Corporate + 1.9% Equity / Other + + Top Holdings: + Symbol Weight Name + -- 7.58% NVIDIA Corp + -- 6.66% Apple Inc + -- 4.91% Microsoft Corp + ... +``` + +The same NPORT-P holdings power [`exposure`](exposure.md)'s +look-through analysis. + +## See also + +- [`exposure`](exposure.md) -- your true exposure to a symbol through ETFs. +- [`enrich`](enrich.md) -- uses EDGAR data to classify holdings. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/exposure.md b/docs/reference/cli/exposure.md new file mode 100644 index 0000000..81e1b8e --- /dev/null +++ b/docs/reference/cli/exposure.md @@ -0,0 +1,44 @@ +# `zfin exposure` + +Show how much of a single underlying symbol you really hold -- directly +plus look-through via the top holdings of every ETF in your portfolio. + +``` +Usage: zfin exposure +``` + +A fund worth $V that holds the symbol at weight w contributes V*w of +exposure. ETF holdings are matched to the symbol by ticker (from the +NPORT-P filing) or by resolving the holding's CUSIP to a ticker +(local cache -> `ZFIN_SERVER` -> OpenFIGI). Holdings with no ticker and +no resolvable CUSIP (bonds, derivatives) are excluded. + +ETF profiles come from SEC EDGAR (cached ~90 days), so the first run on +a cold cache fetches them and can take ~15s. Requires `ZFIN_USER_EMAIL`. + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin exposure SPY +``` + +``` +Exposure to SPY (examples/pre-retirement-both/portfolio.srf) +======================================== + Total exposure 17.3% $238,956.80 + Direct 17.3% $238,956.80 + Look-through 0.0% $0.00 +``` + +This is most useful for an individual stock you also hold inside broad +ETFs -- e.g. `zfin exposure NVDA` to see your true concentration across +VTI, SPY, and QQQ. + +## See also + +- [Read your portfolio](../../guides/read-your-portfolio.md#exposure-look-through-to-a-single-symbol) +- [`etf`](etf.md) -- the holdings that drive look-through. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/history.md b/docs/reference/cli/history.md new file mode 100644 index 0000000..d0ac6b8 --- /dev/null +++ b/docs/reference/cli/history.md @@ -0,0 +1,57 @@ +# `zfin history` + +Two modes in one command, selected by whether you pass a symbol. + +``` +Usage: + zfin history # last 30 days of candles + zfin history [flags] # portfolio-value timeline +``` + +## Symbol mode + +With a positional symbol, shows the last 30 trading days of OHLCV +candles: + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin history VTI +``` + +``` +Price History for VTI (last 30 days) +======================================== + Date Open High Low Close Volume +2026-06-04 370.62 373.99 370.36 373.38 3,111,996 +... +``` + +## Portfolio mode + +With no symbol, reads your `history/*-portfolio.srf` snapshots and +renders rolling-window returns, a braille chart, and a recent-snapshots +table. (Build that history with [`snapshot`](snapshot.md).) + +| Flag | Effect | +|-----------------------|-------------------------------------------------| +| `--since ` | Earliest as-of date (inclusive). | +| `--until ` | Latest as-of date (inclusive). | +| `--metric ` | `liquid` (default), `illiquid`, or `net_worth`. | +| `--resolution ` | `daily` \| `weekly` \| `monthly` \| `auto`. | +| `--limit ` | Cap the recent-snapshots table (default 40). | +| `--rebuild-rollup` | Regenerate `history/rollup.srf` and exit. | + +Dates accept `YYYY-MM-DD` or relative shortcuts (`1W`/`1M`/`1Q`/`1Y`). + +```bash +ZFIN_HOME=examples/post-retirement zfin history --metric net_worth --since 1Y +``` + +## See also + +- [Snapshots and history](../../guides/snapshots-and-history.md) -- the workflow. +- [`compare`](compare.md) -- diff two specific dates. +- [`snapshot`](snapshot.md) -- record the data this reads. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/import.md b/docs/reference/cli/import.md new file mode 100644 index 0000000..507a090 --- /dev/null +++ b/docs/reference/cli/import.md @@ -0,0 +1,51 @@ +# `zfin import` + +Synthesize a portfolio file from a brokerage positions export. Designed +for managed accounts (direct-indexing baskets, accounts you don't track +at lot granularity). + +``` +Usage: zfin -p import (--fidelity FILE | --schwab FILE | --wells-fargo FILE [--account NAME]) [-y] +``` + +Each run **replaces** the target portfolio file with synthetic lots -- +one per (account, symbol) -- drawn from the export. Per-buy history is +lost; git serves as the file-level history. + +**Re-import merge:** when the target already exists, lots still present +in the new export keep their prior `open_date`, `open_price`, and +`note::`, so trailing-return and ST/LT classifications stay stable and +`git diff` flags only genuine brokerage changes. New positions get an +`open_date::1970-01-01` sentinel; disappeared positions are dropped. +Hand-edited fields (`price::`, `ticker::`) are **not** preserved. + +## Options + +| Flag | Effect | +|--------------------------|--------------------------------------------------------------------------------------| +| `-p, --portfolio ` | Target file (a single concrete path, not a glob). **Required.** | +| `--fidelity ` | Fidelity positions CSV. | +| `--schwab ` | Schwab per-account positions CSV. | +| `--wells-fargo ` | Wells Fargo positions paste (`-` for stdin). | +| `--account ` | (Wells Fargo only) account to attribute lots to; must match an `accounts.srf` entry. | +| `-y, --yes` | Don't prompt before overwriting an existing file. | + +Account resolution needs an [`accounts.srf`](../config/accounts-srf.md) +next to the target with `institution::` + `account_number::` entries +matching the export; import refuses to write when an export account is +unmapped. + +## Example + +```bash +zfin -p portfolio_managed.srf import --fidelity ~/Downloads/Portfolio_Positions.csv +``` + +## See also + +- [`audit`](audit.md) -- reconcile an existing portfolio against an export instead of replacing it. +- [Map your accounts](../../guides/set-up-accounts.md) -- the institution/number mapping import needs. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md new file mode 100644 index 0000000..d1fb705 --- /dev/null +++ b/docs/reference/cli/index.md @@ -0,0 +1,88 @@ +# CLI command reference + +Every zfin subcommand, grouped the way [`zfin help`](version.md) groups +them. Each command has its own page with flags and sample output. For a +task-oriented path through these, see the +[user manual](../../README.md#the-user-manual-workflows). + +``` +zfin [global options] [command options] +``` + +Get help at any time with `zfin help` or per command with +`zfin --help`. + +## Per-symbol lookups + +| Command | Does | +|---------------------------|----------------------------------------------------| +| [`perf`](perf.md) | 1y/3y/5y/10y trailing returns (Morningstar-style) | +| [`quote`](quote.md) | Latest quote with a chart and 20-day history | +| [`history`](history.md) | Price history (symbol) or portfolio-value timeline | +| [`divs`](divs.md) | Dividend history with TTM yield | +| [`splits`](splits.md) | Split history | +| [`options`](options.md) | Options chain (all expirations) | +| [`earnings`](earnings.md) | Earnings history with EPS surprise, plus upcoming | +| [`etf`](etf.md) | ETF profile: holdings, sectors, AUM, inception | + +## Portfolio analysis + +| Command | Does | +|---------------------------------|-----------------------------------------------------------| +| [`portfolio`](portfolio.md) | Positions, valuations, and watchlist | +| [`analysis`](analysis.md) | Breakdowns by asset class, sector, geo, account, tax type | +| [`exposure`](exposure.md) | True exposure to a symbol (direct + look-through) | +| [`review`](review.md) | Per-holding performance and risk dashboard | +| [`projections`](projections.md) | Retirement projections and percentile bands | +| [`milestones`](milestones.md) | Portfolio threshold crossings ($1M, doublings, ...) | + +## Time-series and journaling + +| Command | Does | +|-------------------------------------|-------------------------------------------------| +| [`snapshot`](snapshot.md) | Write a portfolio snapshot to `history/` | +| [`compare`](compare.md) | Compare the portfolio at two points in time | +| [`contributions`](contributions.md) | Money added/withdrawn between two git revisions | + +## Data hygiene + +| Command | Does | +|-----------------------|-----------------------------------------------------| +| [`audit`](audit.md) | Reconcile against brokerage exports + hygiene check | +| [`enrich`](enrich.md) | Bootstrap `metadata.srf` from Wikidata + EDGAR | +| [`import`](import.md) | Synthesize a portfolio file from a brokerage export | +| [`lookup`](lookup.md) | Resolve a CUSIP to a ticker via OpenFIGI | + +## Infrastructure + +| Command | Does | +|---------------------------------|------------------------------------------------| +| [`cache`](cache.md) | Inspect or clear the local data cache | +| [`doctor`](doctor.md) | Health-check files and environment (read-only) | +| [`version`](version.md) | Show version and build info | +| [`interactive`](interactive.md) | Launch the interactive TUI (alias `i`) | + +## Global options + +These apply to every command and **must appear before** the +subcommand: + +| Option | Effect | +|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--no-color` | Disable colored output (also respects `NO_COLOR`). | +| `--refresh-data=` | Cache freshness policy. `auto` (default) respects TTLs; `force` re-fetches everything; `never` is offline. See [offline guide](../../guides/offline-and-refresh.md). | +| `-p, --portfolio ` | Portfolio file or glob (repeatable; default `portfolio*.srf`). Resolved against `ZFIN_HOME` when set, else the current directory. Quote globs to prevent shell expansion. | +| `-w, --watchlist ` | Watchlist file (default `watchlist.srf`). | + +```bash +zfin --no-color --refresh-data=never -p 'portfolio_*.srf' analysis +``` + +`metadata.srf` and `accounts.srf` load from the same directory as the +first resolved portfolio file. See +[environment variables](../config/environment.md) for `ZFIN_HOME` and +related settings. + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/cli/interactive.md b/docs/reference/cli/interactive.md new file mode 100644 index 0000000..974e8bb --- /dev/null +++ b/docs/reference/cli/interactive.md @@ -0,0 +1,43 @@ +# `zfin interactive` + +Launch the interactive, multi-tab terminal UI. + +``` +Usage: zfin interactive [options] +Alias: zfin i [options] +``` + +The TUI is a vaxis-rendered interface for browsing your portfolio, +per-symbol data, options chains, earnings, and projections. Press `?` +inside it for the keybinding overlay, and `q` (or Ctrl-C) to quit. With +no portfolio or symbol given, it auto-loads `portfolio.srf` and opens on +the Portfolio tab. + +## Options + +| Flag | Effect | +|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| `-s, --symbol ` | Pre-load a symbol and open on the Quote tab. | +| `--chart ` | Chart graphics: `auto`, `braille`, or `WxH` (e.g. `80x24`). `auto` uses Kitty graphics if the terminal supports it, else braille. | +| `--default-keys` | Print the default keybindings as a `keys.srf` template and exit. | +| `--default-theme` | Print the default theme as a `theme.srf` template and exit. | + +The global flags (`--no-color`, `-p`, `-w`, `--refresh-data=`) +are honored. + +## Examples + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin i +zfin i -s AAPL # open on a symbol, no portfolio +zfin i --default-keys > ~/.config/zfin/keys.srf # generate a keybinding template +``` + +## See also + +- [The interactive TUI](../tui.md) -- the full tab-by-tab tour. +- [Customize the TUI](../../guides/customize-the-tui.md) -- `keys.srf` and `theme.srf`. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/lookup.md b/docs/reference/cli/lookup.md new file mode 100644 index 0000000..c364d54 --- /dev/null +++ b/docs/reference/cli/lookup.md @@ -0,0 +1,36 @@ +# `zfin lookup` + +Resolve a CUSIP (9-character security identifier) to its ticker via the +OpenFIGI API. + +``` +Usage: zfin lookup +``` + +Successful results are cached indefinitely in `cusip_tickers.srf`. +`OPENFIGI_API_KEY` raises the rate limit, but the unauthenticated tier +works for low volume. Mutual funds frequently have no OpenFIGI +coverage; the command says so and suggests a manual portfolio entry. + +## Example + +```bash +zfin lookup 037833100 +``` + +``` +037833100 → AAPL +``` + +Use this when a holding in your portfolio is identified by CUSIP (e.g. +a 401k share class) and you need its ticker for a `ticker::` alias. See +[ticker aliases](../config/portfolio-srf.md#ticker-aliases-and-cusips). + +## See also + +- [`portfolio.srf` reference](../config/portfolio-srf.md) -- using `ticker::` aliases. +- [Environment variables](../config/environment.md) -- `OPENFIGI_API_KEY`. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/milestones.md b/docs/reference/cli/milestones.md new file mode 100644 index 0000000..7dcea10 --- /dev/null +++ b/docs/reference/cli/milestones.md @@ -0,0 +1,52 @@ +# `zfin milestones` + +Find the dates your portfolio first reached each of a series of value +thresholds. + +``` +Usage: zfin milestones --step [--real] +``` + +Reads your snapshot/value history and reports the first date each +threshold was crossed. Two threshold modes: + +- **Absolute dollar:** `1M` / `1m` / `1500000` / `1.5M` / `500K` +- **Relative multiplier:** `2x` / `1.5x` (each multiple of the starting value) + +Rejects `%`, non-positive dollar steps, multipliers <= 1.0, and +NaN/Inf. + +## Options + +| Flag | Effect | +|-----------------|---------------------------------------------------------------------------------| +| `--step ` | Threshold step (**required**). | +| `--real` | Deflate to the last full Shiller year first (CPI-adjusted). Default is nominal. | + +Crossing dates are "first observed at," bounded by the source series +cadence (typically weekly), so they're approximate to within a week. + +## Example + +```bash +ZFIN_HOME=examples/post-retirement zfin milestones --step 250K +``` + +``` +Milestones — step $250,000.00 (nominal) + + Milestone Date Crossed Days Since Prev Days Since First + $1,750,000.00 2018-09-30 — 1001 days + $2,000,000.00 2021-02-14 868 days 1869 days + $2,250,000.00 2023-04-09 784 days 2653 days + $2,500,000.00 2025-04-01 723 days 3376 days +``` + +## See also + +- [Snapshots and history](../../guides/snapshots-and-history.md) -- builds the series this reads. +- [`history`](history.md) -- the full value timeline. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/options.md b/docs/reference/cli/options.md new file mode 100644 index 0000000..404ecda --- /dev/null +++ b/docs/reference/cli/options.md @@ -0,0 +1,62 @@ +# `zfin options` + +Show the options chain (all expirations) for a symbol. + +``` +Usage: zfin options [--ntm ] +``` + +Lists every expiration with its call/put counts and auto-expands the +nearest monthly expiration into strike tables near the money (last/bid/ +ask, volume, open interest, and implied volatility). The other +expirations stay collapsed to their counts. Data comes from CBOE -- no +API key required, cached one hour, 15-minute delayed during market +hours. + +## Options + +| Flag | Default | Effect | +|-------------|---------|-----------------------------------------------------------------| +| `--ntm ` | `8` | Show ±N strikes near the money on the auto-expanded expiration. | + +`NTM` = near the money. `--ntm` only widens or narrows the strike band +on the one auto-expanded expiration; it does not expand the others. + +## Example + +```bash +zfin options AAPL --ntm 12 +``` + +``` +Options Chain for AAPL +======================================== +Underlying: $298.62 26 expiration(s) +/- 12 strikes NTM + +2026-06-18 (108 calls, 108 puts) +... +2026-07-17 (66 calls, 66 puts) [monthly] + CALLS + Strike Last Bid Ask Volume OI IV + ... +``` + +`[monthly]` marks standard monthly expirations; the nearest one is the +table that gets expanded. + +## Filtering to calls or puts + +The CLI has no calls-only / puts-only flag. For that -- plus expanding +*any* expiration and changing the strike band on the fly -- use the +**Options tab** in the [interactive TUI](../tui.md): `c` and `p` toggle +all calls / all puts, and `Ctrl-1` through `Ctrl-9` set the ±N +near-the-money band. + +## See also + +- [`quote`](quote.md) -- the underlying's spot price. +- [`portfolio`](portfolio.md) -- option *positions* you hold (`security_type::option`). + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/perf.md b/docs/reference/cli/perf.md new file mode 100644 index 0000000..bd4700d --- /dev/null +++ b/docs/reference/cli/perf.md @@ -0,0 +1,52 @@ +# `zfin perf` + +Show Morningstar-style trailing returns for a symbol. + +``` +Usage: zfin perf +``` + +`perf` prints 1Y/3Y/5Y/10Y **price-only** and **total-return** CAGR +plus risk metrics (Sharpe, max drawdown, volatility), in two tables: +an **as-of** table (through the latest cached close) and a +**month-end** table (through the most recent calendar month-end, the +way funds quote their stats). Total returns require `POLYGON_API_KEY` +for dividend history; price-only returns work without it. + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin perf VTI +``` + +``` +Trailing Returns for VTI +======================================== +Data points: 6290 (2001-05-31 to 2026-06-04) +Latest close: $373.38 + +As-of 2026-06-04: + Price Only Total Return +---------------------- -------------- -------------- + 1-Year Return: 27.29% 28.79% + 3-Year Return: 20.75% 22.22% ann. + 5-Year Return: 11.22% 12.80% ann. + 10-Year Return: 13.19% 15.10% ann. + +Month-end (2026-05-31): + ... (same shape, through last month-end) + +Risk Metrics (monthly returns): + Volatility Sharpe Max DD + 1-Year: 12.7% 1.65 5.5% +``` + +## See also + +- [Returns and performance](../../explanation/returns-and-performance.md) -- what the columns mean. +- [`review`](review.md) -- the same metrics for every holding at once. +- [`quote`](quote.md) -- the latest price with a short chart. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/portfolio.md b/docs/reference/cli/portfolio.md new file mode 100644 index 0000000..e3e5b56 --- /dev/null +++ b/docs/reference/cli/portfolio.md @@ -0,0 +1,49 @@ +# `zfin portfolio` + +Load and analyze your portfolio: positions, valuations, cash, and +watchlist. + +``` +Usage: zfin portfolio [FILE] +``` + +Reads `portfolio.srf` (or the `-p` pattern / `[FILE]` argument) and +prints a summary: total value, cost, and gain/loss; trailing historical +returns; a per-position table with lot detail; and a cash-by-account +section. Watchlist symbols, if any, appear at the bottom. + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin portfolio +``` + +``` +Portfolio Summary (examples/pre-retirement-both/portfolio.srf) +======================================== + Value: $1,383,137.81 Cost: $658,837.01 Gain/Loss: +$724,300.80 (109.9%) + Lots: 13 open, 0 closed Positions: 5 symbols + Historical: 1M: +3.2% 3M: +13.3% 1Y: +24.5% 3Y: +56.4% 5Y: +56.1% 10Y: +182.6% + + Symbol Shares Avg Cost Price Market Value Gain/Loss Weight ... + VTI 2480.0 $138.35 $373.38 $925,982.40 + $582,874.40 66.9% + open 1100.0 $140.00 $410,718.00 + $256,718.00 2018-06-15 LT Pat 401k + ... + + Cash + Account Balance Note + Joint taxable $48,000.00 + ... +``` + +Manual-priced rows render in warning color (the price may be stale). + +## See also + +- [Read your portfolio](../../guides/read-your-portfolio.md) -- how to interpret this. +- [`portfolio.srf` reference](../config/portfolio-srf.md) -- the input file. +- [`analysis`](analysis.md) -- allocation breakdowns. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/projections.md b/docs/reference/cli/projections.md new file mode 100644 index 0000000..357d9d2 --- /dev/null +++ b/docs/reference/cli/projections.md @@ -0,0 +1,64 @@ +# `zfin projections` + +Retirement projections: percentile bands of your portfolio's projected +value, a benchmark comparison, and safe-withdrawal dollars at multiple +horizons and confidence levels. + +``` +Usage: zfin projections [opts] +``` + +Configuration lives in +[`projections.srf`](../config/projections-srf.md). What the output +shows (accumulation block, earliest-retirement grid, or drawdown-only) +depends on which inputs you set -- see +[Plan for retirement](../../guides/plan-retirement.md) and +[the model](../../explanation/projections-model.md). + +## Sub-modes (mutually exclusive) + +| Flag | View | +|---------------------|-----------------------------------------------------------------------| +| (default) | Percentile bands + benchmark + safe withdrawal. | +| `--vs ` | Side-by-side with a historical snapshot's projection. | +| `--convergence` | The model's predicted retirement date over time. | +| `--return-backtest` | Expected-return claim vs. realized forward CAGR (pair with `--real`). | + +## Options + +| Flag | Effect | +|-------------------------|-----------------------------------------------------------------------------------------| +| `--as-of ` | Project against a historical snapshot (auto-snaps to nearest earlier). | +| `--overlay-actuals` | Plot your realized trajectory from `--as-of` to today on the bands. Requires `--as-of`. | +| `--no-events` | Exclude life events (baseline view). | +| `--real` | With `--return-backtest`, render CPI-adjusted. | +| `--export-chart ` | Render the band chart to a 1920x1080 PNG and exit (default mode only). | + +Dates accept `YYYY-MM-DD` or relative shortcuts (`1W`/`1M`/`1Q`/`1Y`). + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin projections +``` + +``` +Accumulation phase: + Years until possible retirement: 19 (2046-04-12, ages 65/62) + Median portfolio at retirement: $7,871,732.10 + Range (10th–90th percentile): $5,807,693.45 to $18,240,675.15 + +Safe Withdrawal (FIRECalc historical simulation) + 25 Year 35 Year 50 Year +99% safe withdrawal $314,920 $293,374 $264,002 +``` + +## See also + +- [Plan for retirement](../../guides/plan-retirement.md) -- the guided walkthrough. +- [`projections.srf` reference](../config/projections-srf.md) -- every input field. +- [The retirement projection model](../../explanation/projections-model.md) -- the method and caveats. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/quote.md b/docs/reference/cli/quote.md new file mode 100644 index 0000000..5b8a17a --- /dev/null +++ b/docs/reference/cli/quote.md @@ -0,0 +1,46 @@ +# `zfin quote` + +Show the latest quote for a symbol, with a price chart and recent +history. + +``` +Usage: zfin quote +``` + +Prints the last price, the day's open/high/low, volume, and the +day-over-day change, followed by a 20-day chart. Quotes come from Yahoo +(TwelveData fallback) and are **never cached** -- so this command needs +network access and does nothing useful in `--refresh-data=never` mode. + +Supports `--export-chart ` to render the chart as a 1920x1080 +PNG instead of text (see [export charts](../../guides/offline-and-refresh.md) +and the projections page). + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin quote SPY +``` + +``` +SPY $746.74 (close) +======================================== + Date: 2026-06-18 + Open: $747.76 + High: $748.23 + Low: $743.86 + Volume: 80,875,657 + Change: +$5.78 (+0.78%) + + ... (20-day braille chart) +``` + +## See also + +- [`perf`](perf.md) -- trailing returns instead of a spot price. +- [`history`](history.md) -- the last 30 days as a table. +- [Caching](../../explanation/caching.md#quotes-are-never-cached) -- why quotes need the network. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/review.md b/docs/reference/cli/review.md new file mode 100644 index 0000000..50f923b --- /dev/null +++ b/docs/reference/cli/review.md @@ -0,0 +1,52 @@ +# `zfin review` + +A per-holding performance and risk dashboard, with automated findings. + +``` +Usage: zfin review [opts] +``` + +One row per holding: sector, tax status, trailing returns +(1Y/3Y/5Y/10Y month-end total return), risk metrics (3Y+10Y +volatility/Sharpe, 5Y max drawdown), and a correlation-aware totals +row. A findings section flags concentration, sector dominance, +volatility outliers, and tiny positions. Reads +[`metadata.srf`](../config/metadata-srf.md) and +[`accounts.srf`](../config/accounts-srf.md). + +## Options + +| Flag | Effect | +|-----------------|------------------------------------------------------------------------------------------------------------------------------| +| `--sort FIELD` | Sort by `sector`, `symbol`, `weight`, `tax`, `1y`/`3y`/`5y`/`10y`, `3y-vol`/`10y-vol`, `3y-sharpe`/`10y-sharpe`, `5y-maxdd`. | +| `--asc` | Sort ascending (default is descending for numeric fields). | +| `--checks=MODE` | Findings engine: `all` (default), `fast` (skip long checks), `none`. | +| `--show-acked` | Include already-acknowledged findings. | + +Default sort groups by sector, then weight descending within each. + +## Example + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin review +``` + +``` + Symbol Sector Wt% 1Y 3Y ... 3Y-SR 5Y-MaxDD Tax% + VTI Diversified 66.9% +30.2% +23.3% ... 1.29 24.8% 10.9% + QQQ Technology 3.5% +43.1% +29.3% ... 1.41 32.6% 0.0% + + Findings (2 active, 0 acked, 0 resolved) + ⚠️ VTI at 66.9% of liquid (warn at 50.0%, flag at 70.0%) + ❌️ Diversified sector at 85.7% (warn at 60.0%, flag at 75.0%) +``` + +## See also + +- [Read your portfolio](../../guides/read-your-portfolio.md#review-per-holding-performance-and-risk) +- [Returns and performance](../../explanation/returns-and-performance.md) -- the metric definitions. +- [`analysis`](analysis.md) -- portfolio-level breakdowns. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/snapshot.md b/docs/reference/cli/snapshot.md new file mode 100644 index 0000000..01b64f1 --- /dev/null +++ b/docs/reference/cli/snapshot.md @@ -0,0 +1,49 @@ +# `zfin snapshot` + +Compute a portfolio snapshot and write it to `history/` -- the building +block of your value time series. + +``` +Usage: zfin snapshot [opts] +``` + +By default, refreshes candles for held symbols, derives the as-of date +from the cached candle dates, prices each lot at the close on or before +that date, and writes `history/-portfolio.srf` atomically. +The file is a discriminated SRF whose records start with +`kind::`. + +## Options + +| Flag | Effect | +|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| `--dry-run` | Compute and print to stdout; write nothing. | +| `--force` | Overwrite an existing snapshot for the date. | +| `--out ` | Override the output path. | +| `--as-of ` | Write a snapshot for a historical date (uses git to recover state and the candle cache for pricing). Accepts `YYYY-MM-DD` or `1W`/`1M`/`1Q`/`1Y`. | + +If the target file already exists and `--force` isn't passed, the run +skips with a stderr message. + +## Examples + +```bash +zfin snapshot --dry-run # preview today's snapshot +zfin snapshot # write history/-portfolio.srf +zfin snapshot --as-of 2025-01-02 # back-fill a past date +``` + +Automate with cron for a self-building series: + +```cron +0 18 * * 1-5 cd ~/finance && /usr/local/bin/zfin snapshot +``` + +## See also + +- [Snapshots and history](../../guides/snapshots-and-history.md) -- the workflow. +- [`history`](history.md) / [`compare`](compare.md) -- read the snapshots back. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/splits.md b/docs/reference/cli/splits.md new file mode 100644 index 0000000..b490858 --- /dev/null +++ b/docs/reference/cli/splits.md @@ -0,0 +1,41 @@ +# `zfin splits` + +Show stock-split history for a symbol. + +``` +Usage: zfin splits +``` + +Lists each split's effective date and ratio. Split data comes from +Polygon (`POLYGON_API_KEY`), merged with per-row split factors from +Tiingo's price series (which rescues events Polygon's reference +endpoint occasionally misses). + +## Example + +```bash +zfin splits AAPL +``` + +``` +Split History for AAPL +======================================== + Date Ratio +------------ ---------- +2020-08-31 4:1 +2014-06-09 7:1 +2005-02-28 2:1 +2000-06-21 2:1 +1987-06-16 2:1 + +5 split(s) +``` + +## See also + +- [`divs`](divs.md) -- dividend history for a symbol. +- [`history`](history.md) -- split-adjusted price history. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/cli/version.md b/docs/reference/cli/version.md new file mode 100644 index 0000000..094a001 --- /dev/null +++ b/docs/reference/cli/version.md @@ -0,0 +1,36 @@ +# `zfin version` + +Show zfin's version and build information. + +``` +Usage: zfin version [-v|--verbose] +``` + +Prints the version and build date. With `--verbose` / `-v`, also prints +the Zig compiler version, build mode, build target, resolved +`ZFIN_HOME`, and cache directory -- handy for bug reports. + +## Example + +```bash +zfin version +``` + +``` +zfin e246d1e (built 2026-06-19) +``` + +```bash +zfin version --verbose +``` + +Adds the build environment and resolved paths below the version line. + +## See also + +- [Documentation home](../../README.md) +- [`zfin help`](index.md) -- the grouped command list and global options. + +--- + +[CLI command reference](index.md) diff --git a/docs/reference/config/accounts-srf.md b/docs/reference/config/accounts-srf.md new file mode 100644 index 0000000..2574c08 --- /dev/null +++ b/docs/reference/config/accounts-srf.md @@ -0,0 +1,106 @@ +# `accounts.srf` reference + +`accounts.srf` describes each account referenced by your portfolio: +its tax treatment, the institution it lives at, and a few flags that +tune analysis and reconciliation. It powers the **By Tax Type** and +**By Account** breakdowns in [`zfin analysis`](../cli/analysis.md), the +umbrella-exposure estimate, the audit staleness checks, and broker +reconciliation. + +zfin loads `accounts.srf` from the same directory as the resolved +portfolio file. It is optional -- without it, accounts show up as +"Unknown" in the tax-type breakdown and everything else still works. + +## File format + +One record per account. The `account` name must match the +`account::` value used on your portfolio lots **exactly**. + +```srf +#!srfv1 +account::Pat 401k,tax_type::traditional,institution::fidelity,account_number::P401 +account::Joint taxable,tax_type::taxable,institution::schwab,account_number::JT01 +``` + +## Fields + +| Field | Type | Required | Default | Description | +|------------------------|--------|----------|-----------|----------------------------------------------------------------------------------------------------------------------------------| +| `account` | string | Yes | -- | Account name; must match `account::` on lots exactly. | +| `tax_type` | string | Yes | -- | `taxable`, `roth`, `traditional`, or `hsa`. | +| `institution` | string | No | -- | Broker key, e.g. `fidelity`, `schwab`, `vanguard`, `wells_fargo`. Used by [`zfin audit`](../cli/audit.md) to match export files. | +| `account_number` | string | No | -- | Account identifier used with `institution` for audit matching. Use a placeholder, not a full real number. | +| `update_cadence` | string | No | `weekly` | How often you refresh this account's manual data: `weekly`, `monthly`, `quarterly`, or `none`. Drives the audit staleness nag. | +| `cash_is_contribution` | bool | No | `false` | When `true`, raw cash-balance increases on this account count as real external contributions (see below). | +| `direct_indexing` | bool | No | `false` | Marks an account whose lots track a benchmark with tracking-error drift (loosens contribution/audit tolerances). | +| `shielded` | bool | No | (derived) | Umbrella-exposure override (see below). | + +## Tax types + +| Value | Display label | +|---------------|-----------------------| +| `taxable` | Taxable | +| `roth` | Roth (Post-Tax) | +| `traditional` | Traditional (Pre-Tax) | +| `hsa` | HSA (Triple Tax-Free) | + +Any other value is shown as-is. Accounts missing from `accounts.srf` +appear as "Unknown". + +## `update_cadence` and the audit nag + +[`zfin audit`](../cli/audit.md) (run flagless) flags accounts you +haven't refreshed within their cadence window: `weekly` = 7 days, +`monthly` = 30, `quarterly` = 90, `none` = never nag. The default is +`weekly`, so every account reminds you until you silence it -- set +`update_cadence::none` for accounts that update themselves (a live +brokerage feed) or that you simply don't track closely. + +## `cash_is_contribution` + +Most cash-balance movement is internal noise -- interest postings, +dividend credits, CD coupons, settlement sweeps -- which would inflate +the [`zfin contributions`](../cli/contributions.md) attribution total +if counted as new money. So cash deltas are ignored by default. Set +`cash_is_contribution:bool:true` only on accounts whose cash movement +is dominated by external deposits (payroll ESPP accrual, direct 401k +cash contributions). + +## `shielded` (umbrella exposure) + +The umbrella-exposure estimate in [`zfin analysis`](../cli/analysis.md) +splits your liquid net worth into "shielded" (retirement accounts, +assumed judgment-protected) and "exposed" (taxable). The default proxy +is "anything not `taxable` is shielded." Override it when that's wrong: + +- `shielded:bool:false` on a pre-tax account that is **not** + ERISA-protected (deferred-comp plans, non-qualified annuities), or on + IRAs in states with weak IRA protection. +- `shielded:bool:true` to mark a taxable account as shielded (rare; + e.g. some asset-protection trusts). + +IRA protection varies by state and is not modeled automatically; set +this explicitly if it matters to you. + +## Example (from `examples/pre-retirement-both`) + +```srf +#!srfv1 +account::Pat 401k,tax_type::traditional,institution::fidelity,account_number::P401 +account::Pat Roth,tax_type::roth,institution::fidelity,account_number::PROTH +account::Sam 401k,tax_type::traditional,institution::vanguard,account_number::S401 +account::Sam Roth,tax_type::roth,institution::vanguard,account_number::SROTH +account::Joint taxable,tax_type::taxable,institution::schwab,account_number::JT01 +account::Family HSA,tax_type::hsa,institution::fidelity,account_number::HSA01 +account::Kids 529,tax_type::taxable,institution::vanguard,account_number::C529 +``` + +## See also + +- [Map your accounts](../../guides/set-up-accounts.md) -- the walkthrough. +- [`zfin analysis`](../cli/analysis.md) -- tax-type and account breakdowns. +- [`zfin audit`](../cli/audit.md) -- staleness checks and broker reconciliation. + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/config/environment.md b/docs/reference/config/environment.md new file mode 100644 index 0000000..05bf079 --- /dev/null +++ b/docs/reference/config/environment.md @@ -0,0 +1,68 @@ +# Environment variables + +zfin is configured through environment variables. Set them in your +shell, or put them in a `.env` file. The `.env` file is searched first +in the binary's parent directory, then in the current directory; any +value set in the real environment is also honored. + +```bash +# .env +TIINGO_API_KEY=your_key +ZFIN_USER_EMAIL=you@example.com +ZFIN_HOME=/home/you/finance +``` + +To see which of these zfin actually picked up -- and what each one +unlocks -- run [`zfin doctor`](../cli/doctor.md). + +## API keys + +| Variable | Provider | Required for | Without it | +|----------------------|-------------------------|-------------------------------------|--------------------------------------------------------------------------------| +| `TIINGO_API_KEY` | Tiingo | Daily price history (candles) | Candles fall back to Yahoo; some symbols (especially mutual funds) won't price | +| `POLYGON_API_KEY` | Polygon | Dividends and splits (total return) | No forward-looking dividends; total returns may use Tiingo's view only | +| `FMP_API_KEY` | Financial Modeling Prep | Earnings | No earnings data (tab disabled) | +| `TWELVEDATA_API_KEY` | TwelveData | Quote fallback after Yahoo | No quote fallback if Yahoo fails | +| `OPENFIGI_API_KEY` | OpenFIGI | Faster CUSIP lookups | CUSIP lookups use slower anonymous rate limits | + +None are strictly required; without a given key, that data is simply +unavailable. Quotes (Yahoo) and options (CBOE) need no key. See +[Data providers and API keys](../providers.md) for signup links and +free-tier limits. + +## Contact email + +| Variable | Used for | +|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ZFIN_USER_EMAIL` | The contact address SEC EDGAR requires in its `User-Agent` header. Enables ETF profiles and [`zfin enrich`](../cli/enrich.md). Not a key -- just your email. Without it, ETF profiles and enrichment are unavailable. | + +## Paths and directories + +| Variable | Default | Purpose | +|------------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ZFIN_HOME` | (current directory) | Directory holding your `portfolio.srf`, `accounts.srf`, `metadata.srf`, `watchlist.srf`, and `.env`. When set, it is consulted **exclusively** for portfolio resolution (the current directory is not also searched). | +| `ZFIN_CACHE_DIR` | `~/.cache/zfin` | Where fetched provider data is cached. | +| `XDG_CACHE_HOME` | -- | Consulted to build the default cache dir (`$XDG_CACHE_HOME/zfin`) when `ZFIN_CACHE_DIR` is unset. | + +## Server sync + +| Variable | Purpose | +|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ZFIN_SERVER` | Optional URL of a remote [zfin-server](https://git.lerch.org/lobo/zfin-server) instance: a shared cache tier queried between the local cache and the providers. No-ops when unset. See [server sync](../../explanation/caching.md#server-sync-zfin_server). | + +## Output + +| Variable | Purpose | +|------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `NO_COLOR` | When set (to any value), disables colored output, per the [no-color.org](https://no-color.org) convention. Equivalent to the `--no-color` flag. | + +## See also + +- [Getting started](../../getting-started.md) -- the initial setup. +- [`zfin doctor`](../cli/doctor.md) -- reports which variables are set and what each enables. +- [Data providers and API keys](../providers.md) -- where to get each key. +- [Offline use and refreshing data](../../guides/offline-and-refresh.md) -- the `--refresh-data` cache policy. + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/config/keys-srf.md b/docs/reference/config/keys-srf.md new file mode 100644 index 0000000..580d763 --- /dev/null +++ b/docs/reference/config/keys-srf.md @@ -0,0 +1,72 @@ +# `keys.srf` reference + +`keys.srf` rebinds the interactive TUI's keys. zfin reads it from +`~/.config/zfin/keys.srf`. When the file is absent, built-in defaults +apply; when present, it is the **sole** source of bindings. + +Generate a fully-commented starting file from the current defaults: + +```bash +zfin interactive --default-keys > ~/.config/zfin/keys.srf +``` + +## File format + +One binding per line: + +```srf +action::ACTION_NAME,key::KEY_STRING[,scope::SCOPE] +``` + +- **Modifiers:** `ctrl+`, `alt+`, `shift+` (e.g. `ctrl+c`). +- **Special keys:** `tab`, `enter`, `escape`, `space`, `backspace`, + `left`, `right`, `up`, `down`, `page_up`, `page_down`, `home`, + `end`, `F1`-`F12`, `insert`, `delete`. +- **Multiple bindings:** repeat the action on several lines to bind + more than one key to it. +- **`scope`** (optional): omitted or `scope::global` is a global + binding; `scope::` (e.g. `scope::options`) is a tab-local + binding whose `action::` then names that tab's local action. + +A tab-local binding may not reuse a globally-bound key; zfin refuses +to start if you create that conflict. + +## Default global bindings + +```srf +action::quit,key::q +action::quit,key::ctrl+c +action::refresh,key::r +action::refresh,key::F5 +action::prev_tab,key::h +action::prev_tab,key::left +action::prev_tab,key::shift+tab +action::next_tab,key::l +action::next_tab,key::right +action::next_tab,key::tab +action::tab_1,key::1 +action::tab_2,key::2 +... +action::scroll_down,key::ctrl+d +action::scroll_up,key::ctrl+u +action::scroll_top,key::g +action::scroll_bottom,key::G +action::page_down,key::page_down +action::page_up,key::page_up +action::select_next,key::j +action::select_next,key::down +action::select_prev,key::k +``` + +Run `zfin interactive --default-keys` for the complete, current list +(including tab-scoped actions), each line ready to edit. + +## See also + +- [Customize the TUI](../../guides/customize-the-tui.md) -- the walkthrough. +- [The interactive TUI](../tui.md) -- tabs, actions, and the help overlay. +- [`theme.srf`](theme-srf.md) -- recolor the interface. + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/config/metadata-srf.md b/docs/reference/config/metadata-srf.md new file mode 100644 index 0000000..b64b2f5 --- /dev/null +++ b/docs/reference/config/metadata-srf.md @@ -0,0 +1,87 @@ +# `metadata.srf` reference + +`metadata.srf` classifies each symbol by asset class, sector, and +geography so [`zfin analysis`](../cli/analysis.md) can produce +allocation breakdowns and [`zfin review`](../cli/review.md) can group +holdings by sector. zfin loads it from the same directory as the +resolved portfolio file. + +It is optional, but without it the Asset Category / Sector / Geographic +breakdowns have nothing to group by. The fastest way to create one is +[`zfin enrich`](../cli/enrich.md); see +[Classify your holdings](../../guides/classify-holdings.md). + +## File format + +One record per `(symbol, allocation)` pair. A single-asset-class +security needs one line; a blended fund needs several lines that sum to +~100%. + +```srf +#!srfv1 +symbol::VTI,sector::Diversified,geo::US,asset_class::US Large Cap +symbol::AGG,sector::Bonds,geo::US,asset_class::Bonds +``` + +## Fields + +| Field | Type | Required | Default | Description | +|---------------|--------|----------|-----------|---------------------------------------------------------------------------------------------------------------------------| +| `symbol` | string | Yes | -- | Ticker or CUSIP. Must match `symbol::` (or `ticker::`) on a portfolio lot. | +| `name` | string | No | -- | Human-readable security name (e.g. "SPDR S&P 500 ETF Trust"). Shown where available; falls back to the symbol. | +| `asset_class` | string | No | -- | e.g. `US Large Cap`, `Bonds`, `International Developed`, `Emerging Markets`. | +| `sector` | string | No | -- | e.g. `Technology`, `Healthcare`, `Financials`, `Diversified`, `Bonds`. | +| `geo` | string | No | -- | e.g. `US`, `International Developed`, `Emerging Markets`. | +| `bucket` | string | No | (derived) | User-curated grouping label that overrides the auto-derived sector bucket for concentration/dominance checks (see below). | +| `pct` | number | No | `100` | Weight of this allocation line for the symbol. Use multiple lines for blended funds. | + +Cash and CD lots are classified as "Cash & CDs" automatically -- they +need no metadata entry. + +## The `bucket` field + +For concentration and sector-dominance analysis, zfin needs a +meaningful grouping label. It derives one automatically, but the +upstream `sector` can be uninformative -- ETF holdings data often tags +everything as the generic "Equity / Corporate." When several distinct +holdings collapse into one meaningless bucket, set `bucket::` yourself +to a label that actually distinguishes them. When `bucket` is unset, +zfin falls back through: `sector` (if it isn't a fund-decomposition +category) -> a composite `" "` -> `Unclassified`. +See [Classify your holdings](../../guides/classify-holdings.md#fixing-uninformative-sectors) +for a worked `bucket` example. + +## Blended funds + +For a target-date or balanced fund, add one line per asset class with +`pct:num:` weights summing to ~100: + +```srf +#!srfv1 +symbol::02315N600,asset_class::US Large Cap,pct:num:55 +symbol::02315N600,asset_class::International Developed,pct:num:20 +symbol::02315N600,asset_class::Bonds,pct:num:15 +symbol::02315N600,asset_class::Emerging Markets,pct:num:10 +``` + +## Example (from `examples/pre-retirement-both`) + +```srf +#!srfv1 +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 +``` + +## See also + +- [Classify your holdings](../../guides/classify-holdings.md) -- the walkthrough, including `enrich`. +- [`zfin enrich`](../cli/enrich.md) -- bootstrap this file from Wikidata + SEC EDGAR. +- [`zfin analysis`](../cli/analysis.md) -- the breakdowns this file feeds. +- [`zfin review`](../cli/review.md) -- the per-holding Sector column and grouping this file feeds. + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/config/portfolio-srf.md b/docs/reference/config/portfolio-srf.md new file mode 100644 index 0000000..8cd0b59 --- /dev/null +++ b/docs/reference/config/portfolio-srf.md @@ -0,0 +1,159 @@ +# `portfolio.srf` reference + +Your portfolio is a plain-text [SRF](https://git.lerch.org/lobo/srf) +file with **one lot per line**. A lot is a batch of shares of one +security bought on one date in one account. Positions are aggregated +from lots automatically. + +zfin looks for `portfolio.srf` in `ZFIN_HOME` (or the current +directory). You can split holdings across several `portfolio_*.srf` +files; zfin union-merges every match (see +[Build your portfolio](../../guides/set-up-your-portfolio.md) for the +multi-file workflow). + +## File format + +Every SRF line is a comma-separated list of `key::value` pairs. Typed +values use a type tag: + +- `key::value` -- string +- `key:num:value` -- number +- `key:bool:value` -- boolean (`true` / `false`) + +The first line must be the version header `#!srfv1`. Lines beginning +with `#` are comments. + +```srf +#!srfv1 +# A stock/ETF lot +symbol::VTI,shares:num:100,open_date::2024-01-15,open_price:num:220.50,account::Brokerage +``` + +## Lot fields + +| Field | Type | Required | Description | +|-----------------|--------|----------|---------------------------------------------------------------------------------------| +| `symbol` | string | Yes\* | Ticker or CUSIP. \*Optional for `cash` lots. | +| `shares` | number | Yes | Share count (or face value for cash/CDs). Negative for short option positions. | +| `open_date` | string | Yes\*\* | Purchase date `YYYY-MM-DD`. \*\*Not required for `cash`/`watch`. | +| `open_price` | number | Yes\*\* | Purchase price per share. \*\*Not required for `cash`/`watch`. | +| `close_date` | string | No | Sale date. Omit for an open lot. | +| `close_price` | number | No | Sale price per share. | +| `security_type` | string | No | `stock` (default), `option`, `cd`, `cash`, `illiquid`, `watch`. | +| `account` | string | No | Account name. Should match an `account::` entry in [`accounts.srf`](accounts-srf.md). | +| `note` | string | No | Free-text note (shown in cash/CD/illiquid tables). | +| `ticker` | string | No | Ticker alias used for price fetching when `symbol` is a CUSIP. | +| `price` | number | No | Manual price override (for securities the providers don't cover). | +| `price_date` | string | No | Date of the manual price (`YYYY-MM-DD`), for staleness display. | +| `drip` | bool | No | `true` if the lot is from dividend reinvestment (summarized as ST/LT groups). | +| `maturity_date` | string | No | CD maturity or option expiry date (`YYYY-MM-DD`). | +| `rate` | number | No | CD interest rate (e.g. `5.25` = 5.25%). | + +## Security types + +| Type | Meaning | +|-------------------|------------------------------------------------------------------------------------------------| +| `stock` (default) | Stocks, ETFs, mutual funds. Priced from Tiingo (Yahoo fallback), aggregated by symbol. | +| `option` | Option contracts. Shown separately; `shares` is the contract count (negative = written/short). | +| `cd` | Certificates of deposit. Shown by maturity with rate and face value. | +| `cash` | Cash, money-market, settlement balances. Grouped by account. | +| `illiquid` | Real estate, vehicles, etc. Excluded from the liquid total; included in Net Worth. | +| `watch` | Watchlist item: tracks price only, no position. See [`watchlist.srf`](watchlist-srf.md). | + +## Examples + +Each line below is valid on its own. These mirror the bundled +[`examples/`](../../../examples/) portfolios. + +```srf +#!srfv1 +# Stocks / ETFs (multiple lots of the same symbol aggregate) +symbol::VTI,shares:num:1100,open_date::2018-06-15,open_price:num:140.00,account::Pat 401k +symbol::VTI,shares:num:240,open_date::2015-01-08,open_price:num:103.40,account::Pat Roth + +# Cash (no symbol, no open_date/open_price needed) +security_type::cash,shares:num:48000.00,open_date::2026-04-30,open_price:num:1.00,account::Joint taxable + +# Closed (sold) lot +symbol::AMZN,shares:num:10,open_date::2022-03-15,open_price:num:150.25,close_date::2024-01-15,close_price:num:185.50 + +# DRIP lot (dividend reinvestment; summarized as ST/LT groups) +symbol::VTI,shares:num:0.234,open_date::2024-06-15,open_price:num:267.50,drip:bool:true,account::Brokerage + +# CUSIP with a ticker alias for pricing (e.g. a 401k CIT share class) +symbol::02315N600,shares:num:1200,open_date::2022-01-01,open_price:num:140.00,ticker::VTTHX,account::Fidelity 401k,note::VANGUARD TARGET 2035 + +# Manual price override (security with no provider coverage) +symbol::NON40OR52,shares:num:500,open_date::2023-01-01,open_price:num:155.00,price:num:163.636,price_date::2026-02-27,account::Fidelity 401k + +# Option: a written (short) call. Negative shares = contracts sold. +security_type::option,symbol::AAPL 06/20/2025 200.00 C,shares:num:-2,open_date::2025-01-15,open_price:num:12.50,option_type::call,underlying::AAPL,strike:num:200,maturity_date::2025-06-20,account::Brokerage + +# CD +security_type::cd,symbol::912797KR0,shares:num:10000,open_date::2024-06-01,open_price:num:10000,maturity_date::2025-06-01,rate:num:5.25,account::Brokerage,note::6-Month T-Bill + +# Illiquid asset (net worth only) +security_type::illiquid,symbol::HOME,shares:num:450000,open_date::2020-06-01,open_price:num:350000,note::Primary residence +``` + +## Price resolution + +For stock lots, the displayed price is resolved in this order: + +1. **Live API** -- latest close from cached candles (Tiingo, Yahoo fallback). +2. **Manual price** -- the lot's `price` field, for securities without coverage. +3. **Average cost** -- the position's `open_price`, as a last resort. + +Manual-priced rows render in warning color so you know they may be +stale; `price_date` tracks when you last updated them. + +## Ticker aliases and CUSIPs + +Some securities (notably 401k CIT share classes) are identified by +CUSIP but have a retail-ticker equivalent for pricing. Use `ticker::` +so `symbol::` stays the display identifier while the alias drives API +calls: + +```srf +symbol::02315N600,ticker::VTTHX,... +``` + +If the CUSIP and retail ticker have different NAVs (common for CIT vs. +retail funds), use a manual `price::` instead of `ticker::`. To resolve +a CUSIP to a ticker, use [`zfin lookup`](../cli/lookup.md). + +## Advanced and option fields + +These are less common but parse on any lot: + +| Field | Type | Applies to | Description | +|-----------------|--------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `price_ratio` | number | share classes | Multiplier applied to the fetched (alias) price to get the institutional NAV. E.g. if the retail class is $27.78 and the institutional class trades at $144.04, set `price_ratio:num:5.185`. | +| `underlying` | string | options | Underlying stock symbol (e.g. `AMZN`). | +| `strike` | number | options | Strike price. | +| `maturity_date` | string | options | Expiration date (`YYYY-MM-DD`). | +| `multiplier` | number | options | Shares per contract (default `100`). | +| `option_type` | string | options | `call` (default) or `put`. | + +An option lot is defined by these explicit fields, not by decoding the +symbol -- `symbol` is just a display label, so set it to whatever your +brokerage shows (e.g. `AAPL 06/20/2025 200.00 C`). For a worked +two-lot example, see the covered call in +[Build your portfolio](../../guides/set-up-your-portfolio.md#example-a-covered-call). + +## DRIP lots + +Lots marked `drip:bool:true` are collapsed into short-term (ST) and +long-term (LT) groups in the position detail view (split on the +1-year capital-gains threshold) rather than listed individually. + +## See also + +- [Build your portfolio](../../guides/set-up-your-portfolio.md) -- the task-oriented walkthrough. +- [`accounts.srf`](accounts-srf.md) -- give each `account::` a tax type. +- [`metadata.srf`](metadata-srf.md) -- classify each `symbol::` for analysis. +- [`zfin portfolio`](../cli/portfolio.md) and [`zfin import`](../cli/import.md). + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/config/projections-srf.md b/docs/reference/config/projections-srf.md new file mode 100644 index 0000000..a3df8a7 --- /dev/null +++ b/docs/reference/config/projections-srf.md @@ -0,0 +1,142 @@ +# `projections.srf` reference + +`projections.srf` configures [`zfin projections`](../cli/projections.md): +the Monte-Carlo-style retirement simulation run over the Shiller +dataset (1871-present). zfin loads it from the same directory as the +portfolio. It is optional -- without it, the command runs with +sensible defaults (20/30/45-year horizons, 90/95/99% confidence, +no accumulation phase). + +This page is the field reference. For how the simulation actually +works see [The retirement projection model](../../explanation/projections-model.md); +for a guided setup see [Plan for retirement](../../guides/plan-retirement.md). + +## Record types + +Every line starts with a `type::` discriminator: + +| `type::` | Purpose | +|-------------|---------------------------------------------------------------------------------------------------| +| `config` | A single configuration field (allocation, horizon, retirement target, contributions, spending). | +| `birthdate` | A household member's birthdate (drives ages, `horizon_age`, `retirement_age`, life-event timing). | +| `event` | A life event: recurring income or expense (Social Security, pension, tuition, healthcare). | + +```srf +#!srfv1 +type::config,target_stock_pct:num:80 +type::config,horizon:num:25 +type::config,horizon_age:num:95 +type::birthdate,date::1981-04-12 +type::event,name::Social Security,start_age:num:70,amount:num:38400 +``` + +## `config` fields + +| Field | Type | Description | +|--------------------------------------|------|--------------------------------------------------------------------------------------------------------------------------------| +| `target_stock_pct` | num | Asset-allocation target (0-100). Sets the simulation's stock/bond blend. | +| `horizon` | num | Distribution-phase length in years. Repeat the line for multiple horizons. | +| `horizon_age` | num | Horizon expressed as an age; resolves to `target_age - oldest_current_age`. Repeatable. | +| `retirement_age` | num | Age the **oldest** configured person must reach to retire. | +| `retirement_at` | date | Absolute retirement date (`YYYY-MM-DD`). Wins over `retirement_age` if both set. | +| `annual_contribution` | num | Yearly accumulation-phase contribution, in today's dollars. | +| `contribution_inflation_adjusted` | bool | If `true` (default), contributions grow with CPI year over year. | +| `target_spending` | num | Desired retirement spending, in today's dollars. | +| `target_spending_inflation_adjusted` | bool | If `true` (default), target spending grows with CPI during distribution. | +| `retirement_target` | num | Annotation on a `horizon`/`horizon_age` line that overrides the earliest-retirement promotion rule. Allowed: `90`, `95`, `99`. | + +## `birthdate` fields + +| Field | Type | Description | +|----------|------|----------------------------------------------------| +| `date` | date | Birthdate (`YYYY-MM-DD`). | +| `person` | num | Household member (`1` default, `2` for a partner). | + +```srf +type::birthdate,date::1981-04-12 +type::birthdate,date::1983-09-08,person:num:2 +``` + +## `event` fields + +Life events modify annual cash flow in both phases. Positive amounts +are income (offset withdrawals); negative amounts are expenses (added +to withdrawals). + +| Field | Type | Description | +|----------------------|--------|-----------------------------------------------------------------------------------------| +| `name` | string | Label shown in the Life Events table. | +| `amount` | num | Annual amount. Positive = income, negative = expense. | +| `start_age` | num | Age (of `person`) at which the event begins. | +| `duration` | num | Optional length in years. Omit for a permanent event. | +| `person` | num | Whose age `start_age` refers to (`1` default, `2` for a partner). | +| `inflation_adjusted` | bool | If `true` (default), the amount grows with CPI. Set `false` for a fixed nominal amount. | + +```srf +# Permanent income starting at age 70 +type::event,name::Social Security,start_age:num:70,amount:num:38400 + +# 4-year expense starting at age 50 +type::event,name::College Tuition,start_age:num:50,duration:num:4,amount:num:-55000 + +# A partner's pension +type::event,name::Pension,start_age:num:65,person:num:2,amount:num:24000 +``` + +## The two retirement-planning inputs + +`projections.srf` answers a different question depending on which +inputs you set. This is the single most important thing to understand +about the file: + +| You set... | zfin answers... | Display | +|----------------------------------------------------|-----------------------------------------|-----------------------------------------------------------------------| +| A target date (`retirement_age` / `retirement_at`) | "Given my date, what can I spend?" | Accumulation-phase block with a dated headline. | +| A target spending (`target_spending`) | "Given my spending, when can I retire?" | Earliest-retirement grid; one cell is promoted to the headline. | +| Both | Both, back to back | Configured date wins the headline; grid renders below for comparison. | +| Neither | Already-retired view | "Years until possible retirement: none". | + +When `target_spending` is set, the **earliest-retirement grid** shows, +for each (horizon x confidence) pair, the earliest year that sustains +the spending. The default promotion rule picks the headline cell by +walking horizons longest-to-shortest at 99% confidence, preferring the +longest horizon that keeps the oldest person under age 100. Override it +with a `retirement_target` annotation on one horizon line: + +```srf +# use the 35yr x 95% cell as the headline +type::config,horizon:num:35,retirement_target:num:95 +``` + +At most one horizon may carry the annotation; configuring more than one +drops them all and falls back to the default rule. If the promoted cell +is infeasible (no accumulation length <= 50 years sustains the +spending), the headline reads "not feasible" and the grid still renders +so you can pick a workable anchor. + +## The example configurations + +The five bundled examples are fully-configured walkthroughs of each +combination: + +| `examples/...` | Inputs | +|----------------------------------|---------------------------------------------------| +| `pre-retirement-age` | target date only | +| `pre-retirement-spending` | target spending only | +| `pre-retirement-spending-target` | target spending + an explicit (infeasible) anchor | +| `pre-retirement-both` | target date + target spending | +| `post-retirement` | neither (distribution-only) | + +```bash +ZFIN_HOME=examples/pre-retirement-both zfin projections +``` + +## See also + +- [Plan for retirement](../../guides/plan-retirement.md) -- guided setup. +- [The retirement projection model](../../explanation/projections-model.md) -- how the simulation works. +- [`zfin projections`](../cli/projections.md) -- command flags (`--as-of`, `--overlay-actuals`, `--convergence`, `--return-backtest`). + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/config/theme-srf.md b/docs/reference/config/theme-srf.md new file mode 100644 index 0000000..9f0ba47 --- /dev/null +++ b/docs/reference/config/theme-srf.md @@ -0,0 +1,60 @@ +# `theme.srf` reference + +`theme.srf` recolors the interactive TUI. zfin reads it from +`~/.config/zfin/theme.srf`. When the file is absent, built-in defaults +(a monokai/opencode palette) apply; when present, it is the **sole** +source of colors. + +Generate a commented starting file from the current defaults: + +```bash +zfin interactive --default-theme > ~/.config/zfin/theme.srf +``` + +## File format + +One `key::value` per line. Every value is a hex RGB string, +`#rrggbb`: + +```srf +#!srfv1 +bg::#0a0a0a +text::#eeeeee +accent::#9d7cd8 +positive::#7fd88f +negative::#e06c75 +``` + +## Color keys + +| Key | Used for | +|----------------------------------------|-------------------------------------| +| `bg` | Main background | +| `bg_panel` | Panel background | +| `bg_element` | Inset element background | +| `tab_bg` / `tab_fg` | Inactive tab bar | +| `tab_active_bg` / `tab_active_fg` | Active tab | +| `text` | Primary text | +| `text_muted` | Secondary text | +| `text_dim` | De-emphasized text | +| `status_bg` / `status_fg` | Status line | +| `input_bg` / `input_fg` / `input_hint` | Modal text input | +| `accent` | Accent / highlights | +| `positive` | Gains, positive values | +| `negative` | Losses, negative values | +| `warning` | Warnings (e.g. stale manual prices) | +| `info` | Informational highlights | +| `select_bg` / `select_fg` | Selected row | +| `border` | Borders and rules | + +Run `zfin interactive --default-theme` for the full key list with the +default values filled in. + +## See also + +- [Customize the TUI](../../guides/customize-the-tui.md) -- the walkthrough. +- [`keys.srf`](keys-srf.md) -- rebind keys. + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/config/transaction-log-srf.md b/docs/reference/config/transaction-log-srf.md new file mode 100644 index 0000000..f151efb --- /dev/null +++ b/docs/reference/config/transaction-log-srf.md @@ -0,0 +1,68 @@ +# `transaction_log.srf` reference + +`transaction_log.srf` is an optional sibling of `portfolio.srf` that +declares real-world transactions which change how zfin interprets the +portfolio diff. In v1 it holds exactly one kind of record: +**transfers** -- money you moved between accounts you own. + +## Why it exists + +[`zfin contributions`](../cli/contributions.md) infers "new money" by +diffing your portfolio over time. A plain diff can't tell an external +contribution apart from an internal transfer, so moving (say) $50k from +one account to another would otherwise be **double-counted**: the +receiving account's new lots look like contributions while the sending +account's removed lots are ignored. Declaring the transfer here cancels +that out. + +Missing file -> the matcher is a no-op; nothing changes. + +## File format + +One record per **destination**. A record pins the money to exactly one +landing spot: either a specific lot (`SYMBOL@open_date`) or the literal +token `cash`. A sweep that lands in several lots becomes several records +sharing the same `(date, from, to)` but differing in `dest_lot`. + +```srf +#!srfv1 +# Simple cash transfer between two accounts +transfer::2026-05-02,type::cash,amount:num:5000,from::Sample IRA,to::Sample Brokerage,dest_lot::cash + +# Transfer that was invested into a single stock lot on arrival +transfer::2026-05-02,type::cash,amount:num:7000,from::Sample IRA,to::Sample Brokerage,dest_lot::VTI@2026-05-03 + +# Sweep into a basket plus a cash residual (two records, same date/from/to) +transfer::2026-05-02,type::cash,amount:num:145300,from::Sample IRA,to::Sample Brokerage,dest_lot::VTI@2026-05-03 +transfer::2026-05-02,type::cash,amount:num:4700,from::Sample IRA,to::Sample Brokerage,dest_lot::cash +``` + +## Fields + +| Field | Type | Required | Description | +|------------|--------|----------|---------------------------------------------------------------------| +| `transfer` | date | Yes | Transfer date (`YYYY-MM-DD`); the record key. | +| `type` | string | Yes | `cash` or `in_kind` (see v1 scope below). | +| `amount` | num | Yes | Dollar amount transferred to this destination. | +| `from` | string | Yes | Source account name (matches an `account::` in your portfolio). | +| `to` | string | Yes | Destination account name. | +| `dest_lot` | string | Yes | Where it landed: `cash`, or `SYMBOL@YYYY-MM-DD` for a specific lot. | + +## v1 scope and limits + +- Only `transfer::` records. Buys, sells, and dividends stay inferred + from the portfolio diff. +- Only `type::cash` is wired into the contributions classifier. + `type::in_kind` parses (the format is forward-compatible) but the + matcher rejects it with an "in-kind transfers not yet supported" + message. +- Forward-looking only -- there is no historical reconstruction. + +## See also + +- [Track contributions](../../guides/track-contributions.md) -- the walkthrough. +- [`zfin contributions`](../cli/contributions.md) -- the command that consumes this file. + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/config/watchlist-srf.md b/docs/reference/config/watchlist-srf.md new file mode 100644 index 0000000..b8be4c4 --- /dev/null +++ b/docs/reference/config/watchlist-srf.md @@ -0,0 +1,45 @@ +# `watchlist.srf` reference + +A watchlist tracks the price of symbols you don't own. Watchlist +symbols appear at the bottom of the portfolio view (and the TUI +Portfolio tab) showing their latest price, with no position, cost, or +weight. + +zfin loads `watchlist.srf` from `ZFIN_HOME` (or the current +directory), or from an explicit path with the `-w` / `--watchlist` +flag. It is optional. + +## File format + +One `symbol::` per line, under the standard `#!srfv1` header: + +```srf +#!srfv1 +symbol::MSFT +symbol::NVDA +symbol::TSLA +``` + +## Two ways to watch a symbol + +| Approach | When to use | +|---------------------------------------------------------------------|---------------------------------------------------------------------------| +| A `watchlist.srf` file (above) | A standalone list you reuse across portfolios, or one you pass with `-w`. | +| A `security_type::watch` lot in [`portfolio.srf`](portfolio-srf.md) | You want the watch entry to live alongside your holdings. | + +```srf +# Equivalent watch entry inside portfolio.srf +security_type::watch,symbol::NVDA +``` + +The two sources are merged: the effective watchlist is the union of +the watchlist file and any `watch` lots in the portfolio. + +## See also + +- [`portfolio.srf`](portfolio-srf.md) -- the `watch` security type. +- [`zfin quote`](../cli/quote.md) -- a one-off price check without adding to a watchlist. + +--- + +[Documentation home](../../README.md#reference) diff --git a/docs/reference/providers.md b/docs/reference/providers.md new file mode 100644 index 0000000..88eb1af --- /dev/null +++ b/docs/reference/providers.md @@ -0,0 +1,70 @@ +# Data providers and API keys + +zfin aggregates several free-tier data sources, using each for what it +does best and caching aggressively to stay within limits. This page is +the reference for who supplies what, the free-tier ceilings, and where +to get keys. For the design rationale see +[Why multiple data providers](../explanation/data-providers.md); for +how keys are configured see +[environment variables](config/environment.md). + +## Summary + +| Data | Provider | Auth | Free-tier limit | Cache TTL | +|-----------------------|------------------|-------------------------------|----------------------------|------------| +| Daily candles (OHLCV) | Tiingo | `TIINGO_API_KEY` | 1,000 req/day, 50 req/hour | ~24h | +| Real-time quotes | Yahoo | none | unofficial | never | +| Quote fallback | TwelveData | `TWELVEDATA_API_KEY` | 8/min, 800/day | never | +| Dividends | Polygon | `POLYGON_API_KEY` | 5/min | 14 days | +| Splits | Polygon | `POLYGON_API_KEY` | 5/min | 14 days | +| Options chains | CBOE | none | ~30/min (self-imposed) | 1 hour | +| Earnings | FMP | `FMP_API_KEY` | 250 req/day | 30 days | +| ETF profiles | SEC EDGAR | `ZFIN_USER_EMAIL` | 10/sec | ~90 days | +| Classification | Wikidata + EDGAR | `ZFIN_USER_EMAIL` | no daily quota | long-lived | +| CUSIP lookup | OpenFIGI | `OPENFIGI_API_KEY` (optional) | higher with key | indefinite | + +## Where to get a key + +| Provider | Sign up | Notes | +|------------|-------------------------------------|--------------------------------------------------------------------------------------| +| Tiingo | | The one key worth setting first -- it's the primary price source. | +| Polygon | | Enables total return (dividends) and forward-looking dividend dates. | +| FMP | | Earnings actuals + estimates. | +| TwelveData | | Optional quote fallback. | +| OpenFIGI | | Optional; raises CUSIP-lookup rate limits. | +| SEC EDGAR | (no signup) | Set `ZFIN_USER_EMAIL` to your address; EDGAR requires a contact in its `User-Agent`. | + +## Per-provider notes + +- **Tiingo** -- primary for candles across stocks, ETFs, and mutual + funds. Candles are fetched from a fixed 2000-01-01 start so the cache + supports long `--as-of` projections. Its price series also carries + per-row dividend/split data that zfin merges into the Polygon view. +- **Yahoo** -- primary, keyless quote source, and the candle fallback + when Tiingo is unavailable. +- **TwelveData** -- quote fallback only. It is no longer used for + candles (its split-adjusted closes proved unreliable for return math). +- **Polygon** -- primary for dividends and splits, including + forward-looking declared events. +- **CBOE** -- keyless options chains, 15-minute delayed during market + hours, with greeks and open interest. +- **FMP** -- earnings history and estimates. ETFs, mutual funds, + CUSIPs, and some dual-class shares return no earnings on the free + tier (a documented limitation). +- **SEC EDGAR + Wikidata** -- ETF profiles (NPORT-P holdings, sector + weights, AUM) and the classification data behind + [`enrich`](cli/enrich.md). Needs `ZFIN_USER_EMAIL`, not a key. + +## Rate limiting + +Each provider has a client-side token-bucket limiter sized to its +free-tier ceiling. When you'd exceed the rate, zfin blocks until a +token frees up rather than firing a request that would 429 -- so a +large `--refresh-data=force` run paces itself. See +[Caching](../explanation/caching.md#rate-limiting). + +## See also + +- [Environment variables](config/environment.md) -- setting each key. +- [Getting started](../getting-started.md) -- the minimal first-run setup. +- [Why multiple data providers](../explanation/data-providers.md) -- fallback behavior. diff --git a/docs/reference/tui.md b/docs/reference/tui.md new file mode 100644 index 0000000..7e6b05a --- /dev/null +++ b/docs/reference/tui.md @@ -0,0 +1,76 @@ +# The interactive TUI + +```bash +zfin i # or: zfin interactive +``` + +The TUI is a multi-tab terminal interface over the same engine and data +files as the CLI. With no arguments it auto-loads `portfolio.srf` and +opens on the Portfolio tab; `zfin i -s AAPL` opens on the Quote tab for +a symbol. See [`zfin interactive`](cli/interactive.md) for launch flags. + +## Tabs + +Nine tabs, in order. Some show your whole portfolio; others show the +currently-selected symbol. + +| Tab | Scope | Shows | +|-----------------|-----------|--------------------------------------------------------------------------------------------------------| +| **Portfolio** | portfolio | Positions, valuations, cash, watchlist (like [`portfolio`](cli/portfolio.md)). | +| **Analysis** | portfolio | Allocation breakdowns and umbrella exposure (like [`analysis`](cli/analysis.md)). | +| **Review** | portfolio | Per-holding performance/risk dashboard with findings (like [`review`](cli/review.md)). | +| **Projections** | portfolio | Percentile bands, safe withdrawal, and the actuals overlay (like [`projections`](cli/projections.md)). | +| **History** | portfolio | Portfolio-value timeline from snapshots (like [`history`](cli/history.md)). | +| **Quote** | symbol | Latest price and chart for the selected symbol (like [`quote`](cli/quote.md)). | +| **Performance** | symbol | Trailing returns and risk metrics for the symbol (like [`perf`](cli/perf.md)). | +| **Earnings** | symbol | Earnings history and upcoming events (like [`earnings`](cli/earnings.md)). | +| **Options** | symbol | Options chain for the symbol (like [`options`](cli/options.md)). | + +The symbol-scoped tabs follow a single "current symbol"; change it from +the Portfolio tab (select a holding) or by launching with `-s`. + +## Charts + +Tabs with charts render high-fidelity **Kitty graphics** when your +terminal supports them, falling back to braille otherwise. Force a mode +with `--chart auto|braille|WxH`. The Projections tab is the +highest-fidelity surface for the actuals overlay and the +convergence/return-backtest views. + +## Default keybindings + +Press `?` any time for the in-app overlay (it always reflects your +current bindings). The global defaults: + +| Key(s) | Action | +|----------------------------------------------|--------------------------------| +| `q`, `Ctrl-C` | Quit | +| `r`, `F5` | Refresh the current tab's data | +| `l` / `right` / `tab` | Next tab | +| `h` / `left` / `shift+tab` | Previous tab | +| `1`-`8` | Jump to a tab by number | +| `j` / `down`, `k` / `up` | Move the selection | +| `g` / `G` | Jump to top / bottom | +| `Ctrl-d` / `Ctrl-u` | Half-page down / up | +| `page_down` / `Ctrl-f`, `page_up` / `Ctrl-b` | Page down / up | + +Individual tabs add their own actions (e.g. the Projections tab's `d` +to set an as-of date and `o` to toggle the overlay). The `?` overlay +and `zfin i --default-keys` list every binding, global and tab-scoped. + +## Customizing + +Both keys and colors are configurable via files in `~/.config/zfin/`: + +```bash +zfin i --default-keys > ~/.config/zfin/keys.srf +zfin i --default-theme > ~/.config/zfin/theme.srf +``` + +See [Customize the TUI](../guides/customize-the-tui.md), the +[`keys.srf` reference](config/keys-srf.md), and the +[`theme.srf` reference](config/theme-srf.md). + +--- + +[Documentation home](../README.md)