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:
| Field | Meaning |
|---|---|
details.ticker | Option symbol, e.g. O:AAPL261021C00345000 (format explained) |
details.contract_type / strike_price / expiration_date | Call or put, strike, expiration |
day.close | Last trade price today (before the open, the previous session's close) |
last_quote.bid / last_quote.ask | Latest best bid and ask |
implied_volatility | Implied volatility; 0.373 means 37.3% (what IV is) |
greeks.delta / gamma / theta / vega | The Greeks (what each one measures) |
open_interest | Open interest, in contracts |
underlying_asset.price | Latest 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.
Related
- Option Greeks explained: delta, gamma, theta, vega
- What is implied volatility (IV)?
- US option symbols explained
- API docs: Options data