SKILL DETAIL
etf-premium
himself65/finance-skills/etf-premium
Calculate ETF premium/discount vs NAV via Yahoo Finance, and decompose single-day surges into NAV-driven vs structural components (gamma squeeze, dealer hedging, blocked AP arbitrage). Use whenever the user asks about an ETF's premium or discount, NAV comparison, why an ETF diverged from its holdings, or how much of a move is dealer-hedging-driven. Triggers: "ETF premium", "ETF discount", "NAV premium", "is SPY at a premium", "BITO premium", "IBIT premium", "bond ETF discount", "trading above/below NAV", "ETF premium screener", "biggest discount", "compare ETF NAV", "ETF arbitrage", "ETF gamma squeeze", "ETF premium surge", "decompose ETF move", "dealer gamma exposure", "GEX for ETF", "why did this ETF jump", "premium convergence", "AP arbitrage blocked", or any request about the gap between an ETF's price and underlying value. Especially relevant for leveraged, inverse, international, bond, commodity, and crypto ETFs.
Installation
npx skills add https://github.com/himself65/finance-skills --skill etf-premium
Skill files
SKILL.md
Last synced · Sep 6, 2026
README.md›
# ETF Premium/Discount Analysis
Calculate the premium or discount of an ETF's market price relative to its Net Asset Value (NAV).
## When it triggers
- "Is SPY trading at a premium?"
- "AGG premium to NAV"
- "Compare bond ETF discounts"
- "Which ETFs have the biggest discount right now?"
- "Why is BITO at a premium?"
- "ETF premium screener"
- "Why did this ETF jump 13% when its holdings only moved 7%?"
- "Is the rally driven by dealer gamma hedging?"
- "How long until the premium converges?"
- Any request involving ETF market price vs underlying NAV, or decomposing a sudden ETF surge
## What it does
1. Fetches the ETF's current market price and NAV from Yahoo Finance
2. Calculates `(Price - NAV) / NAV × 100` to get the premium/discount percentage
3. Provides context: is this deviation normal for this ETF category?
4. Compares against bid-ask spread to filter out market microstructure noise
5. Supports single ETF analysis, multi-ETF comparison, screener mode, and **gamma-squeeze decomposition** (split a surge into NAV-driven vs structural components, quantify dealer gamma exposure, and assess convergence timeline)
## Platform
**CLI agents only** (Claude Code, Codex, etc.) — requires Python and yfinance.
## Setup
No setup required. The skill auto-installs yfinance if needed.
## Sub-skills
| Sub-skill | Description |
|---|---|
| Single ETF Snapshot | Current premium/discount for one ETF with interpretation |
| Multi-ETF Comparison | Side-by-side comparison ranked by premium/discount |
| Premium Screener | Scan 60+ common ETFs to find extreme premiums/discounts |
| Premium Deep Dive | Full analysis with volatility, liquidity, and causal explanation |
| Premium Surge Decomposition | Decompose a single-day surge into NAV-driven vs excess premium, quantify dealer gamma exposure (GEX) from the options chain, and assess hours/days/weeks convergence timeline |
## Reference files
- `references/etf_premium_reference.md` — Detailed formulas, category benchmarks, ETF universe, creation/redemption mechanics
- `references/gamma_squeeze_reference.md` — Premium decomposition framework, Black-Scholes gamma + GEX formulas with sign conventions, convergence-timeline mechanics, and gamma-squeeze diagnostic table
references/etf_premium_reference.md›
# ETF Premium/Discount Reference
## Core Formula
```
Premium/Discount (%) = (Market Price - NAV) / NAV × 100
```
Where:
- **Market Price** = the price at which the ETF is currently trading on the exchange
- **NAV** (Net Asset Value) = the per-share value of the ETF's underlying holdings, calculated by the fund at end of day
A **positive** value means the ETF trades at a **premium** (more expensive than underlying assets).
A **negative** value means the ETF trades at a **discount** (cheaper than underlying assets).
---
## How ETF Premiums and Discounts Work
### The Creation/Redemption Mechanism
ETFs maintain price alignment with NAV through authorized participants (APs) — large institutional players (banks, broker-dealers) who can:
1. **Create shares**: Buy the underlying basket of securities, deliver them to the ETF issuer, and receive new ETF shares. This increases supply and pushes the price down toward NAV.
2. **Redeem shares**: Return ETF shares to the issuer and receive the underlying basket. This reduces supply and pushes the price up toward NAV.
This arbitrage mechanism keeps most liquid ETFs within a few basis points of NAV. When it breaks down — due to illiquidity, market stress, or structural constraints — premiums and discounts appear.
### Why the Mechanism Can Fail
| Cause | Effect | ETF Types Affected |
|---|---|---|
| Underlying market closed | Price reflects expectations, NAV is stale | International (EEM, VWO, KWEB) |
| Underlying assets illiquid | APs can't efficiently create/redeem | Bond (HYG, JNK, EMB), Small-cap |
| Market stress / volatility | APs widen spreads or step back | All types, especially credit |
| Regulatory constraints | Creation units restricted | Crypto (IBIT, BITO) early days |
| Futures contango/backwardation | NAV drag from roll costs | Commodity (USO, UNG) |
| Daily leverage reset | Compounding creates tracking error | Leveraged (TQQQ, SQQQ) |
| Retail demand surge | Buying pressure exceeds AP capacity | Thematic (ARKK), new launches |
---
## Data Source: yfinance
### Key Fields
| Field | Description | Notes |
|---|---|---|
| `navPrice` | Most recent official NAV per share | Updated daily at market close |
| `regularMarketPrice` | Current/last trading price | May be delayed 15 min |
| `previousClose` | Prior day closing price | Use as fallback for price |
| `totalAssets` | Total fund AUM in dollars | Not per-share |
| `netExpenseRatio` | Annual expense ratio (decimal) | e.g., 0.03 = 0.03% |
| `category` | Morningstar category | e.g., "Intermediate Core Bond" |
| `fundFamily` | ETF issuer | e.g., "iShares", "Vanguard" |
| `quoteType` | Security type | Must be "ETF" |
| `bid` / `ask` | Current bid and ask prices | For spread calculation |
| `averageVolume` | Average daily volume | Liquidity indicator |
| `yield` | Distribution yield (decimal) | e.g., 0.039 = 3.9% |
### Limitations
- **No historical NAV**: yfinance only provides the most recent `navPrice`. You cannot build a time series of premiums/discounts from yfinance alone.
- **NAV timing**: The `navPrice` reflects end-of-day calculation. During trading hours, the market price moves but NAV is static until the next calculation.
- **Not all tickers**: Some very new or obscure ETFs may not have `navPrice` populated.
- **Delay**: Market prices may be delayed 15 minutes for some exchanges.
---
## Category-Specific Benchmarks
### What's "Normal" Premium/Discount by Category
| Category | Typical Range | Explanation |
|---|---|---|
| US Large-Cap Equity (SPY, QQQ, VOO) | ±0.01% to ±0.05% | Extremely liquid, tight arbitrage |
| US Mid/Small-Cap (IWM, IJR) | ±0.02% to ±0.10% | Slightly wider due to smaller underlying stocks |
| US Bond - Investment Grade (AGG, BND, LQD) | ±0.05% to ±0.30% | Bond market less liquid than equities |
| US Bond - High Yield (HYG, JNK) | ±0.10% to ±0.50% | Corporate bonds can be very illiquid |
| EM Bonds (EMB) | ±0.20% to ±1.0% | Illiquid underlyings + time-zone issues |
| International Equity (EFA, EEM, VWO) | ±0.10% to ±0.50% | Time-zone mismatch when US trades but foreign markets closed |
| China/EM Single-Country (KWEB, FXI, INDA) | ±0.15% to ±0.80% | Capital controls, ADR conversion, and time-zone effects |
| Commodity (GLD, SLV, IAU) | ±0.05% to ±0.20% | Physical backing is straightforward but has storage costs |
| Futures-Based Commodity (USO, UNG) | ±0.20% to ±1.0% | Contango/backwardation and roll mechanics |
| Crypto (IBIT, BITO, FBTC) | ±0.50% to ±3.0% | Young market, high demand, AP mechanics still developing |
| Leveraged/Inverse (TQQQ, SQQQ) | ±0.20% to ±1.5% | Daily reset, compounding effects, and swap counterparty risk |
| Thematic/Active (ARKK, JEPI) | ±0.10% to ±0.50% | Varies with popularity and underlying liquidity |
### Stress Scenarios
During market stress (e.g., March 2020 COVID crash, 2022 bond rout), discounts can widen dramatically:
- Bond ETFs saw discounts of 3-5% during March 2020
- High-yield ETFs (HYG, JNK) hit 5%+ discounts
- International ETFs can gap to 2-3% premiums/discounts during geopolitical events
---
## Common ETF Universe for Screening
### Tier 1: Core Liquid ETFs (good for baseline comparison)
```
SPY, QQQ, IVV, VOO, VTI, DIA, IWM
AGG, BND, TLT, HYG, LQD
EFA, EEM, VWO
GLD, SLV
```
### Tier 2: Category Leaders
```
# Bond
VCIT, VCSH, BNDX, EMB, JNK, MUB, TIP, GOVT, SHY, IEF
# International
IEMG, KWEB, FXI, INDA, VEA, MCHI, EWZ, EWJ
# Commodity
USO, UNG, DBC, IAU, PDBC, GSG, WEAT, CORN
# Crypto
IBIT, BITO, FBTC, ETHA, ARKB, GBTC
# Leveraged/Inverse
TQQQ, SQQQ, SPXU, UPRO, JNUG, JDST, SOXL, SOXS
# Sector
XLF, XLE, XLK, XLV, XLI, XLP, XLU, XLRE, XLC, XLB, XLY
# Sector - Semis/Tech (often show large premiums/discounts)
SOXX, SMH, IGV, XSD
# Sector - Healthcare (frequently discounted during volatility)
XBI, IBB, IHI
# Income / Dividend
JEPI, JEPQ, SCHD, VYM, DVY, DIVO, HDV, QYLD
# Thematic / Active (prone to large premiums/discounts due to illiquid underlyings)
ARKK, ARKW, ARKG, HACK, CLOU, WCLD, BUG, BOTZ, ROBO, LIT, TAN, ICLN
```
### Tier 3: Peer Comparison Groups
When analyzing a single ETF, compare it to peers in the same category. This helps distinguish ETF-specific deviations from market-wide patterns.
```
Digital Assets: IBIT, BITO, FBTC, ETHA, ARKB, GBTC
Intermediate Core Bond: AGG, BND, SCHZ
High Yield Bond: HYG, JNK, USHY
Long Government: TLT, VGLT, SPTL
EM Bond: EMB, VWOB, PCY
Large Growth: QQQ, VUG, IWF, SCHG
Large Blend: SPY, VOO, IVV, VTI
Commodities: GLD, IAU, SLV, DBC
China Region: KWEB, FXI, MCHI
Leveraged Bull: TQQQ, UPRO, SOXL, JNUG
Leveraged Bear: SQQQ, SPXU, SOXS, JDST
Derivative Income: JEPI, JEPQ, QYLD
Large Value/Dividend: SCHD, VYM, DVY, HDV
```
---
## Bid-Ask Spread as a Reality Check
A premium/discount that is smaller than the bid-ask spread is not economically meaningful — it's just the cost of trading. Always compare:
```
If |Premium%| < Bid-Ask Spread%:
→ The premium/discount is within market microstructure noise
→ Not actionable
If |Premium%| > Bid-Ask Spread%:
→ The premium/discount represents a real deviation from NAV
→ Worth investigating further
```
---
## Historical Context (Cannot Be Computed from yfinance Alone)
For historical premium/discount analysis, users would need:
- **ETF issuer websites**: iShares, Vanguard, SPDR publish historical premium/discount data for their funds
- **Bloomberg Terminal**: Gold standard for historical NAV time series
- **SEC N-PORT filings**: Contain NAV data but lag by ~60 days
- **SSGA website**: Publishes daily premium/discount history with downloadable Excel files for SPDR ETFs
The skill focuses on **current snapshot** analysis since yfinance provides only the most recent NAV.
references/gamma_squeeze_reference.md›
# ETF Gamma Squeeze & Premium Surge Reference
This document supports **Sub-Skill E** in `SKILL.md`. It covers:
1. The premium-decomposition framework (NAV vs excess)
2. Dealer gamma exposure (GEX) — formula, conventions, and worked example
3. The convergence-timeline framework (hours / days / weeks)
4. Risk indicators that distinguish a real gamma squeeze from a routine rally
---
## 1. Premium Decomposition Framework
When an ETF moves much more than its underlying basket in a single session, the move can be decomposed into two parts:
```
ETF return = NAV-driven return + Excess premium return
```
Where:
- **NAV-driven return** = weighted return of the ETF's holdings, computed from observable underlying prices
- **Excess premium return** = the residual; reflects supply/demand imbalance unmet by AP arbitrage
### Why the residual exists
The AP arbitrage mechanism keeps ETF price ≈ NAV under normal conditions. The residual appears when arbitrage is impeded:
| Source of residual | Mechanism | Typical signature |
|---|---|---|
| Underlying market closed | APs cannot transact in basket securities | International ETFs during US-only hours |
| Options dealer gamma hedging | Dealers short gamma must buy on rallies | Heavy call OI, IV spike, single strike concentration |
| Creation unit cap reached | Issuer limits new share creation | Crypto ETFs at launch; specialty ETFs in surge |
| Sentiment/retail flow surge | Buying pressure outpaces AP capacity | Thematic / meme ETFs in news cycles |
| Underlying basket illiquid | APs cannot price/source basket reliably | EM bond, credit, frontier market ETFs |
### How to estimate NAV return when end-of-day NAV isn't published yet
`yfinance` only exposes the most recent end-of-day `navPrice`. For an intraday or just-closed-day decomposition, estimate NAV change from the holdings:
```
NAV_return ≈ Σ (weight_i × return_i) / Σ weight_i
```
Sources of holdings weights:
1. `yf.Ticker(...).funds_data.top_holdings` — works for many US-listed ETFs but is incomplete
2. ETF issuer holdings page (iShares, SPDR, Invesco) — most authoritative
3. User-supplied weights — for niche or international ETFs
When the underlying market is closed during the ETF's session:
- Substitute ADRs (e.g., for Asian holdings: 005930.KS → could use SSNLF or Korean futures during US session)
- Use sector futures (e.g., E-mini Nasdaq for tech-heavy ETFs)
- Flag the result as a **proxy** — explicitly note it is not an audited NAV
---
## 2. Dealer Gamma Exposure (GEX)
### Single-contract gamma (Black-Scholes)
```
d1 = (ln(S/K) + (r + σ²/2) × T) / (σ × √T)
gamma = φ(d1) / (S × σ × √T)
```
Where:
- `S` = spot price
- `K` = strike price
- `T` = time to expiration in years
- `r` = risk-free rate (decimal, e.g., 0.045)
- `σ` = implied volatility (decimal, e.g., 0.40)
- `φ(x)` = standard normal PDF = `exp(-x²/2) / √(2π)`
### Per-contract dollar gamma per 1% spot move
For one contract with multiplier 100:
```
$ delta change per $1 spot move = 100 × gamma × S (in dollars)
$ delta change per 1% spot move = 100 × gamma × S × (S × 0.01)
= gamma × S² (in dollars)
```
So:
```
$ gamma exposure per 1% move (one contract) = OI × gamma × S²
```
(Implicit assumption: multiplier = 100; which it is for US equity options.)
### Aggregating across the chain
Two conventions are widely used. Always state which one you're using.
#### Convention A: SqueezeMetrics-style net GEX
Assumes **dealers short calls, long puts** (the typical net market-maker book in equity index options):
```
net_GEX_$ = Σ (OI_call × gamma_call) × S²
- Σ (OI_put × gamma_put) × S²
```
Interpretation:
- **Positive net GEX** → dealers are net long gamma → they SELL into rallies, BUY into dips → market is **stabilizing**
- **Negative net GEX** → dealers are net short gamma → they BUY into rallies, SELL into dips → market is **destabilizing** (gamma squeeze fuel)
#### Convention B: Customer-net-long-everything
Assumes **dealers short both calls and puts** — appropriate during retail-driven rallies where customers buy both directionally:
```
gross_hedge_$ = Σ (OI_call × gamma_call) × S²
+ Σ (OI_put × gamma_put) × S²
```
Interpretation:
- This is the **maximum hedging pressure** assumption
- Always implies dealers buy on rallies, sell on dips
- Useful as an upper-bound estimate
For a single-name or thematic ETF rally driven by retail call-buying, Convention A's "net GEX" is the most defensible. For an index ETF, the same convention is standard.
### Reproducing the article's $4-5B per 1% claim
The article claimed dealers needed to buy approximately $4–5 billion per 1% upward move in the DRAM ETF. Working backwards:
```
gamma exposure per 1% = $4.5B (midpoint)
= OI × gamma × S² (summed over the chain)
If S ≈ $50 (June $45 calls deep ITM), S² ≈ 2,500
Total contract-gamma sum ≈ 4.5e9 / 2500 = 1.8e6
With 458,916 total contracts and weighted gamma ~0.04 → 458,916 × 0.04 ≈ 18,357
These don't quite reconcile — suggesting the article's figure includes a non-standard
multiplier, uses a different "1% basis" (e.g., per share rather than per spot %),
or assumes only the most concentrated strikes. Treat magnitude as illustrative,
not precise.
```
Lesson: when reproducing GEX figures from third parties, always check the convention. Dollar GEX numbers can differ by orders of magnitude depending on whether the author means per $1 move, per 1% move, per share, or per contract.
---
## 3. Convergence Timeline
Three time horizons matter — different mechanisms close the gap on each:
### Hours: AP creation/redemption arbitrage
The first-line mechanism. APs can correct an excess premium within minutes by creating new shares (sell premium-priced shares, buy underlying basket, deliver basket for new shares, pocket spread).
This breaks down when:
- The underlying market is **closed** (international ETF during US hours; weekend; holiday)
- The underlying basket is **illiquid** (APs can't source it cheaply)
- The issuer has **capped creation units** (rare; mostly seen in regulated commodity ETFs)
- Spread between bid/ask is widening (AP stepping back from market making)
Signal that AP arbitrage is impeded: the premium persists into the close, and bid/ask spread is wider than typical.
### Days: Options expiration & gamma decay
Even with AP arbitrage blocked, the gamma squeeze fuel decays as options approach expiration:
- Concentrated near-dated calls lose gamma rapidly in the final 1–2 weeks
- After expiration, dealer hedges unwind (sell stock back), creating downward pressure on the ETF — sometimes referred to as a "gamma cliff"
- IV typically compresses post-event, reducing future hedging requirements
Check: where is the dominant strike's expiration? If it's within 5 trading days, the squeeze has a natural fuse.
### Weeks: Flow normalization
If structural inflows are still pushing into the ETF after the squeeze peaks, the premium can stay elevated for weeks. Watch:
- Daily AUM change (proxy for net flows)
- Creation unit activity reported by the issuer
- Short interest in the ETF itself (sometimes shorts get squeezed alongside)
If flows normalize and APs catch up, the premium converges over 1–4 weeks even without an external catalyst.
---
## 4. Distinguishing a Real Gamma Squeeze from a Rally
| Indicator | Real squeeze | Routine rally |
|---|---|---|
| ETF move vs NAV proxy | ETF move >> NAV move (5pp+ excess) | Roughly aligned |
| ATM IV | Spiking — often 2x baseline | Stable or modestly higher |
| Call/Put OI ratio | > 2.5, often 3:1+ | Typically 1–1.5 |
| OI concentration | Single near-dated strike dominates | Diffuse across expirations |
| Net GEX (SqueezeMetrics) | Strongly negative | Mildly positive or near zero |
| Bid/ask spread | Wider than recent average | Stable |
| Underlying market session | Often closed | Open |
A move that hits 5+ of these markers is consistent with a gamma squeeze. A move that hits only 1–2 is more likely a fundamental repricing.
---
## 5. Worked example — DRAM ETF, May 8, 2026
Reproduced from the source article (Zhihu) for reference. Numbers are the article's claims, not verified.
| Item | Value |
|---|---|
| ETF return (intraday + after-hours) | +13.4% |
| Estimated NAV return (Micron 20% / SK Hynix 27% / Samsung 22%, weighted) | +7–8% |
| **Excess premium** | **+5–6 pp** |
| ATM IV | 78 |
| Call/Put OI ratio | 3.1 : 1 |
| Total OI across 12 expirations | 458,916 contracts |
| Concentrated strike | June $45 calls (deep ITM) |
| Estimated dealer $ buying per 1% | $4–5 B |
| Implied dealer share of day's buying | ~35% |
| Convergence outlook | AP blocked (KRX closed); ~3–5 trading days for gamma neutrality; flows still high |
Read this as: roughly half of the move was structural (gamma + AP impedance), and the squeeze had a 1-week fuse via June expirations.
---
## 6. Caveats
- **GEX is sensitive to dealer-positioning assumptions.** Always state the convention. A net-GEX number with a flipped sign convention is worse than no number at all.
- **NAV proxy ≠ official NAV.** End-of-day NAV is calculated by the fund administrator using closing prices in the home market plus FX adjustments. The holdings-weighted estimate is a directional proxy.
- **The dealer-share-of-volume figure is an upper bound.** It assumes every gamma-related share was hedged on the day; in practice hedging spreads over multiple sessions.
- **Implied volatility from yfinance is the option's quoted IV, not a fitted volatility surface.** It's adequate for GEX estimation but not for precise pricing.
- **This skill is descriptive, not predictive.** Quantifying that "35% of buying was dealer hedging today" does not tell you what tomorrow's flows will be.
SKILL.md›
---
name: etf-premium
description: >
Calculate ETF premium/discount vs NAV via Yahoo Finance, and decompose single-day surges
into NAV-driven vs structural components (gamma squeeze, dealer hedging, blocked AP arbitrage).
Use whenever the user asks about an ETF's premium or discount, NAV comparison, why an ETF
diverged from its holdings, or how much of a move is dealer-hedging-driven.
Triggers: "ETF premium", "ETF discount", "NAV premium", "is SPY at a premium", "BITO premium",
"IBIT premium", "bond ETF discount", "trading above/below NAV", "ETF premium screener",
"biggest discount", "compare ETF NAV", "ETF arbitrage", "ETF gamma squeeze",
"ETF premium surge", "decompose ETF move", "dealer gamma exposure", "GEX for ETF",
"why did this ETF jump", "premium convergence", "AP arbitrage blocked", or any request
about the gap between an ETF's price and underlying value. Especially relevant for
leveraged, inverse, international, bond, commodity, and crypto ETFs.
---
# ETF Premium/Discount Analysis Skill
Calculates the premium or discount of an ETF's market price relative to its Net Asset Value (NAV) using data from Yahoo Finance via [yfinance](https://github.com/ranaroussi/yfinance).
**Why this matters:** An ETF's market price can diverge from the value of its underlying holdings (NAV). When you buy at a premium, you're overpaying relative to the assets; at a discount, you're getting a bargain. This divergence is typically small for liquid US equity ETFs but can be significant for bond ETFs, international ETFs, leveraged/inverse products, and crypto ETFs — especially during periods of market stress.
**Important**: For research and educational purposes only. Not financial advice. yfinance is not affiliated with Yahoo, Inc.
---
## Step 1: Ensure Dependencies Are Available
**Current environment status:**
```
!`python3 -c "exec('try:\n import yfinance, pandas, numpy\n print(f\'yfinance={yfinance.__version__} pandas={pandas.__version__} numpy={numpy.__version__}\')\nexcept Exception:\n print(\'DEPS_MISSING\')')"`
```
If `DEPS_MISSING`, install required packages:
```python
import subprocess, sys
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "yfinance", "pandas", "numpy"])
```
If already installed, skip and proceed.
---
## Step 2: Route to the Correct Sub-Skill
Classify the user's request and jump to the matching section. If the user asks a general question about an ETF's premium or discount without specifying a particular analysis type, default to **Sub-Skill A** (Single ETF Snapshot).
| User Request | Route To | Examples |
|---|---|---|
| Single ETF premium/discount | **Sub-Skill A: Single ETF Snapshot** | "is SPY at a premium?", "AGG premium to NAV", "BITO premium" |
| Compare multiple ETFs | **Sub-Skill B: Multi-ETF Comparison** | "compare bond ETF discounts", "which has bigger premium IBIT or BITO", "rank these ETFs by premium" |
| Screener / find extreme premiums | **Sub-Skill C: Premium Screener** | "which ETFs have biggest discount", "find ETFs trading below NAV", "premium screener" |
| Deep analysis with context | **Sub-Skill D: Premium Deep Dive** | "why is HYG at a discount", "is ARKK premium normal", "ETF premium analysis with context" |
| Sudden premium surge / gamma squeeze | **Sub-Skill E: Premium Surge Decomposition** | "why did KWEB jump 13% today", "is this ETF rally driven by gamma", "decompose today's ETF move", "dealer GEX for SOXL", "how long until the premium converges" |
### Defaults
| Parameter | Default |
|---|---|
| Data source | yfinance `navPrice` field |
| Price field | `regularMarketPrice` (falls back to `previousClose`) |
| Screener universe | Common ETF list by category (see Sub-Skill C) |
---
## Sub-Skill A: Single ETF Snapshot
**Goal**: Show the current premium/discount for one ETF with context about what's normal, plus a peer comparison to show how it stacks up against similar ETFs.
### A1: Fetch and compute
```python
import yfinance as yf
# Peer groups by category — used to automatically compare the target ETF against its closest peers
CATEGORY_PEERS = {
"Digital Assets": ["IBIT", "BITO", "FBTC", "ETHA", "ARKB", "GBTC"],
"Intermediate Core Bond": ["AGG", "BND", "SCHZ"],
"High Yield Bond": ["HYG", "JNK", "USHY"],
"Long Government": ["TLT", "VGLT", "SPTL"],
"Emerging Markets Bond": ["EMB", "VWOB", "PCY"],
"Large Growth": ["QQQ", "VUG", "IWF", "SCHG"],
"Large Blend": ["SPY", "VOO", "IVV", "VTI"],
"Commodities Focused": ["GLD", "IAU", "SLV", "DBC"],
"China Region": ["KWEB", "FXI", "MCHI"],
"Trading--Leveraged Equity": ["TQQQ", "UPRO", "SOXL", "JNUG"],
"Trading--Inverse Equity": ["SQQQ", "SPXU", "SOXS", "JDST"],
"Derivative Income": ["JEPI", "JEPQ", "QYLD"],
"Large Value": ["SCHD", "VYM", "DVY", "HDV"],
}
def etf_premium_snapshot(ticker_symbol):
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
# Verify this is an ETF
quote_type = info.get("quoteType", "")
if quote_type != "ETF":
return {"error": f"{ticker_symbol} is not an ETF (quoteType={quote_type})"}
price = info.get("regularMarketPrice") or info.get("previousClose")
nav = info.get("navPrice")
if not price or not nav or nav <= 0:
return {"error": f"NAV data not available for {ticker_symbol}"}
premium_pct = (price - nav) / nav * 100
premium_dollar = price - nav
# Additional context
result = {
"ticker": ticker_symbol,
"name": info.get("longName") or info.get("shortName", ""),
"market_price": round(price, 4),
"nav": round(nav, 4),
"premium_discount_pct": round(premium_pct, 4),
"premium_discount_dollar": round(premium_dollar, 4),
"status": "PREMIUM" if premium_pct > 0 else "DISCOUNT" if premium_pct < 0 else "AT NAV",
"category": info.get("category", "N/A"),
"fund_family": info.get("fundFamily", "N/A"),
"total_assets": info.get("totalAssets"),
"net_expense_ratio": info.get("netExpenseRatio"),
"avg_volume": info.get("averageVolume"),
"bid": info.get("bid"),
"ask": info.get("ask"),
"yield_pct": info.get("yield"),
"ytd_return": info.get("ytdReturn"),
}
# Bid-ask spread as context for whether the premium is meaningful
bid = info.get("bid")
ask = info.get("ask")
if bid and ask and bid > 0:
spread_pct = (ask - bid) / ((ask + bid) / 2) * 100
result["bid_ask_spread_pct"] = round(spread_pct, 4)
return result
```
### A2: Fetch peer comparison
After computing the target ETF's snapshot, look up its `category` and pull premium data for peers in the same category. This gives the user immediate context on whether the premium is ETF-specific or market-wide.
Use the target's `category` to select `CATEGORY_PEERS`, remove the target, and run the same price/NAV calculation for each peer. Skip unavailable NAV rows but report how many peers were requested and returned so missing data is visible.
Present the peer comparison as a small table after the main snapshot. This helps the user see whether the premium is unique to their ETF or shared across the category — for example, if all crypto ETFs are at ~1.5% premium, the user's ETF isn't an outlier.
### A3: Interpret the result
Use this framework to explain whether the premium/discount is meaningful:
| Premium/Discount | Interpretation |
|---|---|
| Within +/- 0.05% | Essentially at NAV — normal for large, liquid ETFs |
| +/- 0.05% to 0.25% | Minor deviation — common and usually not actionable |
| +/- 0.25% to 1.0% | Notable — worth mentioning. Check bid-ask spread and category |
| +/- 1.0% to 3.0% | Significant — common for less liquid, international, or specialty ETFs |
| Beyond +/- 3.0% | Large — may indicate stress, illiquidity, or structural issues |
**Context matters by category:**
- **US large-cap equity** (SPY, QQQ, IVV): premiums > 0.10% are unusual
- **Bond ETFs** (AGG, HYG, LQD, TLT): discounts of 0.5-2% happen during volatility
- **International/EM** (EEM, VWO, KWEB): time-zone mismatch causes regular 0.3-1% deviations
- **Leveraged/Inverse** (TQQQ, SQQQ, JNUG): 0.3-1.5% is normal due to daily reset mechanics
- **Crypto** (IBIT, BITO): 1-3% premiums are common, especially for newer funds
- **Commodity** (GLD, USO, UNG): depends on contango/backwardation in futures
Also compare the premium/discount to the **bid-ask spread**: if the premium is smaller than the spread, it's noise, not signal.
---
## Sub-Skill B: Multi-ETF Comparison
**Goal**: Compare premium/discount across multiple ETFs side by side.
### B1: Fetch and rank
```python
import yfinance as yf
import pandas as pd
def compare_etf_premiums(tickers):
rows = []
for sym in tickers:
try:
t = yf.Ticker(sym)
info = t.info
if info.get("quoteType") != "ETF":
rows.append({"ticker": sym, "error": "Not an ETF"})
continue
price = info.get("regularMarketPrice") or info.get("previousClose")
nav = info.get("navPrice")
if price and nav and nav > 0:
prem = (price - nav) / nav * 100
bid = info.get("bid", 0)
ask = info.get("ask", 0)
spread = (ask - bid) / ((ask + bid) / 2) * 100 if bid and ask and bid > 0 else None
rows.append({
"ticker": sym,
"name": info.get("shortName", ""),
"price": round(price, 2),
"nav": round(nav, 2),
"premium_pct": round(prem, 4),
"spread_pct": round(spread, 4) if spread else None,
"category": info.get("category", "N/A"),
"total_assets": info.get("totalAssets"),
})
else:
rows.append({"ticker": sym, "error": "NAV unavailable"})
except Exception as e:
rows.append({"ticker": sym, "error": str(e)})
df = pd.DataFrame(rows)
if "premium_pct" in df.columns:
df = df.sort_values("premium_pct", ascending=True)
return df
```
### B2: Present as a ranked table
Sort by premium/discount (most discounted first). Highlight:
- Which ETFs are at the deepest discount
- Which are at the highest premium
- Whether the premium/discount exceeds the bid-ask spread (if it doesn't, it's market microstructure noise)
---
## Sub-Skill C: Premium Screener
**Goal**: Scan a universe of common ETFs to find those with the largest premiums or discounts.
### C1: Define the universe and scan
Use the category-organized universe in `references/etf_premium_reference.md`, or the user's own list. Apply the Sub-Skill A calculation to each symbol, preserve category labels, filter by the requested absolute premium threshold, and sort from deepest discount to highest premium. Keep failed or missing-NAV counts visible instead of silently treating them as zero.
### C2: Present the results
Show a ranked table sorted by premium (most discounted first). Group by category if the list is long. Call out:
- **Top 5 deepest discounts** — potential buying opportunities (or signs of stress)
- **Top 5 highest premiums** — overpaying risk
- **Category patterns** — are all bond ETFs at a discount? Are all crypto ETFs at a premium?
Warn that large universes may take 1-2 minutes.
---
## Sub-Skill D: Premium Deep Dive
**Goal**: Combine premium/discount data with additional context to help the user understand *why* the premium exists and whether it's likely to persist.
### D1: Gather comprehensive data
Run the Sub-Skill A snapshot, then pull three months of daily history and add:
- Annualized volatility: `std(daily returns) * sqrt(252)`
- Average daily dollar volume: `mean(close * volume)`
- Percentage distance from the three-month closing high
- AUM, expense ratio, yield, YTD return, and three-year beta
- Bid-ask spread percentage and whether the absolute premium exceeds that spread
Keep unavailable fields as `null` rather than inventing values. Timestamp price and NAV inputs so users can judge whether the comparison is synchronized.
### D2: Explain the *why*
After gathering data, explain the premium/discount using this diagnostic framework:
**Common causes of premiums:**
- **Demand surge** — more buyers than authorized participants can create shares (common for new/hot ETFs like crypto)
- **Time-zone mismatch** — international ETF trading when underlying markets are closed; price reflects anticipated moves
- **Creation mechanism bottleneck** — when authorized participants face constraints on creating new shares
- **Sentiment premium** — retail demand pushes price above fair value during hype cycles
**Common causes of discounts:**
- **Liquidity stress** — during sell-offs, bond and credit ETFs often trade at discounts because underlying bonds are harder to price/trade than the ETF itself
- **Redemption pressure** — heavy outflows but slow authorized participant response
- **Stale NAV** — the official NAV may not reflect after-hours news or events
- **Structural issues** — contango in futures-based ETFs (USO, UNG) creates persistent drag
**Is the premium likely to persist?**
- For liquid US equity ETFs: No — arbitrage corrects deviations within minutes
- For bond ETFs during stress: Discounts can persist for days or weeks
- For crypto ETFs: Premiums tend to narrow as the fund matures and APs become more active
- For international ETFs: Resets daily as underlying markets open
---
## Sub-Skill E: Premium Surge Decomposition (Gamma Squeeze Analysis)
**Goal**: When an ETF has just experienced a dramatic intraday move that diverges from its underlying holdings, decompose the move into (1) a fundamental NAV-driven component and (2) an "excess premium" driven by structural forces — most commonly options dealer gamma hedging, AP arbitrage breakdowns, or sentiment surges. Then assess how long the premium will likely take to converge.
This sub-skill is appropriate when the user reports or asks about:
- An ETF moving 5%+ in a single session
- A divergence between the ETF and its named underlyings (e.g., "MSTR jumped 13% but BTC only rose 3%")
- A suspected gamma squeeze in an ETF or single name
- Whether dealer hedging is amplifying a move
Read `references/gamma_squeeze_reference.md` for the full GEX formula derivation, dealer-positioning conventions, and worked examples before running E2.
### E1: Decompose today's move into NAV-driven vs excess premium
The static `navPrice` field gives only the most recent end-of-day NAV. Estimate today's NAV return from current holdings weights and same-session holding returns, normalize by the covered weight, then calculate:
```text
NAV proxy return = sum(weight_i x return_i) / covered weight
Excess premium return = ETF return - NAV proxy return
```
Report holdings coverage and the per-holding returns used. If `funds_data.top_holdings` is incomplete, prefer issuer-published holdings or user-supplied weights.
**Caveat**: For international ETFs whose underlyings trade in a closed session (e.g., Asian holdings during US hours), the holdings' US-listed proxies (ADRs) or futures must be used. If neither is available, flag this to the user — the NAV proxy will be stale.
### E2: Compute dealer gamma exposure (GEX) from the options chain
GEX approximates dealer hedging sensitivity per 1% underlying move. Read the formulas and both positioning conventions in `references/gamma_squeeze_reference.md`, calculate contract gamma from current spot, strike, time, risk-free rate, and IV, then aggregate `OI x gamma x spot^2` across the chain.
Return call GEX, put GEX, SqueezeMetrics-style net GEX, gross hedge pressure, call/put OI ratio, median near-ATM IV, expirations analyzed, and the top strike/expiry concentrations. State the sign convention explicitly; do not infer actual dealer inventory from public OI alone.
Interpret the output:
- **`net_gex_squeezemetrics_$` highly negative** → dealers are short gamma; rallies will be amplified by their hedging buys. Classic gamma-squeeze fuel.
- **Concentration on a single near-dated strike** (e.g., the article's "June $45 calls") → squeeze is fragile and concentrated. When that strike expires or the spot moves past it, the gamma decays sharply.
- **ATM IV well above the recent average** (article example: 78 vs typical ~30–40) → market is pricing in continued large moves; option premium decay alone will provide some convergence pressure over days.
- **Call/Put OI ratio > 2.5** → call-heavy positioning, consistent with a bullish gamma squeeze setup.
### E3: Compare structural buying pressure to actual volume
Estimate the upper-bound dealer share with:
```text
Implied dealer-driven dollars = abs(GEX per 1% move) x abs(ETF return in percentage points)
Dealer share of volume = implied dealer-driven dollars / (close x volume)
```
This is a rough estimate — it assumes every contract's full gamma was hedged in a single direction during the move. Real hedging is incremental, and not all dealers hedge identically. Treat as an upper-bound heuristic, not a precise figure. Always present it alongside the assumptions.
### E4: Assess premium convergence timeline
The article's three-tier convergence framework:
| Time scale | Mechanism | What to check |
|---|---|---|
| **Hours** | AP creation/redemption arbitrage | Is the underlying market open? Are creation units restricted? Is the spread between bid/ask widening (suggests AP stepping back)? |
| **Days** | Options expiration / gamma decay | When does the dominant strike's expiration land? Is OI rolling forward or being closed? Is IV starting to compress? |
| **Weeks** | Net flow normalization | Is the ETF receiving large daily inflows (signals demand outpacing creation capacity)? Is short interest building (potential additional squeeze fuel)? |
For the hours view, record whether the underlying market is open and whether creation/redemption is constrained. For the days view, calculate days to the largest gamma concentration's expiry and check whether IV and OI are decaying or rolling. For the weeks view, use issuer flow/creation data where available; AUM alone is only a rough proxy.
### E5: Present the decomposition
Format the answer in this order:
1. **Headline number**: today's ETF move, NAV-proxy move, and the excess premium (in pp).
2. **Decomposition table**:
| Component | Contribution |
|---|---|
| NAV-driven (holdings × weights) | +X.X% |
| Excess premium (residual) | +Y.Y% |
| Total ETF move | +Z.Z% |
3. **Dealer hedging quantification**:
- Net GEX (SqueezeMetrics convention)
- Implied dealer $ buying for the day vs actual $ volume
- Estimated dealer share of buying pressure
4. **Risk indicators**: ATM IV, call/put OI ratio, top-3 strike/expiration concentrations.
5. **Convergence outlook**: list each of the hours/days/weeks mechanisms with the current state of each.
6. **Caveats**: the GEX estimate assumes uniform dealer positioning; the NAV proxy is stale during overnight sessions; this is *not* a forecast of future price.
---
## Step 3: Respond to the User
### Always include
- The **ETF name and ticker**
- **Market price** and **NAV** with the calculation shown
- **Premium/discount percentage** clearly labeled
- **Context**: is this deviation normal for this ETF category?
### Always caveat
- NAV data from Yahoo Finance reflects the **most recent official NAV** (typically end of prior trading day) — it is not real-time
- Market price may have a **15-minute delay** depending on the exchange
- Premium/discount can change rapidly during market hours — this is a snapshot, not a live feed
- Small premiums/discounts (< bid-ask spread) are **market microstructure noise**, not real mispricing
- **Never recommend buying or selling** based on premium/discount alone — present the data and let the user decide
### Formatting
- Use markdown tables for multi-ETF comparisons
- Show the formula: `Premium/Discount = (Market Price - NAV) / NAV x 100`
- Use color indicators in text: "trading at a **0.45% discount**" or "at a **1.2% premium**"
- Round percentages to 2-4 decimal places depending on magnitude
---
## Reference Files
- `references/etf_premium_reference.md` — Detailed formulas, category-specific benchmarks, common ETF universe list, and background on the creation/redemption mechanism that drives premiums
- `references/gamma_squeeze_reference.md` — Premium decomposition framework, Black-Scholes gamma + GEX formulas with both SqueezeMetrics and customer-net-long conventions, convergence-timeline framework (hours/days/weeks), gamma-squeeze vs routine-rally diagnostic table, and a worked example. Read this **before** running Sub-Skill E.
Read the reference files for deeper technical detail on ETF premium/discount mechanics, historical context, and the gamma-squeeze decomposition methodology.