345 lines
13 KiB
Markdown
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
|