ETF Premium/Discount Analysis Skill

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).

How to use it

Claude Code
  1. Run the line below. It pulls the whole folder into ~/.claude/skills/etf-premium.
  2. Describe your job in plain words. Claude Code follows the skill from there.
Claude Code — installs the whole folder, not just SKILL.md
npx degit himself65/finance-skills/plugins/market-analysis/skills/etf-premium#main ~/.claude/skills/etf-premium

For one project only, change the path to .claude/skills/etf-premium.

Claude (web or desktop app)
  1. On this page open ⋯ → Download .md.
  2. Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
  3. Pick the file and Save. Claude shows the name and description and runs a security scan.
  4. Check the skill is switched on.
  5. Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
  1. ChatGPT: make a Project and paste it into Instructions.
  2. Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
  • Check which app you pasted it into — the steps above name the right one.
  • Some skills need the paid tier of Claude or ChatGPT.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Source of ETF Premium/Discount Analysis Skill

Show the full text402 lines
namedescription
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.

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 "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:

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 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
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:

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:

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.

1---
2name: etf-premium
3description: >
4 Calculate ETF premium/discount vs NAV via Yahoo Finance, and decompose single-day surges
5 into NAV-driven vs structural components (gamma squeeze, dealer hedging, blocked AP arbitrage).
6 Use whenever the user asks about an ETF's premium or discount, NAV comparison, why an ETF
7 diverged from its holdings, or how much of a move is dealer-hedging-driven.
8 Triggers: "ETF premium", "ETF discount", "NAV premium", "is SPY at a premium", "BITO premium",
9 "IBIT premium", "bond ETF discount", "trading above/below NAV", "ETF premium screener",
10 "biggest discount", "compare ETF NAV", "ETF arbitrage", "ETF gamma squeeze",
11 "ETF premium surge", "decompose ETF move", "dealer gamma exposure", "GEX for ETF",
12 "why did this ETF jump", "premium convergence", "AP arbitrage blocked", or any request
13 about the gap between an ETF's price and underlying value. Especially relevant for
14 leveraged, inverse, international, bond, commodity, and crypto ETFs.
15---
16 
17# ETF Premium/Discount Analysis Skill
18 
19Calculates 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).
20 
21**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.
22 
23**Important**: For research and educational purposes only. Not financial advice. yfinance is not affiliated with Yahoo, Inc.
24 
25---
26 
27## Step 1: Ensure Dependencies Are Available
28 
29**Current environment status:**
30 
31```
32!`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\')')"`
33```
34 
35If `DEPS_MISSING`, install required packages:
36 
37```python
38import subprocess, sys
39subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "yfinance", "pandas", "numpy"])
40```
41 
42If already installed, skip and proceed.
43 
44---
45 
46## Step 2: Route to the Correct Sub-Skill
47 
48Classify 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).
49 
50| User Request | Route To | Examples |
51|---|---|---|
52| Single ETF premium/discount | **Sub-Skill A: Single ETF Snapshot** | "is SPY at a premium?", "AGG premium to NAV", "BITO premium" |
53| 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" |
54| Screener / find extreme premiums | **Sub-Skill C: Premium Screener** | "which ETFs have biggest discount", "find ETFs trading below NAV", "premium screener" |
55| 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" |
56| 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" |
57 
58### Defaults
59 
60| Parameter | Default |
61|---|---|
62| Data source | yfinance `navPrice` field |
63| Price field | `regularMarketPrice` (falls back to `previousClose`) |
64| Screener universe | Common ETF list by category (see Sub-Skill C) |
65 
66---
67 
68## Sub-Skill A: Single ETF Snapshot
69 
70**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.
71 
72### A1: Fetch and compute
73 
74```python
75import yfinance as yf
76 
77# Peer groups by category — used to automatically compare the target ETF against its closest peers
78CATEGORY_PEERS = {
79 "Digital Assets": ["IBIT", "BITO", "FBTC", "ETHA", "ARKB", "GBTC"],
80 "Intermediate Core Bond": ["AGG", "BND", "SCHZ"],
81 "High Yield Bond": ["HYG", "JNK", "USHY"],
82 "Long Government": ["TLT", "VGLT", "SPTL"],
83 "Emerging Markets Bond": ["EMB", "VWOB", "PCY"],
84 "Large Growth": ["QQQ", "VUG", "IWF", "SCHG"],
85 "Large Blend": ["SPY", "VOO", "IVV", "VTI"],
86 "Commodities Focused": ["GLD", "IAU", "SLV", "DBC"],
87 "China Region": ["KWEB", "FXI", "MCHI"],
88 "Trading--Leveraged Equity": ["TQQQ", "UPRO", "SOXL", "JNUG"],
89 "Trading--Inverse Equity": ["SQQQ", "SPXU", "SOXS", "JDST"],
90 "Derivative Income": ["JEPI", "JEPQ", "QYLD"],
91 "Large Value": ["SCHD", "VYM", "DVY", "HDV"],
92}
93 
94def etf_premium_snapshot(ticker_symbol):
95 ticker = yf.Ticker(ticker_symbol)
96 info = ticker.info
97 
98 # Verify this is an ETF
99 quote_type = info.get("quoteType", "")
100 if quote_type != "ETF":
101 return {"error": f"{ticker_symbol} is not an ETF (quoteType={quote_type})"}
102 
103 price = info.get("regularMarketPrice") or info.get("previousClose")
104 nav = info.get("navPrice")
105 
106 if not price or not nav or nav <= 0:
107 return {"error": f"NAV data not available for {ticker_symbol}"}
108 
109 premium_pct = (price - nav) / nav * 100
110 premium_dollar = price - nav
111 
112 # Additional context
113 result = {
114 "ticker": ticker_symbol,
115 "name": info.get("longName") or info.get("shortName", ""),
116 "market_price": round(price, 4),
117 "nav": round(nav, 4),
118 "premium_discount_pct": round(premium_pct, 4),
119 "premium_discount_dollar": round(premium_dollar, 4),
120 "status": "PREMIUM" if premium_pct > 0 else "DISCOUNT" if premium_pct < 0 else "AT NAV",
121 "category": info.get("category", "N/A"),
122 "fund_family": info.get("fundFamily", "N/A"),
123 "total_assets": info.get("totalAssets"),
124 "net_expense_ratio": info.get("netExpenseRatio"),
125 "avg_volume": info.get("averageVolume"),
126 "bid": info.get("bid"),
127 "ask": info.get("ask"),
128 "yield_pct": info.get("yield"),
129 "ytd_return": info.get("ytdReturn"),
130 }
131 
132 # Bid-ask spread as context for whether the premium is meaningful
133 bid = info.get("bid")
134 ask = info.get("ask")
135 if bid and ask and bid > 0:
136 spread_pct = (ask - bid) / ((ask + bid) / 2) * 100
137 result["bid_ask_spread_pct"] = round(spread_pct, 4)
138 
139 return result
140```
141 
142### A2: Fetch peer comparison
143 
144After 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.
145 
146Use 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.
147 
148Present 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.
149 
150### A3: Interpret the result
151 
152Use this framework to explain whether the premium/discount is meaningful:
153 
154| Premium/Discount | Interpretation |
155|---|---|
156| Within +/- 0.05% | Essentially at NAV — normal for large, liquid ETFs |
157| +/- 0.05% to 0.25% | Minor deviation — common and usually not actionable |
158| +/- 0.25% to 1.0% | Notable — worth mentioning. Check bid-ask spread and category |
159| +/- 1.0% to 3.0% | Significant — common for less liquid, international, or specialty ETFs |
160| Beyond +/- 3.0% | Large — may indicate stress, illiquidity, or structural issues |
161 
162**Context matters by category:**
163- **US large-cap equity** (SPY, QQQ, IVV): premiums > 0.10% are unusual
164- **Bond ETFs** (AGG, HYG, LQD, TLT): discounts of 0.5-2% happen during volatility
165- **International/EM** (EEM, VWO, KWEB): time-zone mismatch causes regular 0.3-1% deviations
166- **Leveraged/Inverse** (TQQQ, SQQQ, JNUG): 0.3-1.5% is normal due to daily reset mechanics
167- **Crypto** (IBIT, BITO): 1-3% premiums are common, especially for newer funds
168- **Commodity** (GLD, USO, UNG): depends on contango/backwardation in futures
169 
170Also compare the premium/discount to the **bid-ask spread**: if the premium is smaller than the spread, it's noise, not signal.
171 
172---
173 
174## Sub-Skill B: Multi-ETF Comparison
175 
176**Goal**: Compare premium/discount across multiple ETFs side by side.
177 
178### B1: Fetch and rank
179 
180```python
181import yfinance as yf
182import pandas as pd
183 
184def compare_etf_premiums(tickers):
185 rows = []
186 for sym in tickers:
187 try:
188 t = yf.Ticker(sym)
189 info = t.info
190 if info.get("quoteType") != "ETF":
191 rows.append({"ticker": sym, "error": "Not an ETF"})
192 continue
193 price = info.get("regularMarketPrice") or info.get("previousClose")
194 nav = info.get("navPrice")
195 if price and nav and nav > 0:
196 prem = (price - nav) / nav * 100
197 bid = info.get("bid", 0)
198 ask = info.get("ask", 0)
199 spread = (ask - bid) / ((ask + bid) / 2) * 100 if bid and ask and bid > 0 else None
200 rows.append({
201 "ticker": sym,
202 "name": info.get("shortName", ""),
203 "price": round(price, 2),
204 "nav": round(nav, 2),
205 "premium_pct": round(prem, 4),
206 "spread_pct": round(spread, 4) if spread else None,
207 "category": info.get("category", "N/A"),
208 "total_assets": info.get("totalAssets"),
209 })
210 else:
211 rows.append({"ticker": sym, "error": "NAV unavailable"})
212 except Exception as e:
213 rows.append({"ticker": sym, "error": str(e)})
214 
215 df = pd.DataFrame(rows)
216 if "premium_pct" in df.columns:
217 df = df.sort_values("premium_pct", ascending=True)
218 return df
219```
220 
221### B2: Present as a ranked table
222 
223Sort by premium/discount (most discounted first). Highlight:
224- Which ETFs are at the deepest discount
225- Which are at the highest premium
226- Whether the premium/discount exceeds the bid-ask spread (if it doesn't, it's market microstructure noise)
227 
228---
229 
230## Sub-Skill C: Premium Screener
231 
232**Goal**: Scan a universe of common ETFs to find those with the largest premiums or discounts.
233 
234### C1: Define the universe and scan
235 
236Use 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.
237 
238### C2: Present the results
239 
240Show a ranked table sorted by premium (most discounted first). Group by category if the list is long. Call out:
241- **Top 5 deepest discounts** — potential buying opportunities (or signs of stress)
242- **Top 5 highest premiums** — overpaying risk
243- **Category patterns** — are all bond ETFs at a discount? Are all crypto ETFs at a premium?
244 
245Warn that large universes may take 1-2 minutes.
246 
247---
248 
249## Sub-Skill D: Premium Deep Dive
250 
251**Goal**: Combine premium/discount data with additional context to help the user understand *why* the premium exists and whether it's likely to persist.
252 
253### D1: Gather comprehensive data
254 
255Run the Sub-Skill A snapshot, then pull three months of daily history and add:
256 
257- Annualized volatility: `std(daily returns) * sqrt(252)`
258- Average daily dollar volume: `mean(close * volume)`
259- Percentage distance from the three-month closing high
260- AUM, expense ratio, yield, YTD return, and three-year beta
261- Bid-ask spread percentage and whether the absolute premium exceeds that spread
262 
263Keep unavailable fields as `null` rather than inventing values. Timestamp price and NAV inputs so users can judge whether the comparison is synchronized.
264 
265### D2: Explain the *why*
266 
267After gathering data, explain the premium/discount using this diagnostic framework:
268 
269**Common causes of premiums:**
270- **Demand surge** — more buyers than authorized participants can create shares (common for new/hot ETFs like crypto)
271- **Time-zone mismatch** — international ETF trading when underlying markets are closed; price reflects anticipated moves
272- **Creation mechanism bottleneck** — when authorized participants face constraints on creating new shares
273- **Sentiment premium** — retail demand pushes price above fair value during hype cycles
274 
275**Common causes of discounts:**
276- **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
277- **Redemption pressure** — heavy outflows but slow authorized participant response
278- **Stale NAV** — the official NAV may not reflect after-hours news or events
279- **Structural issues** — contango in futures-based ETFs (USO, UNG) creates persistent drag
280 
281**Is the premium likely to persist?**
282- For liquid US equity ETFs: No — arbitrage corrects deviations within minutes
283- For bond ETFs during stress: Discounts can persist for days or weeks
284- For crypto ETFs: Premiums tend to narrow as the fund matures and APs become more active
285- For international ETFs: Resets daily as underlying markets open
286 
287---
288 
289## Sub-Skill E: Premium Surge Decomposition (Gamma Squeeze Analysis)
290 
291**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.
292 
293This sub-skill is appropriate when the user reports or asks about:
294- An ETF moving 5%+ in a single session
295- A divergence between the ETF and its named underlyings (e.g., "MSTR jumped 13% but BTC only rose 3%")
296- A suspected gamma squeeze in an ETF or single name
297- Whether dealer hedging is amplifying a move
298 
299Read `references/gamma_squeeze_reference.md` for the full GEX formula derivation, dealer-positioning conventions, and worked examples before running E2.
300 
301### E1: Decompose today's move into NAV-driven vs excess premium
302 
303The 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:
304 
305```text
306NAV proxy return = sum(weight_i x return_i) / covered weight
307Excess premium return = ETF return - NAV proxy return
308```
309 
310Report holdings coverage and the per-holding returns used. If `funds_data.top_holdings` is incomplete, prefer issuer-published holdings or user-supplied weights.
311 
312**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.
313 
314### E2: Compute dealer gamma exposure (GEX) from the options chain
315 
316GEX 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.
317 
318Return 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.
319 
320Interpret the output:
321 
322- **`net_gex_squeezemetrics_$` highly negative** → dealers are short gamma; rallies will be amplified by their hedging buys. Classic gamma-squeeze fuel.
323- **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.
324- **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.
325- **Call/Put OI ratio > 2.5** → call-heavy positioning, consistent with a bullish gamma squeeze setup.
326 
327### E3: Compare structural buying pressure to actual volume
328 
329Estimate the upper-bound dealer share with:
330 
331```text
332Implied dealer-driven dollars = abs(GEX per 1% move) x abs(ETF return in percentage points)
333Dealer share of volume = implied dealer-driven dollars / (close x volume)
334```
335 
336This 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.
337 
338### E4: Assess premium convergence timeline
339 
340The article's three-tier convergence framework:
341 
342| Time scale | Mechanism | What to check |
343|---|---|---|
344| **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)? |
345| **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? |
346| **Weeks** | Net flow normalization | Is the ETF receiving large daily inflows (signals demand outpacing creation capacity)? Is short interest building (potential additional squeeze fuel)? |
347 
348For 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.
349 
350### E5: Present the decomposition
351 
352Format the answer in this order:
353 
3541. **Headline number**: today's ETF move, NAV-proxy move, and the excess premium (in pp).
3552. **Decomposition table**:
356 
357 | Component | Contribution |
358 |---|---|
359 | NAV-driven (holdings × weights) | +X.X% |
360 | Excess premium (residual) | +Y.Y% |
361 | Total ETF move | +Z.Z% |
362 
3633. **Dealer hedging quantification**:
364 - Net GEX (SqueezeMetrics convention)
365 - Implied dealer $ buying for the day vs actual $ volume
366 - Estimated dealer share of buying pressure
3674. **Risk indicators**: ATM IV, call/put OI ratio, top-3 strike/expiration concentrations.
3685. **Convergence outlook**: list each of the hours/days/weeks mechanisms with the current state of each.
3696. **Caveats**: the GEX estimate assumes uniform dealer positioning; the NAV proxy is stale during overnight sessions; this is *not* a forecast of future price.
370 
371---
372 
373## Step 3: Respond to the User
374 
375### Always include
376- The **ETF name and ticker**
377- **Market price** and **NAV** with the calculation shown
378- **Premium/discount percentage** clearly labeled
379- **Context**: is this deviation normal for this ETF category?
380 
381### Always caveat
382- NAV data from Yahoo Finance reflects the **most recent official NAV** (typically end of prior trading day) — it is not real-time
383- Market price may have a **15-minute delay** depending on the exchange
384- Premium/discount can change rapidly during market hours — this is a snapshot, not a live feed
385- Small premiums/discounts (< bid-ask spread) are **market microstructure noise**, not real mispricing
386- **Never recommend buying or selling** based on premium/discount alone — present the data and let the user decide
387 
388### Formatting
389- Use markdown tables for multi-ETF comparisons
390- Show the formula: `Premium/Discount = (Market Price - NAV) / NAV x 100`
391- Use color indicators in text: "trading at a **0.45% discount**" or "at a **1.2% premium**"
392- Round percentages to 2-4 decimal places depending on magnitude
393 
394---
395 
396## Reference Files
397 
398- `references/etf_premium_reference.md` — Detailed formulas, category-specific benchmarks, common ETF universe list, and background on the creation/redemption mechanism that drives premiums
399- `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.
400 
401Read the reference files for deeper technical detail on ETF premium/discount mechanics, historical context, and the gamma-squeeze decomposition methodology.
402 

Discussion