Guides

Get a US option chain with Greeks in Python

Pull the full option chain for one expiration and read prices, implied volatility and the Greeks for every contract. Plain requests, no SDK, and the complete script runs as is.

By the end you'll have a Python script that takes a ticker and an expiration and prints an option chain like this (real AAPL output, trimmed):

Expirations: ['2026-10-19', '2026-10-21', '2026-10-23', '2026-10-26', ...]
2026-10-21: 34 contracts
Contract               Type  Strike    Last    Bid    Ask     IV   Delta   Gamma   Theta    Vega      OI
O:AAPL261021C00340000  call     340    6.00   0.00   0.00  42.0%   0.586  0.0327  -0.945   0.113     522
O:AAPL261021C00342500  call   342.5    4.49   0.00   0.00  39.7%   0.503  0.0359  -0.926   0.113     454
O:AAPL261021C00345000  call     345    3.08   0.00   0.00  37.3%   0.410  0.0368  -0.836   0.114    2606
...
O:AAPL261021P00345000  put      345    4.32   0.00   0.00  25.8%  -0.637  0.0524  -0.538   0.110    1029
O:AAPL261021P00347500  put    347.5    5.55   0.00   0.00  22.2%  -0.789  0.0468  -0.345   0.077    3472
O:AAPL261021P00350000  put      350    8.36   0.00   0.00      -       -       -       -       -    1366

Before you start

  • An API key with the options plan (see pricing). The options plan can be bought on its own or added to a stocks plan on the same key.
  • Python 3.8+ and requests: pip install requests

Start with a small pagination helper. List endpoints return one page at a time; next_url in the response points to the next page and already carries every query parameter, so keep following it until it's empty:

import requests

BASE = "https://marilux.dev"
API_KEY = "mh_live_your_key"   # replace with your API key
H = {"Authorization": f"Bearer {API_KEY}"}


def get_all(url, params=None):
    """Call a list endpoint and follow next_url until every page is fetched."""
    rows = []
    while url:
        r = requests.get(url, headers=H, params=params, timeout=30)
        r.raise_for_status()
        data = r.json()
        rows.extend(data.get("results", []))
        url, params = data.get("next_url"), None  # next_url already includes the query
    return rows

Step 1: list the expirations

The contracts endpoint /v3/reference/options/contracts returns every contract for an underlying in a date range. Here we take the next 45 days and collect the distinct expiration dates:

from datetime import date, timedelta

today = date.today()
contracts = get_all(f"{BASE}/v3/reference/options/contracts", {
    "underlying_ticker": "AAPL",
    "expiration_date.gte": today.isoformat(),
    "expiration_date.lte": (today + timedelta(days=45)).isoformat(),
    "limit": 1000,
})
expirations = sorted({c["expiration_date"] for c in contracts})
print("Expirations:", expirations)

Only unexpired contracts are returned by default. Add "expired": "true" to query contracts that have already expired.

Step 2: fetch the chain for one expiration

The chain snapshot endpoint /v3/snapshot/options/{underlying} returns the latest price, implied volatility, Greeks and open interest for every contract in one call. Pick the expiration with expiration_date, and keep only strikes near the money with strike_price.gte / strike_price.lte to save pages:

expiry = expirations[1]
chain = get_all(f"{BASE}/v3/snapshot/options/AAPL", {
    "expiration_date": expiry,
    "strike_price.gte": 320,
    "strike_price.lte": 360,
    "limit": 250,
})
print(f"{expiry}: {len(chain)} contracts")

Add "contract_type": "call" or "put" to get one side only. limit goes up to 250; larger pages mean fewer requests and a much faster run.

Step 3: read prices, implied volatility and the Greeks

Each contract in the chain is an object. The fields you'll use most:

FieldMeaning
details.tickerOption symbol, e.g. O:AAPL261021C00345000 (format explained)
details.contract_type / strike_price / expiration_dateCall or put, strike, expiration
day.closeLast trade price today (before the open, the previous session's close)
last_quote.bid / last_quote.askLatest best bid and ask
implied_volatilityImplied volatility; 0.373 means 37.3% (what IV is)
greeks.delta / gamma / theta / vegaThe Greeks (what each one measures)
open_interestOpen interest, in contracts
underlying_asset.priceLatest price of the underlying stock

Some contracts come without Greeks or implied volatility (see the FAQ below), so read fields with .get() and print a dash for anything missing instead of a misleading zero:

def fmt(v, width, spec=""):
    """Show a dash for fields the API didn't return, never a fake zero."""
    return ("-" if v is None else format(v, spec)).rjust(width)


print(f"{'Contract':<22} {'Type':<5}{'Strike':>7} {'Last':>7}{'Bid':>7}{'Ask':>7}{'IV':>7}"
      f"{'Delta':>8}{'Gamma':>8}{'Theta':>8}{'Vega':>8}{'OI':>8}")
for c in sorted(chain, key=lambda c: (c["details"]["contract_type"], c["details"]["strike_price"])):
    d = c["details"]
    day, quote, g = c.get("day", {}), c.get("last_quote", {}), c.get("greeks", {})
    print(f"{d['ticker']:<22} {d['contract_type']:<5}"
          f"{d['strike_price']:>7} {fmt(day.get('close'), 7, '.2f')}"
          f"{fmt(quote.get('bid'), 7, '.2f')}{fmt(quote.get('ask'), 7, '.2f')}"
          f"{fmt(c.get('implied_volatility'), 7, '.1%')}"
          f"{fmt(g.get('delta'), 8, '.3f')}{fmt(g.get('gamma'), 8, '.4f')}"
          f"{fmt(g.get('theta'), 8, '.3f')}{fmt(g.get('vega'), 8, '.3f')}"
          f"{fmt(c.get('open_interest'), 8)}")

Put the three snippets in one .py file, in order, and you have the complete script that prints the table at the top of this page.

FAQ

Why are bid and ask both 0?

US options only quote during regular trading hours (9:30 to 16:00 US Eastern). Outside those hours, including pre-market and after-hours, last_quote bid and ask are 0. Use day.close (last trade) when you need a price, or run the script after the open.

Why do some contracts have no Greeks or implied volatility?

Deep in-the-money contracts, and contracts that haven't traded or quoted for a long time, don't produce a reliable implied volatility, so the API omits greeks and implied_volatility. In the sample above, that's the puts at 350 and above.

How do I query a single contract?

Use /v3/snapshot/options/{underlying}/{option symbol}. The response has the same shape as one element of the chain:

r = requests.get(f"{BASE}/v3/snapshot/options/AAPL/O:AAPL271217C00300000", headers=H, timeout=30)
print(r.json()["results"]["greeks"])

How do I save the chain as a table or CSV?

With pandas, json_normalize flattens nested fields into columns such as greeks.delta:

import pandas as pd

df = pd.json_normalize(chain)
df.to_csv("aapl_chain.csv", index=False)

Can I use the Polygon SDK instead?

Yes. The SDK paginates for you and the code gets shorter. See Use the Polygon Python SDK with Marilux.

Try it free: verify your email and you get 3 days of the Advanced plan, no card needed. Paid plans start at $6.99/month. See pricing →