Build an Unusual Options Activity Scanner With Python and Free Data

Last month I noticed something odd: SMCI options volume spiked to 8x its 20-day average on a random Tuesday afternoon. No news. No earnings. Three days later, the stock jumped 14% on a surprise partnership announcement. Someone knew.

Unusual options activity (UOA) — when volume on a specific contract explodes beyond normal levels — is one of the most reliable signals that informed money is positioning. Services like Unusual Whales and Cheddar Flow charge $40-80/month to show you this data. I built my own scanner for free in about 200 lines of Python.

What Counts as "Unusual"

Before writing code, you need a working definition. I use three filters:

  1. Volume/Open Interest ratio > 3.0 — When daily volume on a contract is 3x or more the existing open interest, that’s new money entering, not existing positions rolling.
  2. Premium > $25,000 — Filters out noise. A retail trader buying 5 contracts of a cheap OTM option isn’t a signal.
  3. Days to expiration between 7-90 — Too short means gamma scalping. Too long means it’s likely a hedge, not a directional bet.

These aren’t perfect — no filter is. But they eliminate about 95% of the noise and leave you with 10-30 actionable alerts per day instead of thousands.

The Data Problem (and Three Free Solutions)

Options data is expensive. Real-time feeds from OPRA cost thousands per month. But for a daily scanner that runs after market close, you don’t need real-time. Here are three approaches I tested:

Option 1: Tradier Sandbox API (My Pick)

Tradier offers a free sandbox API that includes delayed options chains with volume and open interest. The delay is 15 minutes, which is fine for an end-of-day scanner. Rate limit: 120 requests/minute on the free tier.

import requests

TRADIER_TOKEN = "YOUR_SANDBOX_TOKEN"  # Free at developer.tradier.com
BASE = "https://sandbox.tradier.com/v1"
HEADERS = {
    "Authorization": f"Bearer {TRADIER_TOKEN}",
    "Accept": "application/json"
}

def get_options_chain(symbol: str) -> list[dict]:
    # First get expiration dates
    exp_url = f"{BASE}/markets/options/expirations"
    resp = requests.get(exp_url, headers=HEADERS, params={"symbol": symbol})
    dates = resp.json()["expirations"]["date"]

    all_contracts = []
    for exp_date in dates[:6]:  # Next 6 expirations
        chain_url = f"{BASE}/markets/options/chains"
        params = {"symbol": symbol, "expiration": exp_date}
        resp = requests.get(chain_url, headers=HEADERS, params=params)
        options = resp.json().get("options", {}).get("option", [])
        all_contracts.extend(options)

    return all_contracts

Each contract in the response includes volume, open_interest, last, and option_type. That’s everything you need.

Option 2: Yahoo Finance (yfinance)

The yfinance library pulls options data directly. No API key needed. The catch: it’s slow (one request per ticker) and Yahoo occasionally rate-limits aggressive scraping.

import yfinance as yf

ticker = yf.Ticker("AAPL")
for exp_date in ticker.options[:6]:
    chain = ticker.option_chain(exp_date)
    calls = chain.calls  # DataFrame with volume, openInterest, etc.
    puts = chain.puts

I used this initially but switched to Tradier. Yahoo’s data occasionally has gaps — missing volume on contracts that clearly traded — and the rate limiting makes scanning 100+ symbols painful.

Option 3: Polygon.io Free Tier

Polygon.io gives you 5 API calls/minute on the free tier. That’s rough for options scanning since you need one call per expiration per symbol. I’d only recommend this if you’re scanning fewer than 20 symbols.

The Scanner: 200 Lines That Actually Work

Here’s the core logic. I run this daily at 4:30 PM ET via cron.

from datetime import datetime, timedelta

def scan_unusual(contracts: list[dict], min_vol_oi: float = 3.0,
                 min_premium: float = 25000, max_dte: int = 90) -> list[dict]:
    """Filter options contracts for unusual activity."""
    today = datetime.now()
    unusual = []

    for c in contracts:
        volume = c.get("volume", 0) or 0
        oi = c.get("open_interest", 0) or 0
        last_price = c.get("last", 0) or 0

        # Skip dead contracts
        if volume == 0 or last_price == 0:
            continue

        # Calculate days to expiration
        exp = datetime.strptime(c["expiration_date"], "%Y-%m-%d")
        dte = (exp - today).days
        if dte < 7 or dte > max_dte:
            continue

        # Volume/OI ratio (handle zero OI)
        vol_oi = volume / max(oi, 1)
        if vol_oi < min_vol_oi:
            continue

        # Estimated premium (volume * last * 100 shares per contract)
        premium = volume * last_price * 100
        if premium < min_premium:
            continue

        unusual.append({
            "symbol": c["underlying"],
            "type": c["option_type"],
            "strike": c["strike"],
            "expiry": c["expiration_date"],
            "volume": volume,
            "oi": oi,
            "vol_oi": round(vol_oi, 1),
            "premium": round(premium),
            "dte": dte
        })

    # Sort by premium descending - biggest bets first
    return sorted(unusual, key=lambda x: x["premium"], reverse=True)

Scanning a Watchlist

I scan the S&P 100 plus about 40 high-beta names I track. The full scan takes ~8 minutes with Tradier’s rate limit (120 req/min), which is fine for a post-market script.

import time

WATCHLIST = ["AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "META", "GOOGL",
             "AMD", "SMCI", "PLTR", "MARA", "COIN", "ARM", "SNOW"]
# ... plus the rest of your list

all_unusual = []
for symbol in WATCHLIST:
    try:
        contracts = get_options_chain(symbol)
        hits = scan_unusual(contracts)
        all_unusual.extend(hits)
        time.sleep(0.5)  # Be nice to the API
    except Exception as e:
        print(f"Error scanning {symbol}: {e}")

# Top 20 by premium
for alert in all_unusual[:20]:
    print(f"{alert['symbol']} {alert['type'].upper()} "
          f"${alert['strike']} {alert['expiry']} | "
          f"Vol: {alert['volume']:,} OI: {alert['oi']:,} "
          f"Ratio: {alert['vol_oi']}x | "
          f"Premium: ${alert['premium']:,}")

Sample output from a recent run:

NVDA CALL $135 2026-04-18 | Vol: 42,891 OI: 8,234 Ratio: 5.2x | Premium: $18,432,230
TSLA PUT $230 2026-04-25 | Vol: 18,445 OI: 3,102 Ratio: 5.9x | Premium: $7,921,350
AMD CALL $165 2026-05-16 | Vol: 11,203 OI: 2,876 Ratio: 3.9x | Premium: $3,584,960

Making It Useful: Alerts and Context

Raw UOA data is a starting point, not a strategy. I add two things to make the output actionable:

1. Sentiment context. Are the unusual options mostly calls or puts? If 80% of the premium on a ticker is calls, bullish. If puts dominate, bearish. I calculate a simple call/put premium ratio per symbol.

from collections import defaultdict

def sentiment_summary(alerts: list[dict]) -> dict:
    by_symbol = defaultdict(lambda: {"call_premium": 0, "put_premium": 0})
    for a in alerts:
        key = "call_premium" if a["type"] == "call" else "put_premium"
        by_symbol[a["symbol"]][key] += a["premium"]

    summary = {}
    for sym, data in by_symbol.items():
        total = data["call_premium"] + data["put_premium"]
        if total > 0:
            bull_pct = data["call_premium"] / total * 100
            summary[sym] = {
                "bullish_pct": round(bull_pct),
                "total_premium": total
            }
    return summary

2. Delivery. I push the top alerts to a Telegram channel using a bot. You could also use ntfy.sh (free, self-hostable) or plain email via smtplib.

What I Learned Running This for 6 Months

A few hard-earned observations:

  • UOA predicts direction roughly 60% of the time. That’s better than a coin flip, but it’s not magic. Don’t bet the farm on any single alert.
  • Sector clustering matters more than individual signals. When you see unusual call activity across 5 semiconductor names on the same day, that’s more meaningful than a single NVDA spike.
  • Earnings week is noise. I exclude any ticker with earnings within 5 trading days. The UOA around earnings is mostly people buying lottery tickets, not informed positioning.
  • Friday afternoon sweeps are the best signals. Big money placing bets late Friday when retail has checked out? That often moves Monday-Tuesday.

The Full Setup on a Raspberry Pi

My scanner runs on a Raspberry Pi 5 that also handles my other homelab scripts. Total resource usage: ~40MB RAM, finishes in under 10 minutes. Cron triggers it at 4:30 PM ET, and I get a Telegram notification with the day’s unusual activity by 4:40 PM.

If you want a more portable development environment, a Samsung T7 portable SSD makes it easy to carry your full dev setup between machines — I keep my Python environments and data on one so I can plug into any workstation.

For going deeper on the quantitative side, Python for Finance by Yves Hilpisch is the best resource I’ve found for turning signals like these into a backtestable strategy. It covers everything from data handling to options pricing models.

Should You Actually Trade on UOA?

Honestly? Maybe. I use it as one input alongside technicals and macro. The signals are real — informed money does move through the options market before news drops. But “informed” doesn’t mean “always right,” and options flow data is increasingly gamed by sophisticated players who know retail is watching.

The real value for me has been understanding market sentiment. When I see aggressive call buying across financials before an FOMC meeting, that tells me something about positioning — even if I don’t trade it directly.

If you want daily market intelligence covering signals like these, I run a free Telegram channel: Join Alpha Signal for free market analysis, sector rotation tracking, and macro breakdowns.

The full scanner code is about 200 lines. I’m considering open-sourcing it — if there’s interest, I’ll throw it on GitHub. For now, the snippets above give you everything you need to build your own.

Related: Track Congress Trades with Python | Insider Trading Detector with Python | Algorithmic Trading for Engineers

Full disclosure: Amazon links above are affiliate links.

📧 Get weekly insights on security, trading, and tech. No spam, unsubscribe anytime.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Also by us: StartCaaS — AI Company OS · Hype2You — AI Tech Trends