HomeBlogBuild a Put/Call Ratio Scanner in Under 50 Lines of Python
TutorialFebruary 28, 2026·10 min read

Build a Put/Call Ratio Scanner in Under 50 Lines of Python

CuteMarkets

CuteMarkets Team

Developer Advocacy

Build a Put/Call Ratio Scanner in Under 50 Lines of Python

What is Put/Call Ratio?

The put/call ratio (PCR) compares the volume of put options traded to call options traded over a given period:

PCR = Put Volume / Call Volume

A PCR above 1.0 means more puts than calls are being traded, signaling bearish sentiment. Below 0.7 signals bullish excess. The indicator is most powerful when it reaches extremes, which often coincide with short-term reversals due to crowded positioning.

The Goal

We want a script that:

  1. Accepts a watchlist of tickers.
  2. Fetches today's option chain for each ticker using the official cutemarkets-python library.
  3. Calculates put/call ratio by volume.
  4. Flags tickers where PCR is unusually high or low.
  5. Runs in under 10 seconds on a 20-stock watchlist.

The Full Script

First, install the Python module:

pip install cutemarkets-python

Then, we can set up the scanner:

import cutemarkets
from datetime import date

API_KEY   = "cm_your_key_here"
WATCHLIST = ["SPY", "QQQ", "AAPL", "TSLA", "NVDA",
             "AMZN", "MSFT", "META", "GOOGL", "AMD"]

BEARISH_THRESHOLD = 1.20   # flag if PCR > this
BULLISH_THRESHOLD = 0.55   # flag if PCR < this

# Initialize the client
client = cutemarkets.Client(api_key=API_KEY)

def get_pcr(ticker: str) -> float | None:
    """Return today's put/call volume ratio, or None on error."""
    try:
        today = str(date.today())
        
        # Fetch call and put chains using the SDK
        calls = client.options.get_chain(ticker=ticker, expiration=today, option_type="call")
        puts  = client.options.get_chain(ticker=ticker, expiration=today, option_type="put")
        
        # Depending on SDK parsing, data may be a dict or attribute. Assuming dictionary here.
        calls_vol = sum(c.get("volume", 0) for c in calls.get("data", []))
        puts_vol  = sum(c.get("volume", 0) for c in puts.get("data", []))

        if calls_vol == 0:
            return None
            
        return puts_vol / calls_vol
    except Exception:
        return None

def scan(watchlist: list[str]) -> None:
    print(f"{'Ticker':<8} {'PCR':>6}  Signal")
    print("-" * 30)
    for ticker in watchlist:
        pcr = get_pcr(ticker)
        if pcr is None:
            print(f"{ticker:<8} {'n/a':>6}")
            continue
        if pcr > BEARISH_THRESHOLD:
            signal = "⚠  BEARISH EXTREME"
        elif pcr < BULLISH_THRESHOLD:
            signal = "✦  BULLISH EXTREME"
        else:
            signal = "neutral"
        print(f"{ticker:<8} {pcr:>6.2f}  {signal}")


if __name__ == "__main__":
    scan(WATCHLIST)

Sample Output

Ticker    PCR  Signal
------------------------------
SPY      0.88  neutral
QQQ      0.72  neutral
AAPL     0.61  neutral
TSLA     1.34  ⚠  BEARISH EXTREME
NVDA     0.49  ✦  BULLISH EXTREME
AMZN     0.79  neutral
MSFT     0.68  neutral
META     1.05  neutral
GOOGL    0.83  neutral
AMD      1.41  ⚠  BEARISH EXTREME

In this example, TSLA and AMD are showing unusually heavy put activity, a signal that either sophisticated traders are hedging longs or taking directional short bets. NVDA shows the opposite: call buying dominance that often precedes momentum continuation.

Extending the Scanner

A few ideas to build on this foundation:

Historical comparison: Store daily PCR values and alert only when today's reading is more than 1.5 standard deviations from a 20-day rolling average. This filters out tickers that structurally trade with a skewed PCR.

Open interest weighting: Use OI rather than volume for a less noisy reading that reflects cumulative positioning rather than single-day activity.

Sector aggregation: Group your watchlist by GICS sector and compute a sector-level PCR. Broad sector bearishness is often a more reliable signal than single-stock noise.

SECTORS = {
    "Technology": ["AAPL", "MSFT", "NVDA", "AMD"],
    "Consumer":   ["TSLA", "AMZN", "META"],
    "Broad":      ["SPY", "QQQ"],
}

Performance Notes

The script above runs synchronously, which is fine for 10 tickers. For larger watchlists, switch to the built-in AsyncClient to fetch data concurrently:

import asyncio
from cutemarkets import AsyncClient

# Initialize async client
client = AsyncClient(api_key=API_KEY)

async def get_pcr_async(ticker: str):
    # same logic, using await client.options.get_chain(...)
    ...

async def scan_async(watchlist: list[str]):
    results = await asyncio.gather(*[get_pcr_async(t) for t in watchlist])
    return results

With async fetching, a 50-ticker scan completes in roughly 2–3 seconds on a standard broadband connection.

Wrapping Up

The put/call ratio is a simple but durable sentiment indicator. When powered by real-time options volume data through the cutemarkets-python library, it becomes a live market pulse you can run as a cron job, integrate into a Slack bot, or feed into a more complex signal model.

Get your free API key at cutemarkets.com/signup and run the scanner today.