
Etf Premium
- 1.6k installs
- 3.1k repo stars
- Updated July 21, 2026
- himself65/finance-skills
ETF Premium is a finance agent skill that calculates whether an ETF trades at a premium or discount to its NAV and explains price drivers using Yahoo Finance market and NAV data.
About
ETF Premium is a skill from himself65/finance-skills that answers questions about ETF market price versus net asset value. When triggered, the skill fetches an ETF's current market price and NAV from Yahoo Finance, computes the premium or discount percentage using the price-to-NAV formula, and helps decompose sudden price moves that outpace underlying holdings. Developers and quant engineers reach for ETF Premium when building fintech agents, validating market data pipelines, or debugging why an ETF like SPY, AGG, or BITO diverges from its basket. Trigger phrases include "AGG premium to NAV", "ETF premium screener", and questions about convergence timing or dealer gamma effects on ETF surges.
- Fetches real-time market price and NAV from Yahoo Finance
- Calculates premium/discount as (Price - NAV)/NAV × 100
- Compares deviation against category norms and bid-ask spread
- Supports single ETF snapshot, multi-ETF comparison, and screener mode
- Includes gamma-squeeze decomposition to split surges into NAV-driven vs structural components
Etf Premium by the numbers
- 1,610 all-time installs (skills.sh)
- +155 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #91 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/himself65/finance-skills --skill etf-premiumAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 3.1k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 21, 2026 |
| Repository | himself65/finance-skills ↗ |
How do you calculate ETF premium to NAV?
Instantly calculate whether an ETF is trading at a premium or discount to its NAV and understand the drivers behind price moves.
Who is it for?
Developers building fintech agents or analysts who need quick ETF premium-discount math from live Yahoo Finance quotes.
Skip if: Long-only equity research with no ETF structure or developers who need proprietary exchange feed latency instead of Yahoo Finance data.
When should I use this skill?
The user asks whether an ETF trades at a premium, compares bond ETF discounts, or wants NAV versus market price decomposition.
What you get
Premium or discount percentage, NAV versus market price comparison, and narrative explanation of ETF price divergence drivers.
- Premium or discount percentage
- Price versus NAV comparison
- Divergence explanation
Files
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.
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 "import yfinance, pandas, numpy; print(f'yfinance={yfinance.__version__} pandas={pandas.__version__} numpy={numpy.__version__}')" 2>/dev/null || echo "DEPS_MISSING"`If DEPS_MISSING, install required packages:
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
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 resultA2: 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.
def get_peer_premiums(target_ticker, target_category):
"""Fetch premium/discount for peers in the same category."""
peers = CATEGORY_PEERS.get(target_category, [])
# Remove the target itself from peers
peers = [p for p in peers if p.upper() != target_ticker.upper()]
if not peers:
return []
peer_data = []
for sym in peers:
try:
t = yf.Ticker(sym)
info = t.info
p = info.get("regularMarketPrice") or info.get("previousClose")
n = info.get("navPrice")
if p and n and n > 0:
prem = (p - n) / n * 100
peer_data.append({
"ticker": sym,
"name": info.get("shortName", ""),
"price": round(p, 2),
"nav": round(n, 2),
"premium_pct": round(prem, 4),
"expense_ratio": info.get("netExpenseRatio"),
})
except Exception:
pass
return peer_dataPresent 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
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 dfB2: 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 this default universe organized by category. The user can supply their own list instead.
DEFAULT_ETF_UNIVERSE = {
"US Equity": ["SPY", "QQQ", "IVV", "VOO", "VTI", "DIA", "IWM", "ARKK"],
"Bond": ["AGG", "BND", "TLT", "HYG", "LQD", "VCIT", "VCSH", "BNDX", "EMB", "JNK", "MUB", "TIP"],
"International": ["EFA", "EEM", "VWO", "IEMG", "KWEB", "FXI", "INDA", "VEA", "EWZ", "EWJ"],
"Commodity": ["GLD", "SLV", "USO", "UNG", "DBC", "IAU", "PDBC", "GSG"],
"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": ["SOXX", "SMH", "IGV", "XSD"],
"Sector - Healthcare": ["XBI", "IBB", "IHI"],
"Thematic": ["ARKW", "ARKG", "HACK", "CLOU", "WCLD", "BUG", "BOTZ", "LIT", "ICLN", "TAN"],
"Income": ["JEPI", "JEPQ", "SCHD", "VYM", "DVY", "DIVO", "HDV", "QYLD"],
}
import yfinance as yf
import pandas as pd
def screen_etf_premiums(universe=None, min_abs_premium=0.0):
if universe is None:
universe = DEFAULT_ETF_UNIVERSE
all_tickers = []
for category, tickers in universe.items():
for sym in tickers:
all_tickers.append((sym, category))
rows = []
for sym, category_label in all_tickers:
try:
t = yf.Ticker(sym)
info = t.info
price = info.get("regularMarketPrice") or info.get("previousClose")
nav = info.get("navPrice")
if price and nav and nav > 0:
prem = (price - nav) / nav * 100
if abs(prem) >= min_abs_premium:
rows.append({
"ticker": sym,
"name": info.get("shortName", ""),
"category": category_label,
"price": round(price, 2),
"nav": round(nav, 2),
"premium_pct": round(prem, 4),
"total_assets_B": round(info.get("totalAssets", 0) / 1e9, 2),
"expense_ratio": info.get("netExpenseRatio"),
})
except Exception:
pass
df = pd.DataFrame(rows)
if not df.empty:
df = df.sort_values("premium_pct", ascending=True)
return dfC2: 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?
Note: this screener takes time because it fetches data one ticker at a time. For large universes (60+ ETFs), warn the user it 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 add:
import yfinance as yf
import numpy as np
def premium_deep_dive(ticker_symbol):
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
price = info.get("regularMarketPrice") or info.get("previousClose")
nav = info.get("navPrice")
if not price or not nav or nav <= 0:
return {"error": "NAV data not available"}
premium_pct = (price - nav) / nav * 100
# Historical price data for volatility context
hist = ticker.history(period="3mo")
if not hist.empty:
returns = hist["Close"].pct_change().dropna()
daily_vol = returns.std()
annualized_vol = daily_vol * np.sqrt(252)
avg_volume = hist["Volume"].mean()
dollar_volume = (hist["Close"] * hist["Volume"]).mean()
# Price range context
high_3m = hist["Close"].max()
low_3m = hist["Close"].min()
pct_from_high = (price - high_3m) / high_3m * 100
else:
daily_vol = annualized_vol = avg_volume = dollar_volume = None
high_3m = low_3m = pct_from_high = None
result = {
"ticker": ticker_symbol,
"name": info.get("longName", ""),
"price": round(price, 4),
"nav": round(nav, 4),
"premium_pct": round(premium_pct, 4),
"category": info.get("category", "N/A"),
"fund_family": info.get("fundFamily", "N/A"),
"total_assets": info.get("totalAssets"),
"expense_ratio": info.get("netExpenseRatio"),
"yield_pct": info.get("yield"),
"ytd_return": info.get("ytdReturn"),
"beta_3y": info.get("beta3Year"),
"annualized_vol": round(annualized_vol * 100, 2) if annualized_vol else None,
"avg_daily_dollar_volume": round(dollar_volume, 0) if dollar_volume else None,
"pct_from_3m_high": round(pct_from_high, 2) if pct_from_high else None,
}
# Bid-ask spread
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)
result["premium_exceeds_spread"] = abs(premium_pct) > spread_pct
return resultD2: 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 — it cannot tell you how much of today's move is NAV-driven. Estimate the NAV return from the holdings' returns instead:
import yfinance as yf
import pandas as pd
import numpy as np
def decompose_etf_move(ticker_symbol, holdings_weights=None, window="2d"):
"""
Decompose the ETF's most recent daily move into NAV-driven vs excess premium.
holdings_weights: dict like {"MU": 0.20, "005930.KS": 0.22, "000660.KS": 0.27, ...}
If None, attempts to fetch via yfinance's funds_data;
falls back to user-supplied weights for ETFs where it isn't available.
"""
etf = yf.Ticker(ticker_symbol)
info = etf.info
# ETF return over the most recent session
etf_hist = etf.history(period=window, auto_adjust=False)
if len(etf_hist) < 2:
return {"error": "Not enough history"}
etf_close_today = etf_hist["Close"].iloc[-1]
etf_close_prev = etf_hist["Close"].iloc[-2]
etf_return_pct = (etf_close_today / etf_close_prev - 1) * 100
# Try to auto-fetch holdings if not supplied
if holdings_weights is None:
try:
top_holdings = etf.funds_data.top_holdings # DataFrame
holdings_weights = dict(zip(top_holdings.index, top_holdings["Holding Percent"]))
except Exception:
holdings_weights = {}
if not holdings_weights:
return {
"error": "Holdings weights unavailable — supply manually via holdings_weights={'TICKER': weight, ...}",
"etf_return_pct": round(etf_return_pct, 4),
}
# Weighted return of underlying holdings (proxy for NAV move)
weighted_return = 0.0
coverage = 0.0
holding_returns = {}
for sym, w in holdings_weights.items():
try:
h = yf.Ticker(sym).history(period=window, auto_adjust=False)
if len(h) >= 2:
r = (h["Close"].iloc[-1] / h["Close"].iloc[-2] - 1) * 100
holding_returns[sym] = round(r, 4)
weighted_return += w * r
coverage += w
except Exception:
pass
# Normalize to coverage so partial holdings still give a sensible NAV proxy
nav_return_proxy = weighted_return / coverage if coverage > 0 else None
excess_premium_pct = (
etf_return_pct - nav_return_proxy if nav_return_proxy is not None else None
)
return {
"ticker": ticker_symbol,
"etf_return_pct": round(etf_return_pct, 4),
"nav_return_proxy_pct": round(nav_return_proxy, 4) if nav_return_proxy else None,
"excess_premium_pct": round(excess_premium_pct, 4) if excess_premium_pct else None,
"holdings_coverage_pct": round(coverage * 100, 2),
"holding_returns": holding_returns,
"interpretation": (
"Most of the move is NAV-driven — limited structural component"
if excess_premium_pct is not None and abs(excess_premium_pct) < 1
else "Significant excess premium — investigate dealer hedging, AP bottlenecks, or sentiment"
if excess_premium_pct is not None
else "Cannot conclude without holdings data"
),
}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 quantifies how much hedging buying/selling dealers must do per 1% move in the underlying. Large positive GEX accumulating on the call side during a rally indicates a gamma squeeze in progress.
import numpy as np
from datetime import datetime, timezone
from math import log, sqrt, exp, pi
def _norm_pdf(x):
return exp(-0.5 * x * x) / sqrt(2 * pi)
def _bsm_gamma(S, K, T, r, sigma):
"""Black-Scholes gamma. Returns 0 for degenerate inputs."""
if S <= 0 or K <= 0 or T <= 0 or sigma <= 0:
return 0.0
d1 = (log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrt(T))
return _norm_pdf(d1) / (S * sigma * sqrt(T))
def compute_gex(ticker_symbol, risk_free_rate=0.045, max_expirations=8):
"""
Compute gross and net dealer gamma exposure.
Conventions:
- Per contract, dollar gamma per 1% move = OI * 100 * gamma * spot * (spot * 0.01)
= OI * gamma * spot^2 (with multiplier=100)
- SqueezeMetrics convention (assumes dealers SHORT calls, LONG puts):
net_gex = call_gamma_$ - put_gamma_$
Positive net_gex = stabilizing (dealers sell rallies, buy dips)
Negative net_gex = destabilizing (dealers buy rallies, sell dips → squeeze)
- "Customer-net-long-everything" convention (dealers SHORT both):
gross_hedge = call_gamma_$ + put_gamma_$
This is the maximum hedging pressure assumption.
"""
t = yf.Ticker(ticker_symbol)
info = t.info
spot = info.get("regularMarketPrice") or info.get("previousClose")
if not spot:
return {"error": "No spot price"}
expirations = t.options[:max_expirations]
if not expirations:
return {"error": "No options chain available"}
now = datetime.now(timezone.utc)
rows = []
for exp_str in expirations:
try:
chain = t.option_chain(exp_str)
except Exception:
continue
exp_date = datetime.strptime(exp_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
T = max((exp_date - now).total_seconds() / (365.25 * 86400), 1e-6)
for side, df in [("call", chain.calls), ("put", chain.puts)]:
for _, row in df.iterrows():
K = row.get("strike")
iv = row.get("impliedVolatility")
oi = row.get("openInterest", 0) or 0
if not K or not iv or oi <= 0:
continue
gamma = _bsm_gamma(spot, K, T, risk_free_rate, iv)
# Dollar value per 1% spot move:
gamma_dollars_per_1pct = oi * gamma * spot * spot
rows.append({
"expiration": exp_str,
"side": side,
"strike": K,
"iv": iv,
"oi": oi,
"gamma": gamma,
"gamma_$_per_1pct": gamma_dollars_per_1pct,
})
if not rows:
return {"error": "No usable contracts"}
df = pd.DataFrame(rows)
call_gex = df[df["side"] == "call"]["gamma_$_per_1pct"].sum()
put_gex = df[df["side"] == "put"]["gamma_$_per_1pct"].sum()
# Top concentration: which expiration & strike dominate
top_strikes = (
df.groupby(["expiration", "strike", "side"])["gamma_$_per_1pct"]
.sum()
.sort_values(ascending=False)
.head(10)
.reset_index()
)
total_call_oi = df[df["side"] == "call"]["oi"].sum()
total_put_oi = df[df["side"] == "put"]["oi"].sum()
cp_ratio = total_call_oi / total_put_oi if total_put_oi > 0 else None
# Pull near-term ATM IV as a single representative number
df["moneyness"] = abs(df["strike"] / spot - 1)
near_atm = df.sort_values("moneyness").head(20)
atm_iv_pct = near_atm["iv"].median() * 100 if len(near_atm) else None
return {
"ticker": ticker_symbol,
"spot": spot,
"call_gex_per_1pct_$": call_gex,
"put_gex_per_1pct_$": put_gex,
"net_gex_squeezemetrics_$": call_gex - put_gex,
"gross_hedge_pressure_$": call_gex + put_gex,
"total_call_oi": int(total_call_oi),
"total_put_oi": int(total_put_oi),
"call_put_oi_ratio": round(cp_ratio, 2) if cp_ratio else None,
"atm_iv_pct": round(atm_iv_pct, 2) if atm_iv_pct else None,
"expirations_analyzed": len(expirations),
"top_concentrations": top_strikes,
}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
The article's most concrete claim was that ~35% of the day's buying was dealer-driven. Reproduce this comparison:
def estimate_dealer_share_of_volume(ticker_symbol, gex_per_1pct_dollars, etf_return_pct):
"""
Implied dealer-driven $ buying = |gex_per_1pct| * |etf_return_pct|
Compare to actual dollar volume.
"""
t = yf.Ticker(ticker_symbol)
hist = t.history(period="2d", auto_adjust=False)
if hist.empty:
return None
today = hist.iloc[-1]
actual_dollar_volume = today["Close"] * today["Volume"]
implied_dealer_buying = abs(gex_per_1pct_dollars) * abs(etf_return_pct)
share = implied_dealer_buying / actual_dollar_volume if actual_dollar_volume > 0 else None
return {
"actual_dollar_volume_$": round(actual_dollar_volume, 0),
"implied_dealer_buying_$": round(implied_dealer_buying, 0),
"dealer_share_of_volume_pct": round(share * 100, 2) if share else None,
}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)? |
def assess_convergence(ticker_symbol, top_concentrations_df):
"""Returns a dict of qualitative convergence signals."""
t = yf.Ticker(ticker_symbol)
info = t.info
# 1. AP arbitrage: market hours of underlying
region = info.get("region") or info.get("market") or "unknown"
underlying_session_note = (
"International — check whether underlying market overlaps US trading hours; "
"AP arbitrage may be blocked when underlying market is closed"
if "us_market" not in (info.get("market") or "").lower()
else "US-listed underlying — AP arbitrage active during US hours"
)
# 2. Options expiration: nearest concentrated strike
if not top_concentrations_df.empty:
next_major_exp = top_concentrations_df.iloc[0]["expiration"]
days_to_exp = (datetime.strptime(next_major_exp, "%Y-%m-%d") - datetime.now()).days
exp_note = f"Largest gamma concentration expires in {days_to_exp} days ({next_major_exp})"
else:
exp_note = "No clear strike concentration"
# 3. Flow proxy: AUM trajectory (very rough)
aum = info.get("totalAssets")
aum_note = f"Total AUM: ${aum/1e9:.2f}B" if aum else "AUM unavailable"
return {
"ap_arbitrage": underlying_session_note,
"options_window": exp_note,
"flows": aum_note,
}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 premiumsreferences/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.
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 mechanicsreferences/gamma_squeeze_reference.md— Premium decomposition framework, Black-Scholes gamma + GEX formulas with sign conventions, convergence-timeline mechanics, and gamma-squeeze diagnostic table
ETF Premium/Discount Reference
Core Formula
Premium/Discount (%) = (Market Price - NAV) / NAV × 100Where:
- 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
navPricereflects 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
navPricepopulated. - 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, SLVTier 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, ICLNTier 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.
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 returnWhere:
- 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_iSources 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 priceK= strike priceT= time to expiration in yearsr= 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.
Related skills
How it compares
Use ETF Premium for quick NAV-versus-price diagnostics on liquid ETFs; skip it for single-stock valuation or non-ETF fund structures.
FAQ
Where does ETF Premium get price and NAV data?
ETF Premium fetches the ETF's current market price and net asset value from Yahoo Finance. The skill then calculates the premium or discount as the percentage difference between market price and NAV for symbols like SPY, AGG, or BITO.
What ETF questions trigger the ETF Premium skill?
ETF Premium activates on requests such as "Is SPY trading at a premium?", bond ETF discount comparisons, premium screeners, and cases where an ETF jumped sharply while underlying holdings moved less, including convergence timing questions.
Is Etf Premium safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.