etf-premium

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ETF Premium/Discount Analysis Skill

ETF溢价/折价分析技能

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.

通过yfinance调用Yahoo Finance数据,计算ETF市场价格相对于其资产净值(NAV)的溢价或折价。
重要性说明:ETF的市场价格可能与其底层资产的价值(NAV)产生偏离。以溢价买入意味着你支付的价格高于资产实际价值;以折价买入则相当于捡到了便宜。对于流动性强的美国股票ETF,这种偏离通常很小,但对于债券ETF、国际ETF、杠杆/反向产品以及加密货币ETF,尤其是在市场压力时期,偏离幅度可能会很大。
重要提示:仅供研究和教育用途,不构成财务建议。yfinance与Yahoo, Inc.无关。

Step 1: Ensure Dependencies Are Available

步骤1:确保依赖项可用

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:
python
import subprocess, sys
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "yfinance", "pandas", "numpy"])
If already installed, skip and proceed.

当前环境状态
!`python3 -c "import yfinance, pandas, numpy; print(f'yfinance={yfinance.__version__} pandas={pandas.__version__} numpy={numpy.__version__}')" 2>/dev/null || echo "DEPS_MISSING"`
如果输出为
DEPS_MISSING
,请安装所需包:
python
import subprocess, sys
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "yfinance", "pandas", "numpy"])
如果已安装,请跳过此步骤并继续。

Step 2: Route to the Correct Sub-Skill

步骤2:路由至对应子技能

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 RequestRoute ToExamples
Single ETF premium/discountSub-Skill A: Single ETF Snapshot"is SPY at a premium?", "AGG premium to NAV", "BITO premium"
Compare multiple ETFsSub-Skill B: Multi-ETF Comparison"compare bond ETF discounts", "which has bigger premium IBIT or BITO", "rank these ETFs by premium"
Screener / find extreme premiumsSub-Skill C: Premium Screener"which ETFs have biggest discount", "find ETFs trading below NAV", "premium screener"
Deep analysis with contextSub-Skill D: Premium Deep Dive"why is HYG at a discount", "is ARKK premium normal", "ETF premium analysis with context"
对用户的请求进行分类,并跳转至匹配的章节。如果用户仅一般性询问ETF的溢价或折价,未指定具体分析类型,则默认使用子技能A(单只ETF快照分析)
用户请求路由至示例
单只ETF溢价/折价子技能A:单只ETF快照分析"is SPY at a premium?", "AGG premium to NAV", "BITO premium"
多只ETF对比子技能B:多只ETF对比分析"compare bond ETF discounts", "which has bigger premium IBIT or BITO", "rank these ETFs by premium"
筛选工具/寻找极端溢价子技能C:溢价筛选工具"which ETFs have biggest discount", "find ETFs trading below NAV", "premium screener"
带上下文深度分析子技能D:溢价深度剖析"why is HYG at a discount", "is ARKK premium normal", "ETF premium analysis with context"

Defaults

默认参数

ParameterDefault
Data sourceyfinance
navPrice
field
Price field
regularMarketPrice
(falls back to
previousClose
)
Screener universeCommon ETF list by category (see Sub-Skill C)

参数默认值
数据源yfinance的
navPrice
字段
价格字段
regularMarketPrice
( fallback至
previousClose
筛选工具覆盖范围按类别划分的常见ETF列表(见子技能C)

Sub-Skill A: Single ETF Snapshot

子技能A:单只ETF快照分析

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.
目标:展示单只ETF当前的溢价/折价情况,附带正常水平的上下文信息,并进行同类ETF对比,以显示该ETF在同类中的表现。

A1: Fetch and compute

A1:获取并计算数据

python
import yfinance as yf
python
import yfinance as yf

Peer groups by category — used to automatically compare the target ETF against its closest peers

按类别划分的同类ETF组——用于自动将目标ETF与其最相近的同类进行对比

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
undefined
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
# 验证是否为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

# 附加上下文信息
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 = 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
undefined

A2: Fetch peer comparison

A2:获取同类ETF对比数据

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.
python
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.
计算目标ETF的快照数据后,查询其
category
并获取同类ETF的溢价数据。这能让用户立即了解该溢价是ETF特有的还是全市场普遍现象。
python
def get_peer_premiums(target_ticker, target_category):
    """Fetch premium/discount for peers in the same category."""
    peers = CATEGORY_PEERS.get(target_category, [])
    # 从同类列表中移除目标ETF自身
    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
在主快照数据后以小型表格形式展示同类对比结果。这有助于用户判断该溢价是其ETF独有的,还是整个类别都存在的——例如,如果所有加密货币ETF都有~1.5%的溢价,那么用户的ETF并非异常值。

A3: Interpret the result

A3:结果解读

Use this framework to explain whether the premium/discount is meaningful:
Premium/DiscountInterpretation
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.

使用以下框架解释溢价/折价是否具有意义:
溢价/折价幅度解读
+/- 0.05%以内基本与NAV持平——对于规模大、流动性强的ETF属于正常情况
+/- 0.05%至0.25%小幅偏离——常见且通常不具备可操作性
+/- 0.25%至1.0%值得关注——需提及该情况,同时查看买卖价差和类别特性
+/- 1.0%至3.0%显著偏离——在流动性较差、国际型或特殊类型ETF中较为常见
超出+/-3.0%大幅偏离——可能表明市场压力、流动性不足或结构性问题
按类别区分的上下文要点
  • 美国大盘股ETF(SPY、QQQ、IVV):溢价超过0.10%属于异常情况
  • 债券ETF(AGG、HYG、LQD、TLT):波动期间出现0.5-2%的折价是正常现象
  • 国际/新兴市场ETF(EEM、VWO、KWEB):时区差异通常会导致0.3-1%的常规偏离
  • 杠杆/反向ETF(TQQQ、SQQQ、JNUG):由于每日重置机制,0.3-1.5%的偏离属于正常情况
  • 加密货币ETF(IBIT、BITO):1-3%的溢价较为常见,尤其是新发行的基金
  • 商品ETF(GLD、USO、UNG):偏离幅度取决于期货市场的升水/贴水情况
同时需将溢价/折价与买卖价差进行对比:如果溢价幅度小于价差,则属于市场微观结构噪音,而非有效信号。

Sub-Skill B: Multi-ETF Comparison

子技能B:多只ETF对比分析

Goal: Compare premium/discount across multiple ETFs side by side.
目标:并排对比多只ETF的溢价/折价情况。

B1: Fetch and rank

B1:获取并排序数据

python
import yfinance as yf
import pandas as pd

def compare_etf_premiums(tickers):
    rows = []
    for sym in tickers:
        try:
            t = yf.Ticker(sym)
            info = t.info
            if info.get("quoteType") != "ETF":
                rows.append({"ticker": sym, "error": "Not an ETF"})
                continue
            price = info.get("regularMarketPrice") or info.get("previousClose")
            nav = info.get("navPrice")
            if price and nav and nav > 0:
                prem = (price - nav) / nav * 100
                bid = info.get("bid", 0)
                ask = info.get("ask", 0)
                spread = (ask - bid) / ((ask + bid) / 2) * 100 if bid and ask and bid > 0 else None
                rows.append({
                    "ticker": sym,
                    "name": info.get("shortName", ""),
                    "price": round(price, 2),
                    "nav": round(nav, 2),
                    "premium_pct": round(prem, 4),
                    "spread_pct": round(spread, 4) if spread else None,
                    "category": info.get("category", "N/A"),
                    "total_assets": info.get("totalAssets"),
                })
            else:
                rows.append({"ticker": sym, "error": "NAV unavailable"})
        except Exception as e:
            rows.append({"ticker": sym, "error": str(e)})

    df = pd.DataFrame(rows)
    if "premium_pct" in df.columns:
        df = df.sort_values("premium_pct", ascending=True)
    return df
python
import yfinance as yf
import pandas as pd

def compare_etf_premiums(tickers):
    rows = []
    for sym in tickers:
        try:
            t = yf.Ticker(sym)
            info = t.info
            if info.get("quoteType") != "ETF":
                rows.append({"ticker": sym, "error": "Not an ETF"})
                continue
            price = info.get("regularMarketPrice") or info.get("previousClose")
            nav = info.get("navPrice")
            if price and nav and nav > 0:
                prem = (price - nav) / nav * 100
                bid = info.get("bid", 0)
                ask = info.get("ask", 0)
                spread = (ask - bid) / ((ask + bid) / 2) * 100 if bid and ask and bid > 0 else None
                rows.append({
                    "ticker": sym,
                    "name": info.get("shortName", ""),
                    "price": round(price, 2),
                    "nav": round(nav, 2),
                    "premium_pct": round(prem, 4),
                    "spread_pct": round(spread, 4) if spread else None,
                    "category": info.get("category", "N/A"),
                    "total_assets": info.get("totalAssets"),
                })
            else:
                rows.append({"ticker": sym, "error": "NAV unavailable"})
        except Exception as e:
            rows.append({"ticker": sym, "error": str(e)})

    df = pd.DataFrame(rows)
    if "premium_pct" in df.columns:
        df = df.sort_values("premium_pct", ascending=True)
    return df

B2: Present as a ranked table

B2:以排序表格形式展示

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)

按溢价/折价幅度排序(折价最深的排在最前)。重点突出:
  • 折价最深的ETF
  • 溢价最高的ETF
  • 溢价/折价幅度是否超过买卖价差(如果未超过,则属于市场微观结构噪音)

Sub-Skill C: Premium Screener

子技能C:溢价筛选工具

Goal: Scan a universe of common ETFs to find those with the largest premiums or discounts.
目标:扫描常见ETF范围,找出溢价或折价幅度最大的ETF。

C1: Define the universe and scan

C1:定义覆盖范围并扫描

Use this default universe organized by category. The user can supply their own list instead.
python
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
使用以下按类别划分的默认覆盖范围。用户也可以提供自定义列表。
python
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

C2:展示结果

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.

以排序表格形式展示(折价最深的排在最前)。如果列表较长,可按类别分组。重点指出:
  • 折价最深的前5只ETF——潜在的买入机会(或市场压力信号)
  • 溢价最高的前5只ETF——存在支付过高价格的风险
  • 类别模式——是否所有债券ETF都处于折价状态?是否所有加密货币ETF都处于溢价状态?
注意:此筛选工具需要一定时间,因为它需要逐个获取每只ETF的数据。对于较大的覆盖范围(60只以上ETF),需提醒用户可能需要1-2分钟。

Sub-Skill D: Premium Deep Dive

子技能D:溢价深度剖析

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

D1:收集全面数据

Run the Sub-Skill A snapshot, then add:
python
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 result
运行子技能A的快照分析,然后添加以下数据:
python
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

    # 用于波动上下文的历史价格数据
    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()

        # 价格区间上下文
        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 = 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 result

D2: Explain the why

D2:解释背后原因

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

收集数据后,使用以下诊断框架解释溢价/折价的产生原因:
溢价的常见原因
  • 需求激增——买家数量超过授权参与者的份额创建能力(在加密货币等热门新ETF中常见)
  • 时区差异——国际ETF在底层市场闭市时交易,价格反映预期走势
  • 创建机制瓶颈——授权参与者在创建新份额时面临限制
  • 情绪溢价——零售需求在炒作周期中将价格推高至公允价值之上
折价的常见原因
  • 流动性压力——在抛售期间,债券和信用ETF通常会折价交易,因为底层债券的定价/交易难度比ETF本身更高
  • 赎回压力——大量资金流出,但授权参与者响应缓慢
  • NAV过时——官方NAV可能未反映盘后消息或事件
  • 结构性问题——基于期货的ETF(如USO、UNG)的升水会造成持续的折价拖累
溢价是否可能持续?
  • 对于流动性强的美国股票ETF:否——套利机制会在数分钟内修正偏离
  • 对于市场压力时期的债券ETF:折价可能持续数天或数周
  • 对于加密货币ETF:随着基金成熟和授权参与者活跃度提升,溢价通常会收窄
  • 对于国际ETF:随着底层市场开盘,每日会重置价格

Step 3: Respond to the User

步骤3:响应用户

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?
  • ETF名称和代码
  • 市场价格NAV,并展示计算过程
  • 清晰标注的溢价/折价百分比
  • 上下文信息:该偏离对于该类别ETF是否正常?

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
  • Yahoo Finance提供的NAV数据反映的是最新官方NAV(通常为前一交易日收盘时的数据)——并非实时数据
  • 市场价格可能存在15分钟延迟,具体取决于交易所
  • 溢价/折价在交易时段内可能快速变化——此为快照数据,并非实时推送
  • 小幅溢价/折价(小于买卖价差)属于市场微观结构噪音,而非真正的定价偏差
  • 绝不基于溢价/折价单独推荐买入或卖出——仅展示数据,由用户自行决策

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

  • 使用markdown表格展示多只ETF对比结果
  • 展示计算公式:
    溢价/折价 = (市场价格 - NAV) / NAV × 100
  • 在文本中使用颜色标识:“当前交易存在0.45%折价”或“当前交易存在1.2%溢价
  • 根据幅度大小,将百分比保留2-4位小数

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
Read the reference file for deeper technical detail on ETF premium/discount mechanics and historical context.
  • references/etf_premium_reference.md
    ——详细公式、类别基准、常见ETF列表,以及驱动溢价/折价的份额创建/赎回机制背景知识
阅读参考文件可深入了解ETF溢价/折价机制的技术细节和历史背景。