stock-liquidity

使用来自雅虎财经的数据,基于买卖价差、成交量分布(volume profiles)、订单簿深度、市场冲击(market impact)估计以及换手率(turnover ratios)来分析股票流动性。每当用户询问流动性、交易成本、买卖价差、市场深度、成交量分析、滑点、市场冲击、换手率,或“在不影响股价的情况下交易某只股票的难易程度”时,都运用该技能。触发条件包括:“AAPL 有多流动”、 “买卖价差”、 “成交量分析”、 “订单簿深度”、 “大额订单的市场冲击”、 “换手率”、 “滑点估计”、 “我能在不移动股价的情况下交易10万股吗”、 “流动性对比”、 “价差分析”、 “ADTV”、 “Amihud 非流动性”、 “美元成交额”、 “执行成本估计”、 “流动性评分”、以及便士股、小盘股或交易稀薄的证券。

安装

热度:0

下载并解压到你的 skills 目录

复制命令,发送给 OpenClaw 自动安装:

下载并安装这个技能 https://openskills.cc/api/download?slug=himself65-skills-stock-liquidity&locale=zh&source=copy
name:stock-liquiditydescription:>Triggers:"how liquid is AAPL", "bid-ask spread", "volume analysis", "order book depth",

Stock Liquidity Analysis Skill

Analyzes stock liquidity across multiple dimensions — bid-ask spreads, volume patterns, order book depth, estimated market impact, and turnover ratios — using data from Yahoo Finance via yfinance.

Liquidity matters because it determines the real cost of trading. The quoted price is not what you actually pay — spreads, slippage, and market impact all eat into returns, especially for larger positions or less liquid names.

Important: This is 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 for a general liquidity assessment without specifying a particular metric, run Sub-Skill A (Liquidity Dashboard) which computes all key metrics together.

User RequestRoute ToExamples
General liquidity check, "how liquid is X"Sub-Skill A: Liquidity Dashboard"how liquid is AAPL", "liquidity analysis for TSLA", "is this stock liquid enough"
Bid-ask spread, trading costs, effective spreadSub-Skill B: Spread Analysis"bid-ask spread for AMD", "what's the spread on NVDA options", "trading cost estimate"
Volume, ADTV, dollar volume, volume profileSub-Skill C: Volume Analysis"volume analysis MSFT", "average daily volume", "volume profile for SPY"
Order book depth, market depth, level 2Sub-Skill D: Order Book Depth"order book depth for AAPL", "market depth", "show me the book"
Market impact, slippage, execution cost for large ordersSub-Skill E: Market Impact"how much would 50k shares move the price", "slippage estimate", "market impact of $1M order"
Turnover ratio, trading activity relative to floatSub-Skill F: Turnover Ratio"turnover ratio for GME", "float turnover", "how actively traded is this"
Compare liquidity across multiple stocksSub-Skill A (multi-ticker mode)"compare liquidity AAPL vs TSLA", "which is more liquid AMD or INTC"

Defaults

ParameterDefault
Lookback period3mo (3 months)
Data interval1d (daily)
Market impact modelSquare-root model
Intraday interval (when needed)5m


Sub-Skill A: Liquidity Dashboard

Goal: Produce a comprehensive liquidity snapshot combining all key metrics for one or more tickers.

A1: Fetch data and compute all metrics

import yfinance as yf
import pandas as pd
import numpy as np

def liquidity_dashboard(ticker_symbol, period="3mo"):
    ticker = yf.Ticker(ticker_symbol)
    info = ticker.info
    hist = ticker.history(period=period)

    if hist.empty:
        return None

    # --- Spread metrics (from current quote) ---
    bid = info.get("bid", None)
    ask = info.get("ask", None)
    current_price = info.get("currentPrice") or info.get("regularMarketPrice") or hist["Close"].iloc[-1]

    spread = None
    spread_pct = None
    if bid and ask and bid > 0 and ask > 0:
        spread = round(ask - bid, 4)
        midpoint = (ask + bid) / 2
        spread_pct = round((spread / midpoint) * 100, 4)

    # --- Volume metrics ---
    avg_volume = hist["Volume"].mean()
    median_volume = hist["Volume"].median()
    avg_dollar_volume = (hist["Close"] * hist["Volume"]).mean()
    volume_std = hist["Volume"].std()
    volume_cv = volume_std / avg_volume if avg_volume > 0 else None  # coefficient of variation

    # --- Turnover ratio ---
    shares_outstanding = info.get("sharesOutstanding", None)
    float_shares = info.get("floatShares", None)
    base_shares = float_shares or shares_outstanding
    turnover_ratio = round(avg_volume / base_shares, 6) if base_shares else None

    # --- Amihud illiquidity ratio ---
    # Average of |daily return| / daily dollar volume
    returns = hist["Close"].pct_change().dropna()
    dollar_volume = (hist["Close"] * hist["Volume"]).iloc[1:]  # align with returns
    amihud_values = returns.abs() / dollar_volume
    amihud = amihud_values[amihud_values.replace([np.inf, -np.inf], np.nan).notna()].mean()

    # --- Market impact estimate (square-root model) ---
    # For a hypothetical order of 1% of ADV
    adv = avg_volume
    order_size = adv * 0.01
    daily_volatility = returns.std()
    sigma = daily_volatility
    participation_rate = order_size / adv if adv > 0 else 0
    impact_bps = sigma * np.sqrt(participation_rate) * 10000  # in basis points

    return {
        "ticker": ticker_symbol,
        "current_price": round(current_price, 2),
        "bid": bid,
        "ask": ask,
        "spread": spread,
        "spread_pct": spread_pct,
        "avg_daily_volume": int(avg_volume),
        "median_daily_volume": int(median_volume),
        "avg_dollar_volume": round(avg_dollar_volume, 0),
        "volume_cv": round(volume_cv, 3) if volume_cv else None,
        "shares_outstanding": shares_outstanding,
        "float_shares": float_shares,
        "turnover_ratio": turnover_ratio,
        "amihud_illiquidity": round(amihud * 1e9, 4) if not np.isnan(amihud) else None,
        "daily_volatility": round(daily_volatility * 100, 2),
        "impact_1pct_adv_bps": round(impact_bps, 2),
        "observations": len(hist),
    }

A2: Interpret and present

Present as a summary card. For the Amihud illiquidity ratio, multiply by 1e9 for readability (standard convention).

Liquidity grade (use these rough thresholds for US equities):

GradeAvg Dollar VolumeSpread (%)Amihud (×10⁹)
Very High> $500M/day< 0.03%< 0.01
High$50M–$500M/day0.03–0.10%0.01–0.1
Moderate$5M–$50M/day0.10–0.50%0.1–1.0
Low$500K–$5M/day0.50–2.00%1.0–10
Very Low< $500K/day> 2.00%> 10

When comparing multiple tickers, show a side-by-side table and highlight which is more liquid and why.


Sub-Skill B: Spread Analysis

Goal: Detailed bid-ask spread analysis including current spread, historical context from options data, and effective spread estimates.

B1: Current spread from quote

import yfinance as yf

def spread_analysis(ticker_symbol):
    ticker = yf.Ticker(ticker_symbol)
    info = ticker.info

    bid = info.get("bid", 0)
    ask = info.get("ask", 0)
    bid_size = info.get("bidSize", None)
    ask_size = info.get("askSize", None)
    current_price = info.get("currentPrice") or info.get("regularMarketPrice", 0)

    result = {"bid": bid, "ask": ask, "bid_size": bid_size, "ask_size": ask_size}

    if bid > 0 and ask > 0:
        midpoint = (bid + ask) / 2
        result["absolute_spread"] = round(ask - bid, 4)
        result["relative_spread_pct"] = round((ask - bid) / midpoint * 100, 4)
        result["relative_spread_bps"] = round((ask - bid) / midpoint * 10000, 2)
    return result

B2: Options spread context

Options data from yfinance includes bid/ask for each strike, which gives a sense of derivatives liquidity:

def options_spread_analysis(ticker_symbol):
    ticker = yf.Ticker(ticker_symbol)
    expirations = ticker.options
    if not expirations:
        return None

    # Use nearest expiration
    chain = ticker.option_chain(expirations[0])
    for label, df in [("Calls", chain.calls), ("Puts", chain.puts)]:
        atm = df[df["inTheMoney"]].tail(3).append(df[~df["inTheMoney"]].head(3))
        atm = pd.concat([df[df["inTheMoney"]].tail(3), df[~df["inTheMoney"]].head(3)])
        atm["spread"] = atm["ask"] - atm["bid"]
        atm["spread_pct"] = (atm["spread"] / ((atm["ask"] + atm["bid"]) / 2) * 100).round(2)
    return chain

B3: Present results

Show:

  • Current quoted spread (absolute, relative %, basis points)

  • Bid/ask sizes if available

  • Near-the-money options spreads for context

  • How the spread compares to typical ranges for this market cap tier

  • Sub-Skill C: Volume Analysis

    Goal: Analyze trading volume patterns — averages, trends, relative volume, and dollar volume.

    C1: Compute volume metrics

    import yfinance as yf
    import pandas as pd
    import numpy as np
    
    def volume_analysis(ticker_symbol, period="3mo"):
        ticker = yf.Ticker(ticker_symbol)
        hist = ticker.history(period=period)
    
        if hist.empty:
            return None
    
        vol = hist["Volume"]
        close = hist["Close"]
        dollar_vol = vol * close
    
        # Relative volume (today vs average)
        rvol = vol.iloc[-1] / vol.mean() if vol.mean() > 0 else None
    
        # Volume trend (linear regression slope over the period)
        x = np.arange(len(vol))
        slope, _ = np.polyfit(x, vol.values, 1) if len(vol) > 1 else (0, 0)
        trend_pct = (slope * len(vol)) / vol.mean() * 100  # % change over period
    
        # Volume profile by day of week
        hist_copy = hist.copy()
        hist_copy["DayOfWeek"] = hist_copy.index.dayofweek
        day_names = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri"}
        vol_by_day = hist_copy.groupby("DayOfWeek")["Volume"].mean()
        vol_by_day.index = vol_by_day.index.map(day_names)
    
        # High/low volume days
        high_vol_days = hist.nlargest(5, "Volume")[["Close", "Volume"]]
        low_vol_days = hist.nsmallest(5, "Volume")[["Close", "Volume"]]
    
        return {
            "avg_volume": int(vol.mean()),
            "median_volume": int(vol.median()),
            "avg_dollar_volume": round(dollar_vol.mean(), 0),
            "current_volume": int(vol.iloc[-1]),
            "relative_volume": round(rvol, 2) if rvol else None,
            "volume_trend_pct": round(trend_pct, 1),
            "volume_by_day": vol_by_day.to_dict(),
            "high_vol_days": high_vol_days,
            "low_vol_days": low_vol_days,
            "max_volume": int(vol.max()),
            "min_volume": int(vol.min()),
        }

    C2: Present results

    Show:

  • Average daily volume (shares and dollar) with median for comparison

  • Relative volume (RVOL) — today's volume vs. the average. RVOL > 1.5 is elevated; RVOL < 0.5 is unusually quiet

  • Volume trend — is trading activity increasing or declining?

  • Day-of-week pattern (if meaningful variation exists)

  • Top 5 highest-volume days with context (earnings? news?)

  • Sub-Skill D: Order Book Depth

    Goal: Estimate order book depth using available bid/ask data from the equity quote and options chain.

    Yahoo Finance does not provide full Level 2 / order book data. Be upfront about this limitation. What we can do:

  • Equity quote: bid, ask, bid size, ask size (top of book only)

  • Options chain: bid/ask and open interest across strikes give a proxy for derivatives depth

  • Intraday volume distribution: how volume is distributed within the day suggests how deep the continuous market is
  • D1: Gather available depth data

    import yfinance as yf
    import pandas as pd
    import numpy as np
    
    def order_book_proxy(ticker_symbol):
        ticker = yf.Ticker(ticker_symbol)
        info = ticker.info
    
        # Top of book
        top_of_book = {
            "bid": info.get("bid"),
            "ask": info.get("ask"),
            "bid_size": info.get("bidSize"),
            "ask_size": info.get("askSize"),
        }
    
        # Intraday volume distribution (5-min bars, last 5 days)
        intraday = ticker.history(period="5d", interval="5m")
        if not intraday.empty:
            intraday_copy = intraday.copy()
            intraday_copy["time"] = intraday_copy.index.time
            vol_by_time = intraday_copy.groupby("time")["Volume"].mean()
            # Normalize to percentage of daily volume
            total = vol_by_time.sum()
            vol_pct = (vol_by_time / total * 100).round(2) if total > 0 else vol_by_time
    
        # Options open interest as depth proxy
        expirations = ticker.options
        if expirations:
            chain = ticker.option_chain(expirations[0])
            total_call_oi = chain.calls["openInterest"].sum()
            total_put_oi = chain.puts["openInterest"].sum()
            total_call_volume = chain.calls["volume"].sum()
            total_put_volume = chain.puts["volume"].sum()
    
        return top_of_book, vol_pct if not intraday.empty else None

    D2: Present results

    Show:

  • Top of book: current bid/ask with sizes

  • Intraday volume shape: where volume concentrates (open/close vs. midday)

  • Options depth: total open interest and volume as a proxy for derivatives liquidity

  • Honest limitation: "Yahoo Finance provides top-of-book only. For full Level 2 depth, a direct market data feed (e.g., NYSE OpenBook, NASDAQ TotalView) is needed."

  • Sub-Skill E: Market Impact

    Goal: Estimate how much a given order size would move the price, using the square-root market impact model.

    The standard model in practice is: Impact (%) = σ × √(Q / V) where σ is daily volatility, Q is order size in shares, and V is average daily volume. This is a simplified version of the Almgren-Chriss framework used by institutional traders.

    E1: Compute market impact estimate

    import yfinance as yf
    import numpy as np
    
    def market_impact(ticker_symbol, order_shares=None, order_dollars=None, period="3mo"):
        ticker = yf.Ticker(ticker_symbol)
        hist = ticker.history(period=period)
        info = ticker.info
    
        if hist.empty:
            return None
    
        current_price = info.get("currentPrice") or hist["Close"].iloc[-1]
        avg_volume = hist["Volume"].mean()
        daily_volatility = hist["Close"].pct_change().dropna().std()
    
        # Determine order size in shares
        if order_dollars and not order_shares:
            order_shares = order_dollars / current_price
        elif not order_shares:
            # Default: estimate for various sizes
            order_shares = avg_volume * 0.01  # 1% of ADV
    
        participation_rate = order_shares / avg_volume if avg_volume > 0 else 0
        pct_adv = (order_shares / avg_volume * 100) if avg_volume > 0 else 0
    
        # Square-root impact model
        impact_pct = daily_volatility * np.sqrt(participation_rate) * 100
        impact_bps = impact_pct * 100
        impact_dollars = impact_pct / 100 * current_price * order_shares
    
        # Generate impact curve for multiple order sizes
        sizes = [0.001, 0.005, 0.01, 0.02, 0.05, 0.10, 0.20, 0.50]  # as fraction of ADV
        curve = []
        for s in sizes:
            q = avg_volume * s
            imp = daily_volatility * np.sqrt(s) * 100
            curve.append({
                "pct_adv": round(s * 100, 1),
                "shares": int(q),
                "dollars": round(q * current_price, 0),
                "impact_bps": round(imp * 100, 1),
                "impact_dollars_per_share": round(imp / 100 * current_price, 4),
            })
    
        return {
            "ticker": ticker_symbol,
            "current_price": round(current_price, 2),
            "avg_daily_volume": int(avg_volume),
            "daily_volatility_pct": round(daily_volatility * 100, 2),
            "order_shares": int(order_shares),
            "order_dollars": round(order_shares * current_price, 0),
            "pct_of_adv": round(pct_adv, 2),
            "estimated_impact_bps": round(impact_bps, 1),
            "estimated_impact_pct": round(impact_pct, 4),
            "estimated_impact_total_dollars": round(impact_dollars, 2),
            "impact_curve": curve,
        }

    E2: Present results

    Show:

  • The estimated impact for the user's specific order size

  • An impact curve table showing how cost scales with order size

  • Context: "This uses the square-root market impact model, a standard institutional estimate. Actual impact depends on execution strategy (VWAP, TWAP, etc.), time of day, and current market conditions."

  • If impact > 50 bps, flag that the order is large relative to liquidity and suggest the user consider algorithmic execution or splitting the order across days

  • Sub-Skill F: Turnover Ratio

    Goal: Measure how actively a stock trades relative to its shares outstanding and free float.

    F1: Compute turnover metrics

    import yfinance as yf
    import pandas as pd
    import numpy as np
    
    def turnover_analysis(ticker_symbol, period="3mo"):
        ticker = yf.Ticker(ticker_symbol)
        hist = ticker.history(period=period)
        info = ticker.info
    
        if hist.empty:
            return None
    
        avg_volume = hist["Volume"].mean()
        shares_outstanding = info.get("sharesOutstanding")
        float_shares = info.get("floatShares")
    
        result = {
            "avg_daily_volume": int(avg_volume),
            "shares_outstanding": shares_outstanding,
            "float_shares": float_shares,
        }
    
        if shares_outstanding:
            daily_turnover = avg_volume / shares_outstanding
            result["daily_turnover_ratio"] = round(daily_turnover, 6)
            result["annualized_turnover"] = round(daily_turnover * 252, 2)
            result["days_to_trade_float"] = round(
                (float_shares or shares_outstanding) / avg_volume, 1
            ) if avg_volume > 0 else None
    
        if float_shares:
            float_turnover = avg_volume / float_shares
            result["float_turnover_daily"] = round(float_turnover, 6)
            result["float_turnover_annualized"] = round(float_turnover * 252, 2)
    
        # Turnover trend
        vol = hist["Volume"]
        base = float_shares or shares_outstanding
        if base:
            hist_copy = hist.copy()
            hist_copy["turnover"] = hist_copy["Volume"] / base
            recent_turnover = hist_copy["turnover"].tail(20).mean()
            older_turnover = hist_copy["turnover"].head(20).mean()
            if older_turnover > 0:
                result["turnover_trend_pct"] = round(
                    (recent_turnover - older_turnover) / older_turnover * 100, 1
                )
    
        return result

    F2: Present results

    Show:

  • Daily and annualized turnover ratios (vs. outstanding and float)

  • "Days to trade the float" — how many days at average volume to turn over the entire free float

  • Turnover trend — is the stock becoming more or less actively traded?

  • Context:
  • Turnover (Annualized)Interpretation
    > 500%Extremely active — likely speculative or momentum-driven
    100–500%Actively traded
    30–100%Moderate activity
    < 30%Thinly traded — likely institutional buy-and-hold or neglected


    Step 3: Respond to the User

    After running the appropriate sub-skill:

    Always include

  • The lookback period used for historical metrics

  • The data timestamp — spreads and quotes are snapshots, not real-time

  • Any tickers that returned empty data (invalid symbol, delisted, etc.)
  • Always caveat

  • Yahoo Finance quote data has a 15-minute delay for most exchanges — spreads shown may not reflect the current live market

  • Full order book (Level 2) data is not available through Yahoo Finance

  • Market impact estimates are models, not guarantees — actual execution costs depend on strategy, timing, and market conditions

  • Liquidity can change rapidly — a stock that's liquid today may not be tomorrow (especially around events, halts, or during extended hours)
  • Practical guidance (mention when relevant)

  • Position sizing: If estimated impact exceeds 25 bps, the position may be too large for the stock's liquidity

  • Small/micro-cap warning: Stocks with < $1M daily dollar volume require careful execution

  • Spread costs compound: A 0.10% spread on a round-trip (buy + sell) costs 0.20% — this adds up for active strategies

  • Illiquidity premium: Less liquid stocks historically earn higher returns as compensation — but the transaction costs can eat this premium
  • Important: Never recommend specific trades. Present liquidity data and let the user make their own decisions.


    Reference Files

  • references/liquidity_reference.md — Detailed formulas, extended code templates, metric interpretation guides, and academic references for all liquidity measures
  • Read the reference file when you need exact formulas, edge case handling, or deeper background on liquidity metrics.