A terminal-based ETF research and portfolio analytics application built with Textual.
etfray converts SEC fund filings and IBKR portfolio data into holdings, exposure, concentration, margin, and risk workflows — all from your terminal.
- No cloud accounts — No sign-ups, no API keys to manage, no third-party dashboards. Your data stays on your machine.
- No subscriptions — ETF holdings data comes directly from SEC EDGAR filings. Free, authoritative, and always available.
- Keyboard-first — Designed for speed. Command palette, tree navigation, and keybindings — no mouse required.
- ETF Research — Search ETFs, view holdings, sector/geographic exposure, concentration, fees, risk metrics, and SEC documents via EDGAR
- Seasonals — TradingView-style seasonals chart with year-over-year cumulative returns, period returns table (1W to Max), and year range selection
- Fund Overview — Rich fund profile combining SEC filings with Yahoo Finance metadata (category, expense ratio, dividend yield, beta, returns, description)
- Watchlist — Track ETFs with at-a-glance metrics: concentration, top sectors, overlap vs portfolio, and data freshness
- Portfolio Analytics — Connect to IBKR TWS/Gateway for live positions, lookthrough exposure, concentration analysis, and margin/leverage monitoring
- Side-by-side Compare — Compare multiple ETFs across holdings, exposure, and fees in a single view
- Export — Save any view to CSV or JSON for further analysis
- Keyboard-first — Full TUI with command palette, tree navigation, and keybindings
- Local & private — All data cached locally in SQLite; no cloud accounts required
| Capability | Details |
|---|---|
| ETF coverage | Thousands of ETFs via SEC EDGAR N-PORT filings |
| Data sources | EDGAR (official), alternative web scraper, Yahoo Finance (metadata & price history), IBKR TWS |
| Holdings analysis | Full position-level breakdown with weight, value, shares |
| Fund metadata | Category, expense ratio, dividend yield, beta, inception date, returns via yfinance |
| Seasonals | Year-over-year cumulative return chart with matplotlib or plotext rendering |
| Exposure | Sector and geographic exposure from underlying holdings |
| Concentration | Top-N analysis (top 10, 25, 50) with cumulative weight |
| Watchlist | Track ETFs with concentration metrics, sector breakdown, and portfolio overlap |
| Portfolio | Real-time positions, lookthrough exposure, margin & leverage |
| Storage | Local SQLite — no cloud, no external databases |
| Freshness | Configurable staleness thresholds (default: 30 days fresh, 90 days acceptable) |
- Launch
etfrayand navigate to Research → Search in the sidebar - Press
/to open ETF Search, type a ticker (e.g.,VTI), and press Enter - Browse tabs: Overview → Seasonals → Holdings → Exposure → Concentration → Risk
- Press
wto add the ETF to your watchlist
- Search for an ETF (e.g.,
SPY) - Press
tto jump to the Seasonals view - Select year range to compare seasonal patterns across years
- Review the period returns table for standard return intervals
- Navigate to Workspace → Watchlist in the sidebar
- Click Add ticker to search and add ETFs
- View concentration, sector, and overlap metrics at a glance
- Double-click any row to open that ETF's research view
- Ensure IBKR TWS/Gateway is running with API enabled on port 7497
- Navigate to Portfolio → Positions in the sidebar
- etfray connects lazily — positions load automatically on first access
- Switch to Lookthrough to see aggregated exposure across all your ETF holdings
- Check Margin for leverage ratio and margin cushion warnings
graph LR
A[SEC EDGAR API] --> C[Data Services]
B[Web Scraper] --> C
Y[Yahoo Finance] --> C
D[IBKR TWS API] --> C
C --> E[(SQLite Cache)]
E --> F[Domain Analytics]
F --> G[Textual TUI]
Design principles:
- Local-first — All data cached in SQLite. Works offline after initial fetch.
- Source provenance — Every data point tracks its origin and fetch date so you know how fresh it is.
- Lazy connection — IBKR connects only when portfolio views are accessed, not at startup.
- Separation of concerns —
data/handles I/O,domain/handles computation,ui/handles presentation.
All settings are managed via Workspace → Settings in the sidebar and stored in ~/.etfray/data.db.
| Setting | Default | Description |
|---|---|---|
ibkr_port |
7497 |
IBKR TWS/Gateway API port |
edgar_identity |
(empty) | Your email — required by SEC fair use policy |
data_source |
auto |
Holdings source: auto, edgar, or web |
freshness_days_fresh |
30 |
Days before cached data is no longer considered fresh |
margin_warning_cushion |
0.15 |
Margin cushion threshold for warnings |
See the full configuration reference for all options.
pip install etfrayRequires Python 3.11+.
Seasonals chart (optional): For a matplotlib seasonals chart in the Seasonals tab:
pip install etfray[charts]
# or from source:
pip install -e ".[charts]"Verify dependencies: python scripts/check_charts.py (should report Chart: image (matplotlib) and True).
Terminal image support is required for a crisp chart (not blocky ASCII). Enable one of:
- Cursor / VS Code: Settings →
terminal.integrated.enableImages→true, then restart the terminal - iTerm2, Kitty, WezTerm, or Windows Terminal 1.22+ (recommended)
Without [charts] or without image support, etfray uses an ASCII plotext chart and shows the active mode in the Seasonals summary line.
Blurry chart? If the summary says Chart: image (halfcell) or (unicode), the terminal is using a low-resolution block renderer. For a sharp chart, run etfray in iTerm2 or Kitty, or enable Cursor terminal.integrated.enableImages and restart the terminal. Check python scripts/check_charts.py for protocol: sixel or tgp.
etfrayUse the sidebar tree to navigate between Research and Portfolio workspaces. Press ctrl+p to open the command palette.
To use portfolio analytics, you need IBKR TWS or IB Gateway running with API connections enabled (default port 7497).
Configure the connection in Workspace → Settings in the sidebar.
Full documentation at etfray.readthedocs.io:
git clone https://github.com/alwank/etfray.git
cd etfray
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev,docs]"
pytest



