Build a Put/Call Ratio Scanner in Under 50 Lines of Python
CuteMarkets Team
Developer Advocacy

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:
- Accepts a watchlist of tickers.
- Fetches today's option chain for each ticker using the official cutemarkets-python library.
- Calculates put/call ratio by volume.
- Flags tickers where PCR is unusually high or low.
- 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.
Product links
Build the workflow with CuteMarkets
This article is part of the broader CuteMarkets product and research stack. Use the landing pages below to move from the blog into the specific API workflow you want to evaluate.
Options Data API
See the canonical product page for real-time and historical options data.
Historical Options Data API
Inspect the historical contracts, quotes, trades, and aggregates workflow.
Options Chain API
Go straight to chain snapshots, expirations, and strike discovery.
Pricing
Review plans before you move from free evaluation into production usage.