Finance APIs Series — Part 4 of 5

Options & Derivatives APIs — Chains, Flow & Greeks

From CBOE delayed data to Polygon.io's institutional options feed. Calculate Greeks, compute max pain, build an options scanner, and visualize IV surfaces — all with code.

Options Chains Greeks Flow Data IV Surface
Finance APIs — The Ultimate Guide4/5
OverviewOptions APIsGreeksMax PainOptions ScannerIV SurfaceComparisonKey Takeaways
The Options Data Landscape

Why Options Data Is the Smartest Money Signal

Options data is the closest thing to a crystal ball in finance. While social media tells you what retail traders say, options flow tells you what institutional money does. A $5M call sweep on AAPL expiring in 2 weeks is a far stronger signal than 10,000 bullish StockTwits posts.

The options market is roughly 40x the size of the stock market in notional value. Every trade in the options market expresses a view on direction, timing, and magnitude — information that doesn't exist in stock trades alone.

Options Data Primer

An options chain shows all available contracts for a given stock: calls and puts at every strike price, for every expiration date. Each contract has:

The Greeks — Quick Reference

Δ

Delta

Price sensitivity. How much the option moves per $1 move in the stock. Also approximates probability of expiring ITM.

Γ

Gamma

Rate of change of Delta. High near ATM and near expiration. The "acceleration" of options pricing.

Θ

Theta

Time decay. How much value the option loses per day. Accelerates near expiration.

V

Vega

Volatility sensitivity. How much the option moves per 1% change in implied volatility.

Options Data APIs

The APIs — Deep Dive

CBOE (Chicago Board Options Exchange)

Free (Delayed)

The CBOE is where options were invented. Their website provides free delayed options data including the VIX, put/call ratios, and basic options chains. For programmatic access, their market data portal offers delayed snapshots. The data is 15-20 minutes delayed but perfectly fine for end-of-day analysis and backtesting.

Pros

  • Free delayed data
  • VIX and put/call ratio
  • Authoritative source

Cons

  • No API (web scraping required)
  • 15-20 min delay
  • Limited historical data

Tradier

Free (Sandbox) Free Delayed $0 with Account

Tradier is a brokerage-as-a-service platform with an excellent free API tier. With a paper trading account (free), you get real-time options chains, streaming quotes, and order execution in sandbox mode. Delayed data is completely free with just an API key — no brokerage account needed.

Pros

  • Free delayed options chains
  • Real-time with brokerage account
  • Sandbox for paper trading
  • Excellent API design
  • Streaming via WebSocket

Cons

  • No historical options data
  • Greeks not included (calculate yourself)
  • US options only
Python
import requests

TRADIER_KEY = "YOUR_TRADIER_KEY"
headers = {"Authorization": f"Bearer {TRADIER_KEY}", "Accept": "application/json"}

# Get available expiration dates
exp_url = "https://api.tradier.com/v1/markets/options/expirations?symbol=AAPL"
expirations = requests.get(exp_url, headers=headers).json()
dates = expirations["expirations"]["date"]
print(f"Available expirations: {dates[:5]}")

# Get full options chain for nearest expiration
chain_url = f"https://api.tradier.com/v1/markets/options/chains?symbol=AAPL&expiration={dates[0]}"
chain = requests.get(chain_url, headers=headers).json()

for opt in chain["options"]["option"][:5]:
    print(f"  {opt['option_type'].upper()} {opt['strike']} — Bid: ${opt['bid']}, Ask: ${opt['ask']}, "
          f"Vol: {opt['volume']}, OI: {opt['open_interest']}, IV: {opt.get('greeks', {}).get('mid_iv', 'N/A')}")

Polygon.io Options

$29-199/mo

Polygon.io provides the most complete options data API for developers. Their Options Starter plan ($29/mo) includes real-time options chains and snapshots. The Business plan ($199/mo) adds historical options data, Greeks, and the full OPRA feed. This is the go-to for building production options analytics.

Pros

  • Full OPRA feed (real-time)
  • Historical options data
  • Greeks included
  • WebSocket streaming
  • Snapshot endpoint (all contracts at once)

Cons

  • Full data on $199/mo tier only
  • Historical data is massive (storage)
  • US options only

Unusual Whales

$40/mo

Unusual Whales specializes in options flow — not just the raw chain data, but the interpreted flow: large trades, sweeps, dark pool prints, and unusual activity. Their API identifies institutional-sized options bets and categorizes them by sentiment, size, and urgency. Essential for flow-based trading strategies.

Pros

  • Pre-interpreted flow data
  • Sweep detection (institutional trades)
  • Dark pool prints
  • Historical flow database

Cons

  • No free tier
  • API documentation is basic
  • Flow interpretation is subjective

TD Ameritrade / Schwab API

Free with Account

The TD Ameritrade (now Schwab) API provides free real-time options chains with a funded brokerage account. The data includes full Greeks, IV, and all standard chain data. Since Schwab's minimum account is $0, this is effectively free. The API is well-documented and supports OAuth 2.0.

Yahoo Finance Options

Free

Yahoo Finance (via yfinance) provides free options chain data including bid/ask, volume, open interest, and implied volatility. The data is delayed 15-20 minutes. While not suitable for real-time trading, it's perfect for end-of-day analysis, backtesting options strategies, and learning.

Python
import yfinance as yf

ticker = yf.Ticker("AAPL")

# List all expiration dates
expirations = ticker.options
print(f"Expirations: {expirations[:5]}")

# Get options chain for nearest expiration
chain = ticker.option_chain(expirations[0])

# Calls
print("\\nTop 5 Calls by Volume:")
top_calls = chain.calls.nlargest(5, "volume")
for _, c in top_calls.iterrows():
    print(f"  CALL {c['strike']} — Bid: ${c['bid']}, Ask: ${c['ask']}, "
          f"Vol: {c['volume']}, OI: {c['openInterest']}, IV: {c['impliedVolatility']:.1%}")

# Put/Call ratio
total_call_oi = chain.calls["openInterest"].sum()
total_put_oi = chain.puts["openInterest"].sum()
pcr = total_put_oi / total_call_oi if total_call_oi > 0 else 0
print(f"\\nPut/Call OI Ratio: {pcr:.2f}")
Calculating Greeks

Black-Scholes Greeks Calculator

Black-Scholes Call Price
C = S · N(d1) - K · e-rT · N(d2)
Python
import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq

class BlackScholes:
    """Black-Scholes Greeks calculator."""

    @staticmethod
    def d1(S, K, T, r, sigma):
        return (np.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))

    @staticmethod
    def d2(S, K, T, r, sigma):
        return BlackScholes.d1(S, K, T, r, sigma) - sigma * np.sqrt(T)

    @staticmethod
    def call_price(S, K, T, r, sigma):
        d1 = BlackScholes.d1(S, K, T, r, sigma)
        d2 = BlackScholes.d2(S, K, T, r, sigma)
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)

    @staticmethod
    def put_price(S, K, T, r, sigma):
        d1 = BlackScholes.d1(S, K, T, r, sigma)
        d2 = BlackScholes.d2(S, K, T, r, sigma)
        return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)

    @staticmethod
    def greeks(S, K, T, r, sigma, option_type="call"):
        d1 = BlackScholes.d1(S, K, T, r, sigma)
        d2 = BlackScholes.d2(S, K, T, r, sigma)
        sqrt_T = np.sqrt(T)

        # Delta
        delta = norm.cdf(d1) if option_type == "call" else norm.cdf(d1) - 1

        # Gamma (same for calls and puts)
        gamma = norm.pdf(d1) / (S * sigma * sqrt_T)

        # Theta (per day)
        common = -(S * norm.pdf(d1) * sigma) / (2 * sqrt_T)
        if option_type == "call":
            theta = common - r * K * np.exp(-r * T) * norm.cdf(d2)
        else:
            theta = common + r * K * np.exp(-r * T) * norm.cdf(-d2)
        theta /= 365  # per day

        # Vega (per 1% IV change)
        vega = S * sqrt_T * norm.pdf(d1) / 100

        return {"delta": delta, "gamma": gamma, "theta": theta, "vega": vega}

    @staticmethod
    def implied_volatility(market_price, S, K, T, r, option_type="call"):
        """Solve for IV using Brent's method."""
        price_fn = BlackScholes.call_price if option_type == "call" else BlackScholes.put_price
        try:
            iv = brentq(lambda s: price_fn(S, K, T, r, s) - market_price, 0.01, 5.0)
            return iv
        except:
            return float('nan')

# Example: AAPL $230 call, 30 days to expiry
S = 228.50   # Current price
K = 230.00   # Strike
T = 30/365   # Time to expiry (years)
r = 0.045    # Risk-free rate
sigma = 0.25 # Implied volatility (25%)

price = BlackScholes.call_price(S, K, T, r, sigma)
greeks = BlackScholes.greeks(S, K, T, r, sigma, "call")

print(f"Call Price: ${price:.2f}")
print(f"Delta: {greeks['delta']:.4f}")
print(f"Gamma: {greeks['gamma']:.4f}")
print(f"Theta: ${greeks['theta']:.4f}/day")
print(f"Vega: ${greeks['vega']:.4f}/1% IV")
Max Pain Calculation

Computing Max Pain from Open Interest

What Is Max Pain?

Max Pain is the strike price where the total payout to options holders is minimized — i.e., where market makers lose the least money. The theory suggests that stocks tend to gravitate toward the max pain price at expiration because market makers hedge their positions dynamically. While controversial, many traders use max pain as a weekly price target.

Python
import yfinance as yf
import numpy as np

def calculate_max_pain(symbol, expiration=None):
    """Calculate max pain for a given stock and expiration."""
    ticker = yf.Ticker(symbol)
    if expiration is None:
        expiration = ticker.options[0]  # Nearest expiration

    chain = ticker.option_chain(expiration)
    calls = chain.calls[["strike", "openInterest"]].dropna()
    puts = chain.puts[["strike", "openInterest"]].dropna()

    strikes = sorted(set(calls["strike"].tolist() + puts["strike"].tolist()))
    total_pain = {}

    for test_price in strikes:
        # Call pain: sum of (test_price - strike) * OI for ITM calls
        call_pain = 0
        for _, row in calls.iterrows():
            if test_price > row["strike"]:
                call_pain += (test_price - row["strike"]) * row["openInterest"]

        # Put pain: sum of (strike - test_price) * OI for ITM puts
        put_pain = 0
        for _, row in puts.iterrows():
            if test_price < row["strike"]:
                put_pain += (row["strike"] - test_price) * row["openInterest"]

        total_pain[test_price] = call_pain + put_pain

    max_pain_strike = min(total_pain, key=total_pain.get)
    return {
        "symbol": symbol,
        "expiration": expiration,
        "max_pain": max_pain_strike,
        "current_price": ticker.info.get("regularMarketPrice", 0),
        "pain_by_strike": total_pain
    }

result = calculate_max_pain("AAPL")
print(f"AAPL Max Pain: ${result['max_pain']} (Current: ${result['current_price']})")

Max Pain Visualization

Building an Options Scanner

Building an Options Scanner

An options scanner monitors the entire options market for unusual activity: volume spikes, large open interest changes, and IV rank extremes. Here's a production-ready scanner:

Python
import yfinance as yf
from dataclasses import dataclass
from typing import List

@dataclass
class OptionsAlert:
    symbol: str
    alert_type: str    # "unusual_volume", "iv_spike", "large_oi_change"
    option_type: str   # "call" or "put"
    strike: float
    expiration: str
    volume: int
    open_interest: int
    iv: float
    vol_oi_ratio: float
    score: float

class OptionsScanner:
    """Scan for unusual options activity."""

    def scan(self, symbols: List[str], min_volume=100, min_vol_oi=2.0) -> List[OptionsAlert]:
        alerts = []
        for symbol in symbols:
            try:
                ticker = yf.Ticker(symbol)
                for exp in ticker.options[:3]:  # Check next 3 expirations
                    chain = ticker.option_chain(exp)
                    for opt_type, df in [("call", chain.calls), ("put", chain.puts)]:
                        for _, row in df.iterrows():
                            vol = row.get("volume", 0) or 0
                            oi = row.get("openInterest", 0) or 0
                            iv = row.get("impliedVolatility", 0) or 0

                            if vol < min_volume:
                                continue

                            vol_oi = vol / oi if oi > 0 else 999

                            if vol_oi >= min_vol_oi:
                                score = min(vol_oi * 20, 100)  # Normalize to 0-100
                                alerts.append(OptionsAlert(
                                    symbol=symbol, alert_type="unusual_volume",
                                    option_type=opt_type, strike=row["strike"],
                                    expiration=exp, volume=int(vol),
                                    open_interest=int(oi), iv=iv,
                                    vol_oi_ratio=vol_oi, score=score
                                ))
            except Exception as e:
                print(f"Error scanning {symbol}: {e}")

        return sorted(alerts, key=lambda x: x.score, reverse=True)

# Scan top 20 S&P 500 names
scanner = OptionsScanner()
symbols = ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "JPM", "V", "JNJ"]
alerts = scanner.scan(symbols, min_volume=500, min_vol_oi=3.0)

print(f"Found {len(alerts)} unusual activity alerts:")
for a in alerts[:10]:
    print(f"  {a.symbol} {a.option_type.upper()} ${a.strike} exp:{a.expiration} — "
          f"Vol:{a.volume} OI:{a.open_interest} Ratio:{a.vol_oi_ratio:.1f}x IV:{a.iv:.0%} Score:{a.score:.0f}")
IV Surface Visualization

Implied Volatility Surface

The IV surface is a 3D visualization of implied volatility across all strike prices and expiration dates. It reveals the market's volatility expectations and can identify mispriced options. The "volatility smile" (IV is higher for deep OTM and ITM options) and "volatility term structure" (IV varies by expiration) are both visible in the surface.

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

def build_iv_surface(symbol):
    """Build IV surface data from options chain."""
    ticker = yf.Ticker(symbol)
    current_price = ticker.info.get("regularMarketPrice", 0)
    surface_data = []

    for exp in ticker.options[:6]:  # Next 6 expirations
        chain = ticker.option_chain(exp)
        dte = (pd.Timestamp(exp) - pd.Timestamp.now()).days

        for _, row in chain.calls.iterrows():
            iv = row.get("impliedVolatility", 0)
            if iv > 0 and iv < 3:  # Filter outliers
                moneyness = row["strike"] / current_price
                if 0.8 <= moneyness <= 1.2:  # +/- 20% from spot
                    surface_data.append({
                        "strike": row["strike"],
                        "moneyness": moneyness,
                        "dte": dte,
                        "iv": iv,
                        "expiration": exp
                    })

    return pd.DataFrame(surface_data)

surface = build_iv_surface("AAPL")
print(surface.head(10))
Comparison

Options API Comparison

API Price Chains Greeks Flow Historical Real-Time
Yahoo (yfinance) Free Yes (delayed) No (calculate) No No 15min delay
Tradier Free / $0* Yes Basic No No Yes (with acct)
Polygon.io $29-199/mo Yes Yes ($199) No Yes ($199) Yes
Unusual Whales $40/mo No No Yes (best) Yes Yes
TD/Schwab Free* Yes Yes No No Yes
CBOE Free (delayed) Basic No No No 20min delay

* "Free" requires a funded brokerage account ($0 minimum)

Key Takeaways

Key Takeaways

What You Should Remember

What is OPRA and why is it expensive?

OPRA (Options Price Reporting Authority) is the consolidated tape for all US options exchanges. It includes every trade, quote, and open interest update from all 16 options exchanges. The raw OPRA feed processes 100+ billion messages per day — more data than all stock exchanges combined. This is why options data is expensive: the sheer volume of data requires significant infrastructure. Polygon.io at $199/mo is actually cheap for OPRA-quality data.

Should I use Black-Scholes or the Binomial model?

Black-Scholes is fine for European-style options and quick Greeks calculations. For American-style options (which can be exercised early), the Binomial (Cox-Ross-Rubinstein) model is more accurate, especially for deep ITM puts and high-dividend stocks. In practice, the difference is small for ATM options with 30+ days to expiry. Use Black-Scholes for speed, Binomial for accuracy on edge cases.

Next in Series — Part 5
Building Your Aggregator — One API to Rule Them All
Finance APIs — The Ultimate Guide4/5