ETF research and portfolio analytics terminal
Project description
etfray
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.
Why etfray?
- 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.
Features
- Home Dashboard — Live startup screen with benchmark marquee (SPY/QQQ/AGG/GLD YTD), watchlist snapshot, ETF daily movers (top-5 gainers/losers), seasonal spotlight for the current month, and recent quick-jump pills
- 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, margin/leverage monitoring, and stress scenarios
- Side-by-side Compare — Compare multiple ETFs across holdings, exposure, overlap, fees, and 52-week returns 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
Home Dashboard |
Seasonals Chart |
ETF Holdings |
Watchlist |
Side-by-side Compare |
Portfolio ETF Lookthrough |
Key Capabilities
| 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 & screener), IBKR TWS |
| Home Dashboard | Live benchmark marquee, ETF movers (gainers/losers), watchlist snapshot, seasonal spotlight, recent quick-jump |
| 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; seasonal spotlight on home screen |
| Exposure | Sector and geographic exposure from underlying holdings |
| Concentration | Top-N analysis (top 10, 25, 50) with cumulative weight; Jaccard pairwise overlap scoring at portfolio level |
| Compare | Side-by-side comparison of 2–5 ETFs with weight-adjusted overlap and 52-week return columns |
| Watchlist | Track ETFs with concentration metrics, sector breakdown, and portfolio overlap |
| Portfolio | Real-time positions, lookthrough exposure, margin & leverage, stress scenarios (−10%/−20%) |
| Storage | Local SQLite — no cloud, no external databases |
| Freshness | Configurable staleness thresholds (default: 30 days fresh, 90 days acceptable) |
Usage Examples
Home Dashboard
The Home screen loads automatically at startup with four live panels:
- Benchmark Marquee — Scrolling YTD return bar for SPY, QQQ, AGG, GLD. Click Refresh to force a fresh fetch.
- Watchlist Snapshot — Compact table of your tracked ETFs with YTD, Top-10 Weight, Effective N, HHI, and Top Sector. Double-click any row to open that ETF.
- ETF Movers — Top-5 daily gainers and losers. A yellow "Last session" label appears when market data is stale (outside trading hours). Double-click to open an ETF. Click Refresh for the latest data.
- Seasonal Spotlight — Current-month win rate and MTD return for each watchlist ticker (e.g.,
SPY ↑9/15 yrs +1.2% MTD).
Recent ETFs appear as quick-jump pill buttons below the panels.
Research an ETF
- 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
View seasonals
- 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
Manage your watchlist
- 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
Monitor your portfolio
- 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
Architecture
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.
Configuration
All settings are managed via Workspace → Settings in the sidebar and stored in ~/.etfray/data.db.
| Setting | Default | Description |
|---|---|---|
ibkr_host |
127.0.0.1 |
IBKR TWS/Gateway host address |
ibkr_port |
7497 |
IBKR TWS/Gateway API port |
ibkr_client_id |
1 |
Client ID for the IBKR API connection |
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 |
freshness_days_acceptable |
90 |
Days before cached data is considered stale and re-fetched |
margin_warning_cushion |
0.15 |
Margin cushion threshold for warnings |
leverage_warning |
2.0 |
Leverage ratio warning threshold |
cache_dir |
~/.etfray/cache |
Directory for SEC series/class lookup cache files |
export_dir |
~/.etfray/exports |
Directory where CSV/JSON exports are saved |
See the full configuration reference for all options.
Installation
pip install etfray
Requires 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.
Quick Start
etfray
Use the sidebar tree to navigate between Research and Portfolio workspaces. Press ctrl+p to open the command palette.
IBKR Connection
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.
Documentation
Full documentation at etfray.readthedocs.io:
Development
git clone https://github.com/alwank/etfray.git
cd etfray
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev,docs]"
pytest
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file etfray-1.5.0.tar.gz.
File metadata
- Download URL: etfray-1.5.0.tar.gz
- Upload date:
- Size: 12.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c43ef0eb0ceefff66612e9b115a59a29a88732f2480c4d48ef28ed94f0c965e
|
|
| MD5 |
7ece73bbb973b0496e51839562fda70b
|
|
| BLAKE2b-256 |
67adee2f59c95cd0b95c3421f09cc94e88d09d3bf4da7d253e0d45ce059b0c4a
|
Provenance
The following attestation bundles were made for etfray-1.5.0.tar.gz:
Publisher:
publish.yml on alwank/etfray
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etfray-1.5.0.tar.gz -
Subject digest:
9c43ef0eb0ceefff66612e9b115a59a29a88732f2480c4d48ef28ed94f0c965e - Sigstore transparency entry: 1630385840
- Sigstore integration time:
-
Permalink:
alwank/etfray@667beb99e12beadcf0100a474a582f3516cbb004 -
Branch / Tag:
refs/tags/v1.5.0 - Owner: https://github.com/alwank
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@667beb99e12beadcf0100a474a582f3516cbb004 -
Trigger Event:
release
-
Statement type:
File details
Details for the file etfray-1.5.0-py3-none-any.whl.
File metadata
- Download URL: etfray-1.5.0-py3-none-any.whl
- Upload date:
- Size: 192.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac8a805abd9694f89a6c6314ea633bbd4090783c444acbe073f63ab683e8652e
|
|
| MD5 |
13dcafa10ee2762b4abc70e40320e46e
|
|
| BLAKE2b-256 |
af0d8f3a78e31838349cd974127375db20217edce68649639adb24612e2d3091
|
Provenance
The following attestation bundles were made for etfray-1.5.0-py3-none-any.whl:
Publisher:
publish.yml on alwank/etfray
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etfray-1.5.0-py3-none-any.whl -
Subject digest:
ac8a805abd9694f89a6c6314ea633bbd4090783c444acbe073f63ab683e8652e - Sigstore transparency entry: 1630385861
- Sigstore integration time:
-
Permalink:
alwank/etfray@667beb99e12beadcf0100a474a582f3516cbb004 -
Branch / Tag:
refs/tags/v1.5.0 - Owner: https://github.com/alwank
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@667beb99e12beadcf0100a474a582f3516cbb004 -
Trigger Event:
release
-
Statement type: