add docs/guides
This commit is contained in:
parent
c832ecf1bf
commit
74fc219afd
59 changed files with 5154 additions and 837 deletions
876
README.md
876
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 <command> [args]
|
||||
|
||||
Commands:
|
||||
perf <SYMBOL> Trailing returns (1yr/3yr/5yr/10yr, price + total)
|
||||
quote <SYMBOL> Real-time quote
|
||||
history <SYMBOL> Last 30 days price history
|
||||
divs <SYMBOL> Dividend history with TTM yield
|
||||
splits <SYMBOL> Split history
|
||||
options <SYMBOL> Options chains (all expirations)
|
||||
earnings <SYMBOL> Earnings history and upcoming events
|
||||
etf <SYMBOL> ETF profile (expense ratio, holdings, sectors)
|
||||
portfolio <FILE> Portfolio analysis from .srf file
|
||||
snapshot [opts] Write a daily portfolio snapshot to history/
|
||||
compare <DATE> [<DATE>]
|
||||
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 <DATE> Compare snapshot at DATE vs current live portfolio
|
||||
zfin compare <DATE1> <DATE2> 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 <PATH>`)
|
||||
|
||||
The `quote` and `projections` commands support `--export-chart <PATH>` 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 <FILE> Portfolio file (.srf format)
|
||||
-w, --watchlist <FILE> Watchlist file (default: watchlist.srf if present)
|
||||
-s, --symbol <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 `<portfolio-dir>/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 <DATE> --overlay-actuals` to plot the
|
||||
**realized portfolio trajectory** from `<DATE>` 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 `<portfolio-dir>/history/*-portfolio.srf`
|
||||
— produced by `zfin snapshot`. Highest fidelity (full lot-level
|
||||
state, exact totals).
|
||||
2. **Imported values** in `<portfolio-dir>/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 <DATE>`
|
||||
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 <command> [args]
|
||||
|
||||
Commands:
|
||||
perf <SYMBOL> Trailing returns (1yr/3yr/5yr/10yr, price + total)
|
||||
quote <SYMBOL> Real-time quote with chart
|
||||
history <SYMBOL> Last 30 days price history
|
||||
divs <SYMBOL> Dividend history with TTM yield
|
||||
splits <SYMBOL> Split history
|
||||
options <SYMBOL> Options chains (all expirations)
|
||||
earnings <SYMBOL> Earnings history and upcoming events
|
||||
etf <SYMBOL> 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 <D1> [<D2>] Compare portfolio state across two dates
|
||||
enrich <FILE|SYMBOL> Generate metadata.srf from Wikidata + SEC EDGAR
|
||||
lookup <CUSIP> 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 <FILE> 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
|
||||
|
|
|
|||
26
TODO.md
26
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 <PATH>` shipped for `quote` and `projections`
|
||||
|
|
|
|||
103
docs/README.md
Normal file
103
docs/README.md
Normal file
|
|
@ -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.
|
||||
121
docs/explanation/caching.md
Normal file
121
docs/explanation/caching.md
Normal file
|
|
@ -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/<SYMBOL>/<type>.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}/<SYMBOL>/<type>`
|
||||
(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).
|
||||
126
docs/explanation/concepts.md
Normal file
126
docs/explanation/concepts.md
Normal file
|
|
@ -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/<date>-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.
|
||||
72
docs/explanation/data-providers.md
Normal file
72
docs/explanation/data-providers.md
Normal file
|
|
@ -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.
|
||||
105
docs/explanation/faq-troubleshooting.md
Normal file
105
docs/explanation/faq-troubleshooting.md
Normal file
|
|
@ -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.
|
||||
141
docs/explanation/projections-model.md
Normal file
141
docs/explanation/projections-model.md
Normal file
|
|
@ -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.
|
||||
127
docs/explanation/returns-and-performance.md
Normal file
127
docs/explanation/returns-and-performance.md
Normal file
|
|
@ -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.
|
||||
243
docs/getting-started.md
Normal file
243
docs/getting-started.md
Normal file
|
|
@ -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 <https://tiingo.com>.
|
||||
|
||||
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) | <https://polygon.io> |
|
||||
| `FMP_API_KEY` | Earnings history and estimates | <https://financialmodelingprep.com> |
|
||||
| `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)
|
||||
244
docs/guides/audit-against-brokerage.md
Normal file
244
docs/guides/audit-against-brokerage.md
Normal file
|
|
@ -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 <CSV>` |
|
||||
| **Schwab** (per-account) | *Accounts -> Positions* -> **Export** (one CSV per account) | `--schwab <CSV>` |
|
||||
| **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:
|
||||
|
||||

|
||||
|
||||
*(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. **`<portfolio-dir>/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)
|
||||
114
docs/guides/classify-holdings.md
Normal file
114
docs/guides/classify-holdings.md
Normal file
|
|
@ -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)
|
||||
73
docs/guides/customize-the-tui.md
Normal file
73
docs/guides/customize-the-tui.md
Normal file
|
|
@ -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::<tab>` (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)
|
||||
80
docs/guides/offline-and-refresh.md
Normal file
80
docs/guides/offline-and-refresh.md
Normal file
|
|
@ -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)
|
||||
177
docs/guides/periodic-review.md
Normal file
177
docs/guides/periodic-review.md
Normal file
|
|
@ -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/<today>-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)
|
||||
310
docs/guides/plan-retirement.md
Normal file
310
docs/guides/plan-retirement.md
Normal file
|
|
@ -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)
|
||||
190
docs/guides/read-your-portfolio.md
Normal file
190
docs/guides/read-your-portfolio.md
Normal file
|
|
@ -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)
|
||||
101
docs/guides/set-up-accounts.md
Normal file
101
docs/guides/set-up-accounts.md
Normal file
|
|
@ -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)
|
||||
193
docs/guides/set-up-your-portfolio.md
Normal file
193
docs/guides/set-up-your-portfolio.md
Normal file
|
|
@ -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)
|
||||
149
docs/guides/snapshots-and-history.md
Normal file
149
docs/guides/snapshots-and-history.md
Normal file
|
|
@ -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 `<portfolio-dir>/history/<date>-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/<today>-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)
|
||||
117
docs/guides/track-contributions.md
Normal file
117
docs/guides/track-contributions.md
Normal file
|
|
@ -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)
|
||||
BIN
docs/images/fidelity-positions-download.png
Normal file
BIN
docs/images/fidelity-positions-download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
51
docs/reference/cli/analysis.md
Normal file
51
docs/reference/cli/analysis.md
Normal file
|
|
@ -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)
|
||||
58
docs/reference/cli/audit.md
Normal file
58
docs/reference/cli/audit.md
Normal file
|
|
@ -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 <N>` | Manual-price staleness threshold (default 3). |
|
||||
| `--fidelity <CSV>` | Fidelity positions CSV ("All accounts" -> Positions tab -> Download). |
|
||||
| `--schwab <CSV>` | 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)
|
||||
35
docs/reference/cli/cache.md
Normal file
35
docs/reference/cli/cache.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# `zfin cache`
|
||||
|
||||
Inspect or clear the local provider-data cache.
|
||||
|
||||
```
|
||||
Usage: zfin cache <stats|clear>
|
||||
```
|
||||
|
||||
| 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)
|
||||
50
docs/reference/cli/compare.md
Normal file
50
docs/reference/cli/compare.md
Normal file
|
|
@ -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 <DATE> # compare DATE vs. live portfolio
|
||||
zfin compare <DATE1> <DATE2> # 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 <DATE>` / `--snapshot-after <DATE>` | Override a side's snapshot (`--snapshot-after live` for the current portfolio). |
|
||||
| `--commit-before <SPEC>` / `--commit-after <SPEC>` | 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)
|
||||
55
docs/reference/cli/contributions.md
Normal file
55
docs/reference/cli/contributions.md
Normal file
|
|
@ -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 <DATE>` | commit at/before DATE vs. HEAD (or working copy if dirty) |
|
||||
| `--since <D1> --until <D2>` | 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 <DATE>` | Earliest side (resolves to commit at/before). |
|
||||
| `--until <DATE>` | Latest side (pair with `--since`). |
|
||||
| `--commit-before <SPEC>` | Pin the before commit directly (same grammar as `--commit-after`, minus `working`). |
|
||||
| `--commit-after <SPEC>` | 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)
|
||||
37
docs/reference/cli/divs.md
Normal file
37
docs/reference/cli/divs.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# `zfin divs`
|
||||
|
||||
Show dividend history for a symbol, with trailing-twelve-month yield.
|
||||
|
||||
```
|
||||
Usage: zfin divs <SYMBOL>
|
||||
```
|
||||
|
||||
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)
|
||||
102
docs/reference/cli/doctor.md
Normal file
102
docs/reference/cli/doctor.md
Normal file
|
|
@ -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)
|
||||
45
docs/reference/cli/earnings.md
Normal file
45
docs/reference/cli/earnings.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# `zfin earnings`
|
||||
|
||||
Show earnings history (with EPS surprise) and upcoming events for a
|
||||
symbol.
|
||||
|
||||
```
|
||||
Usage: zfin earnings <SYMBOL>
|
||||
```
|
||||
|
||||
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)
|
||||
48
docs/reference/cli/enrich.md
Normal file
48
docs/reference/cli/enrich.md
Normal file
|
|
@ -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)
|
||||
47
docs/reference/cli/etf.md
Normal file
47
docs/reference/cli/etf.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# `zfin etf`
|
||||
|
||||
Show an ETF's profile: net assets, holdings count, sector allocation,
|
||||
and top holdings.
|
||||
|
||||
```
|
||||
Usage: zfin etf <SYMBOL>
|
||||
```
|
||||
|
||||
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)
|
||||
44
docs/reference/cli/exposure.md
Normal file
44
docs/reference/cli/exposure.md
Normal file
|
|
@ -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 <SYMBOL>
|
||||
```
|
||||
|
||||
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)
|
||||
57
docs/reference/cli/history.md
Normal file
57
docs/reference/cli/history.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# `zfin history`
|
||||
|
||||
Two modes in one command, selected by whether you pass a symbol.
|
||||
|
||||
```
|
||||
Usage:
|
||||
zfin history <SYMBOL> # 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 <DATE>` | Earliest as-of date (inclusive). |
|
||||
| `--until <DATE>` | Latest as-of date (inclusive). |
|
||||
| `--metric <name>` | `liquid` (default), `illiquid`, or `net_worth`. |
|
||||
| `--resolution <name>` | `daily` \| `weekly` \| `monthly` \| `auto`. |
|
||||
| `--limit <N>` | 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)
|
||||
51
docs/reference/cli/import.md
Normal file
51
docs/reference/cli/import.md
Normal file
|
|
@ -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 <PORTFOLIO> 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 <FILE>` | Target file (a single concrete path, not a glob). **Required.** |
|
||||
| `--fidelity <CSV>` | Fidelity positions CSV. |
|
||||
| `--schwab <CSV>` | Schwab per-account positions CSV. |
|
||||
| `--wells-fargo <FILE>` | Wells Fargo positions paste (`-` for stdin). |
|
||||
| `--account <NAME>` | (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)
|
||||
88
docs/reference/cli/index.md
Normal file
88
docs/reference/cli/index.md
Normal file
|
|
@ -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> [command options]
|
||||
```
|
||||
|
||||
Get help at any time with `zfin help` or per command with
|
||||
`zfin <command> --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=<auto\|force\|never>` | Cache freshness policy. `auto` (default) respects TTLs; `force` re-fetches everything; `never` is offline. See [offline guide](../../guides/offline-and-refresh.md). |
|
||||
| `-p, --portfolio <PATTERN>` | 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 <FILE>` | 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)
|
||||
43
docs/reference/cli/interactive.md
Normal file
43
docs/reference/cli/interactive.md
Normal file
|
|
@ -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 <SYMBOL>` | Pre-load a symbol and open on the Quote tab. |
|
||||
| `--chart <MODE>` | 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=<value>`)
|
||||
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)
|
||||
36
docs/reference/cli/lookup.md
Normal file
36
docs/reference/cli/lookup.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# `zfin lookup`
|
||||
|
||||
Resolve a CUSIP (9-character security identifier) to its ticker via the
|
||||
OpenFIGI API.
|
||||
|
||||
```
|
||||
Usage: zfin lookup <CUSIP>
|
||||
```
|
||||
|
||||
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)
|
||||
52
docs/reference/cli/milestones.md
Normal file
52
docs/reference/cli/milestones.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# `zfin milestones`
|
||||
|
||||
Find the dates your portfolio first reached each of a series of value
|
||||
thresholds.
|
||||
|
||||
```
|
||||
Usage: zfin milestones --step <expr> [--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 <expr>` | 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)
|
||||
62
docs/reference/cli/options.md
Normal file
62
docs/reference/cli/options.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# `zfin options`
|
||||
|
||||
Show the options chain (all expirations) for a symbol.
|
||||
|
||||
```
|
||||
Usage: zfin options <SYMBOL> [--ntm <N>]
|
||||
```
|
||||
|
||||
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 <N>` | `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)
|
||||
52
docs/reference/cli/perf.md
Normal file
52
docs/reference/cli/perf.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# `zfin perf`
|
||||
|
||||
Show Morningstar-style trailing returns for a symbol.
|
||||
|
||||
```
|
||||
Usage: zfin perf <SYMBOL>
|
||||
```
|
||||
|
||||
`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)
|
||||
49
docs/reference/cli/portfolio.md
Normal file
49
docs/reference/cli/portfolio.md
Normal file
|
|
@ -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)
|
||||
64
docs/reference/cli/projections.md
Normal file
64
docs/reference/cli/projections.md
Normal file
|
|
@ -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 <DATE>` | 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 <DATE>` | 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 <PATH>` | 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)
|
||||
46
docs/reference/cli/quote.md
Normal file
46
docs/reference/cli/quote.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# `zfin quote`
|
||||
|
||||
Show the latest quote for a symbol, with a price chart and recent
|
||||
history.
|
||||
|
||||
```
|
||||
Usage: zfin quote <SYMBOL>
|
||||
```
|
||||
|
||||
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 <PATH>` 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)
|
||||
52
docs/reference/cli/review.md
Normal file
52
docs/reference/cli/review.md
Normal file
|
|
@ -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)
|
||||
49
docs/reference/cli/snapshot.md
Normal file
49
docs/reference/cli/snapshot.md
Normal file
|
|
@ -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/<as_of_date>-portfolio.srf` atomically.
|
||||
The file is a discriminated SRF whose records start with
|
||||
`kind::<meta|total|tax_type|account|lot>`.
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Effect |
|
||||
|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--dry-run` | Compute and print to stdout; write nothing. |
|
||||
| `--force` | Overwrite an existing snapshot for the date. |
|
||||
| `--out <path>` | Override the output path. |
|
||||
| `--as-of <DATE>` | 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/<today>-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)
|
||||
41
docs/reference/cli/splits.md
Normal file
41
docs/reference/cli/splits.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# `zfin splits`
|
||||
|
||||
Show stock-split history for a symbol.
|
||||
|
||||
```
|
||||
Usage: zfin splits <SYMBOL>
|
||||
```
|
||||
|
||||
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)
|
||||
36
docs/reference/cli/version.md
Normal file
36
docs/reference/cli/version.md
Normal file
|
|
@ -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)
|
||||
106
docs/reference/config/accounts-srf.md
Normal file
106
docs/reference/config/accounts-srf.md
Normal file
|
|
@ -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)
|
||||
68
docs/reference/config/environment.md
Normal file
68
docs/reference/config/environment.md
Normal file
|
|
@ -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)
|
||||
72
docs/reference/config/keys-srf.md
Normal file
72
docs/reference/config/keys-srf.md
Normal file
|
|
@ -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::<tab>` (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)
|
||||
87
docs/reference/config/metadata-srf.md
Normal file
87
docs/reference/config/metadata-srf.md
Normal file
|
|
@ -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 `"<geo> <asset_class>"` -> `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)
|
||||
159
docs/reference/config/portfolio-srf.md
Normal file
159
docs/reference/config/portfolio-srf.md
Normal file
|
|
@ -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)
|
||||
142
docs/reference/config/projections-srf.md
Normal file
142
docs/reference/config/projections-srf.md
Normal file
|
|
@ -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)
|
||||
60
docs/reference/config/theme-srf.md
Normal file
60
docs/reference/config/theme-srf.md
Normal file
|
|
@ -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)
|
||||
68
docs/reference/config/transaction-log-srf.md
Normal file
68
docs/reference/config/transaction-log-srf.md
Normal file
|
|
@ -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)
|
||||
45
docs/reference/config/watchlist-srf.md
Normal file
45
docs/reference/config/watchlist-srf.md
Normal file
|
|
@ -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)
|
||||
70
docs/reference/providers.md
Normal file
70
docs/reference/providers.md
Normal file
|
|
@ -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 | <https://tiingo.com> | The one key worth setting first -- it's the primary price source. |
|
||||
| Polygon | <https://polygon.io> | Enables total return (dividends) and forward-looking dividend dates. |
|
||||
| FMP | <https://financialmodelingprep.com> | Earnings actuals + estimates. |
|
||||
| TwelveData | <https://twelvedata.com> | Optional quote fallback. |
|
||||
| OpenFIGI | <https://www.openfigi.com/api> | 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.
|
||||
76
docs/reference/tui.md
Normal file
76
docs/reference/tui.md
Normal file
|
|
@ -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)
|
||||
Loading…
Add table
Reference in a new issue