AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Etf Premium

skill-himself65-finance-skills-etf-premium · by himself65

>

No reviews yet
0 installs
19 views
0.0% view→install

Install

$ agentstack add skill-himself65-finance-skills-etf-premium

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution Used
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-himself65-finance-skills-etf-premium)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Etf Premium? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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 else "DISCOUNT" if premium_pct  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.

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_data

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

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?

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

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 — 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-sup

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [himself65](https://github.com/himself65)
- **Source:** [himself65/finance-skills](https://github.com/himself65/finance-skills)
- **License:** MIT
- **Homepage:** https://skills.himself65.com

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.