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

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

Viktoria Chapov

Viktoria Chapov

Product & Education

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

Term map

Options-data vocabulary for this article

Read chains, contracts, quote freshness, trade tape context, Greeks, implied volatility, open interest, and entitlement gates as separate data objects. That vocabulary keeps an options-data workflow precise when it moves from docs to scanners, dashboards, and historical research.

Follow the linked definitions for Option chain snapshot, Contract snapshot, Volume/OI pressure, Options flow false positive, Scanner artifact, Historical REST window, Backfill, DTE bucket, Moneyness band, Quote condition, Trade condition, and IV skew.

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.

Data terminology that keeps the workflow honest

This workflow should treat the Option chain snapshot as the broad surface, then use a Contract snapshot only after the exact row is selected. The Options data API path should preserve the OCC option symbol, OCC root, expiration, strike, side, DTE bucket, Moneyness band, open interest, volume, and quote state so the result is not reduced to a ticker-level opinion.

Activity signals need the same discipline. Volume/OI pressure can highlight a contract, but it is not proof of direction. An Options flow false positive can come from cheap far-OTM volume, event hedging, rolls, wide markets, stale quotes, or missing context. A Scanner artifact should therefore store the selected contract, score inputs, rejected contracts, timestamp window, and the fields that explain why the row was shown.

Quote and trade language must stay separate in the body of the implementation. A Quote condition explains the displayed bid and ask market; a Trade condition explains a print. Quote/trade condition fields and Quote vs trade semantics prevent the code from using last sale as a fill price when the Bid/ask spread or Midpoint says the market was not actually tradable.

Historical review needs a Historical REST window, REST snapshot, Backfill policy, Pagination cursor handling, and clear Entitlement gate labels. OPRA-originating data, WebSocket stream updates, and delayed or live access can all produce different operational states. Those states belong in logs and UI labels because the same contract can be useful for a delayed research dashboard and unsafe for live routing.

Risk displays should keep IV skew, 0DTE contract handling, Greeks, quote age, spread percent, and timestamp semantics visible. Those fields explain why one contract belongs in a scanner row while another belongs in a reject table. When the terminology is used this way, the article gives readers implementation rules rather than a long list of market-data labels.

For a put/call ratio scanner, the terminology changes how the signal is interpreted. The scanner should show whether call volume and put volume came from the same expiration set, whether contracts were filtered by DTE bucket or Moneyness band, and whether rejected rows failed quote quality or entitlement checks. That turns PCR from a sentiment headline into a reproducible scanner artifact.

Scanner acceptance checks

A production put/call ratio scanner should store more than the final ratio. Keep the call volume, put volume, expiration filter, contract count, rejected rows, quote freshness, and timestamp window used for each ticker. That makes it clear whether the ratio came from liquid contracts across a normal expiration set or from a narrow set of noisy contracts.

The scanner should also separate market-wide sentiment from contract-level alerts. A ticker-level PCR can be useful for watchlist ranking, but an individual trade review still needs the exact contract, bid/ask spread, open interest, and quote or trade conditions. The ratio says where to look; the contract evidence says whether the row deserves action.

When the scanner runs on a schedule, keep a daily artifact for each ticker. Include the watchlist version, session date, expiration filters, total contracts scanned, API response envelope, pagination cursor state, and every threshold used to label bullish, bearish, or neutral sentiment. That audit trail makes the ratio comparable across days and avoids treating a missing page, stale entitlement, or thin overnight market as a genuine sentiment shift.

Ratio rows need contract evidence

A put/call ratio is a ticker-level summary, but the rows underneath are contract-level market data. Store the chain snapshot timestamp, expiration set, DTE filter, moneyness filter, contract count, call volume, put volume, open interest, and rejected contracts. If the scanner excludes contracts with stale quotes, wide spreads, no bid, or missing entitlement, include those counts next to the ratio.

The quote fields matter even when PCR is based on volume. A ticker can show bearish put volume while the selected puts have a thin top-of-book market or a quote condition that makes the row poor execution evidence. Keeping bid, ask, bid size, ask size, quote freshness, and trade condition in the artifact tells the reader whether the ratio is a sentiment clue or a tradeable setup.

For larger watchlists, the scanner should also track request budget. Keep rate-limit usage, response envelope fields, pagination cursor state, and backfill windows. If one ticker is scanned with a complete chain and another is scanned after a cursor failure, their PCR values should not be compared as if the inputs were identical.

The same artifact should support replay. A developer should be able to rerun a day by loading the watchlist, expiration filters, chain snapshots, quote filters, trade filters, and thresholds from the saved record. If the replay produces a different bullish or bearish label, the scanner should show whether the drift came from new open interest, corrected trades, changed quote conditions, or a different entitlement state.

For live monitors, show the artifact age beside the ratio. A PCR value built from a complete realtime chain should not look the same as one built from delayed rows, missing expirations, or contracts excluded by quote-quality filters.

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

Terminology

Market-data terms used in this article

These terms keep the article connected to the CuteMarkets knowledge base and to the exact API workflow behind the research.

Option chain snapshot

The current breadth view for an underlying across expirations, strikes, Greeks, IV, OI, quotes, and trades.

Contract snapshot

The focused one-leg view after a chain scanner or user selects an exact contract.

Volume/OI pressure

Same-day option volume divided by prior open interest, used as an attention filter rather than proof of new positioning.

Options flow false positive

A scanner row that looks meaningful but weakens after spread, quote age, event, trade, or structure checks.

Scanner artifact

The saved contract, score, volume, OI, premium, quote, trade, tag, and reject record behind an alert.

Historical REST window

A timestamp-bounded request for quotes, trades, contracts, or bars used to rebuild a past market state.

Backfill

A REST request used after a stream gap, retry, or missing cache hit to repair an interval explicitly.

DTE bucket

A days-to-expiration grouping such as 0DTE, weekly, monthly, LEAPS, or event-window contracts.

Moneyness band

The ITM, ATM, or OTM relationship between strike, contract side, underlying price, and delta.

Quote condition

A code attached to a bid/ask update that affects whether it belongs in scanners, backtests, or displayed state.

Trade condition

A code attached to a print that affects whether the last sale is regular, corrected, excluded, or only contextual.

IV skew

The shape of implied volatility across strikes or expirations, usually read with Greeks and term-structure context.

0DTE contract

An option that expires the same trading day and needs tighter spread, quote-age, and session-state controls.

OCC root

The symbol root inside the OCC option identifier, which can differ from casual ticker text in adjusted or special cases.

Options data API

The product surface for chains, contracts, quotes, trades, aggregates, Greeks, IV, open interest, and expirations.

OPRA-originating data

The U.S. listed-options source context behind quotes, trades, exchange participation, and consolidated option-market records.

OCC option symbol

The exact option contract identifier that preserves root, expiration, call or put side, and strike.

Bid/ask spread

The execution interval between bid and ask that determines whether a contract is realistically tradable.

Midpoint

The computed center between bid and ask, useful as a reference price but not proof that an order would fill.

Quote/trade condition

The condition-code, exchange, correction, sequence, and timestamp context that explains how a quote or trade row can be used.

Quote vs trade semantics

The distinction between executable bid/ask markets, printed transactions, and bar-level summaries.

REST snapshot

A reproducible request for current or historical market state, used for initialization, backfills, and audit logs.

WebSocket stream

A persistent live connection that needs subscription topics, reconnect tracking, freshness labels, and REST repair paths.

Entitlement gate

The product, plan, quote, live, delayed, historical, or commercial-use boundary checked before data is shown.

Viktoria Chapov

Written by

Viktoria Chapov

Product & Education

Viktoria writes the approachable side of CuteMarkets: product updates, practical tutorials, market context, and beginner-friendly API workflows.