zfin/README.md

345 lines
13 KiB
Markdown

# zfin
A financial data library, CLI, and terminal UI written in Zig. Tracks portfolios, analyzes trailing returns, displays options chains, earnings history, and more -- all from the terminal.
## Quick start
```bash
# Set at least one API key (see "API keys" below)
export TWELVEDATA_API_KEY=your_key
# Build
zig build
# CLI usage
zig build run -- perf VTI # trailing returns
zig build run -- quote AAPL # real-time quote
zig build run -- options AAPL # options chains
zig build run -- earnings MSFT # earnings history
# Interactive TUI
zig build run -- i # auto-loads portfolio.srf from cwd
zig build run -- i -p portfolio.srf -w watchlist.srf
zig build run -- i -s AAPL # start with a symbol, no portfolio
```
Requires Zig 0.15.2 or later.
## 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
| Data type | Provider | Auth | Free-tier limit | Cache TTL |
|---|---|---|---|---|
| Daily candles (OHLCV) | TwelveData | `TWELVEDATA_API_KEY` | 8 req/min, 800/day | 24 hours |
| Real-time quotes | TwelveData | `TWELVEDATA_API_KEY` | 8 req/min, 800/day | Never cached |
| Dividends | Polygon | `POLYGON_API_KEY` | 5 req/min | 7 days |
| Splits | Polygon | `POLYGON_API_KEY` | 5 req/min | 7 days |
| Options chains | CBOE | None required | ~30 req/min (self-imposed) | 1 hour |
| Earnings | Finnhub | `FINNHUB_API_KEY` | 60 req/min | 24 hours |
| ETF profiles | Alpha Vantage | `ALPHAVANTAGE_API_KEY` | 25 req/day | 30 days |
### TwelveData
**Used for:** daily candles and real-time quotes.
- Endpoint: `https://api.twelvedata.com/time_series` and `/quote`
- Free tier: 8 API credits per minute, 800 per day. Each symbol in a request costs 1 credit (batch requests do NOT reduce credit cost).
- Candles are fetched with a 10-year + 60-day lookback window for trailing return calculations.
- Returns split-adjusted but NOT dividend-adjusted prices. Total returns are computed separately using Polygon dividend data.
- Quotes are never cached (always a live fetch, ~15 min delay on free tier).
### Polygon
**Used for:** dividend history and stock splits.
- Endpoints: `https://api.polygon.io/v3/reference/dividends` and `/v3/reference/splits`
- Free tier: 5 requests per minute, unlimited daily. Full historical dividend/split data.
- Dividend endpoint uses cursor-based pagination (automatically followed).
- Provides dividend type classification (regular, special, supplemental).
### 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.
### Finnhub
**Used for:** earnings calendar (historical and upcoming).
- Endpoint: `https://finnhub.io/api/v1/calendar/earnings`
- Free tier: 60 requests per minute.
- Fetches 5 years back and 1 year forward from today.
- Note: Finnhub requires TLS 1.2. Since Zig's HTTP client only supports TLS 1.3, requests to Finnhub automatically fall back to system `curl`.
### Alpha Vantage
**Used for:** ETF profiles (expense ratio, holdings, sector weights).
- Endpoint: `https://www.alphavantage.co/query?function=ETF_PROFILE`
- Free tier: 25 requests per day. Used sparingly -- ETF profiles rarely change.
## API keys
Set keys as environment variables or in a `.env` file (searched in the executable's parent directory, then cwd):
```bash
TWELVEDATA_API_KEY=your_key # Required for candles and quotes
POLYGON_API_KEY=your_key # Required for dividends/splits (total returns)
FINNHUB_API_KEY=your_key # Required for earnings data
ALPHAVANTAGE_API_KEY=your_key # Required for ETF profiles
```
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 |
|---|---|
| `TWELVEDATA_API_KEY` | No candles, quotes, or trailing returns |
| `POLYGON_API_KEY` | No dividends -- trailing returns show price-only (no total return) |
| `FINNHUB_API_KEY` | No earnings data (tab disabled) |
| `ALPHAVANTAGE_API_KEY` | No ETF profiles |
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 | 24 hours | Only changes once per trading day |
| Dividends | 7 days | Declared well in advance |
| Splits | 7 days | Rare corporate events |
| Options | 1 hour | Prices change continuously during market hours |
| Earnings | 24 hours | Quarterly events, estimates update periodically |
| ETF profiles | 30 days | Holdings/weights change slowly |
| Quotes | Never cached | Intended for live price checks |
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 |
|---|---|
| TwelveData | 8/minute |
| Polygon | 5/minute |
| Finnhub | 60/minute |
| CBOE | 30/minute |
| Alpha Vantage | 25/day |
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
cache stats Show cached symbols
cache clear Delete all cached data
interactive, i Launch interactive TUI
help Show usage
```
### 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 five tabs: Portfolio, Quote, Performance, Options, and Earnings.
### 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.
**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) when Polygon data is available. Also shows risk metrics (volatility, Sharpe ratio, max drawdown).
**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.
**Earnings** -- historical and upcoming earnings events with EPS estimate/actual, surprise amount and percentage. Future events are dimmed. Tab is disabled for ETFs.
### Keybindings
All keybindings are configurable via `~/.config/zfin/keys.srf`. Generate the default config:
```bash
zfin i --default-keys > ~/.config/zfin/keys.srf
```
Default keybindings:
| Key | Action |
|---|---|
| `q`, `Ctrl+c` | Quit |
| `r`, `F5` | Refresh current tab (invalidates cache) |
| `h`, Left | Previous tab |
| `l`, Right, Tab | Next tab |
| `1`-`5` | Jump to tab |
| `j`, Down | Select next row |
| `k`, Up | Select previous row |
| `Enter` | Expand/collapse (positions, expirations, calls/puts) |
| `s` | Select symbol from portfolio for other tabs |
| `/` | Enter symbol search |
| `e` | Edit portfolio/watchlist in `$EDITOR` |
| `c` | Toggle all calls collapsed/expanded (options tab) |
| `p` | Toggle all puts collapsed/expanded (options tab) |
| `Ctrl+1`-`Ctrl+9` | Set options near-the-money filter to +/- N strikes |
| `g` | Scroll to top |
| `G` | Scroll to bottom |
| `Ctrl+d` | Half-page down |
| `Ctrl+u` | Half-page up |
| `PageDown` | Page down |
| `PageUp` | Page up |
| `?` | Help screen |
Mouse: scroll wheel navigates, left-click selects rows and switches tabs, double-click expands/collapses.
### 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 files with one lot per line:
```
#!srfv1
symbol::VTI,shares:num:100,open_date::2024-01-15,open_price:num:220.50
symbol::AAPL,shares:num:50,open_date::2024-03-01,open_price:num:170.00
symbol::AAPL,shares:num:25,open_date::2023-06-15,open_price:num:155.00,account::Roth IRA
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
```
### Lot fields
| Field | Type | Required | Description |
|---|---|---|---|
| `symbol` | string | Yes | Ticker symbol |
| `shares` | number | Yes | Number of shares |
| `open_date` | string | Yes | Purchase date (YYYY-MM-DD) |
| `open_price` | number | Yes | Purchase price per share |
| `close_date` | string | No | Sale date (null = open lot) |
| `close_price` | number | No | Sale price per share |
| `note` | string | No | Tag or note |
| `account` | string | No | Account name (e.g. "Roth IRA", "Brokerage") |
Open lots (no `close_date`) contribute to positions. Closed lots (with `close_date` and `close_price`) show realized P&L. The `account` field is displayed in the lot detail view when a position is expanded.
### Watchlist format
A watchlist is an SRF file with just symbol fields:
```
#!srfv1
symbol::NVDA
symbol::TSLA
symbol::GOOG
```
Watchlist symbols appear at the bottom of the portfolio tab. They show the latest cached price but no position data. Press Enter or double-click to jump to the Quote tab for that symbol.
## Architecture
```
src/
root.zig Library root, exports all public types
config.zig Configuration from env vars / .env files
service.zig DataService: cache-check -> fetch -> cache -> return
models/
candle.zig OHLCV price bars
date.zig Date type with arithmetic, snapping, formatting
dividend.zig Dividend records with type classification
split.zig Stock splits
option.zig Option contracts and chains
earnings.zig Earnings events with surprise calculation
etf_profile.zig ETF profiles with holdings and sectors
portfolio.zig Lots, positions, and portfolio aggregation
quote.zig Real-time quote data
ticker_info.zig Security metadata
providers/
provider.zig Type-erased provider interface (vtable)
twelvedata.zig TwelveData: candles, quotes
polygon.zig Polygon: dividends, splits
finnhub.zig Finnhub: earnings
cboe.zig CBOE: options chains (no API key)
alphavantage.zig Alpha Vantage: ETF profiles
analytics/
performance.zig Trailing returns (as-of-date + month-end)
risk.zig Volatility, Sharpe, drawdown, portfolio summary
cache/
store.zig SRF file cache with TTL freshness checks
net/
http.zig HTTP client with retries and TLS 1.2 fallback
rate_limiter.zig Token-bucket rate limiter
cli/
main.zig CLI entry point and all commands
tui/
main.zig Interactive TUI application
keybinds.zig Configurable keybinding system
theme.zig Configurable color theme
```
### Dependencies
| Dependency | Source | Purpose |
|---|---|---|
| [SRF](https://github.com/lobo/srf) | Local (`../../srf`) | Cache file format and portfolio/watchlist parsing |
| [libvaxis](https://github.com/rockorager/libvaxis) | Git (v0.5.1) | Terminal UI rendering |
## Building
```bash
zig build # build the zfin binary
zig build test # run all tests
zig build run -- <args> # build and run
```
The compiled binary is at `zig-out/bin/zfin`.
## License
MIT