How to get historical options prices, expired contracts included
A live chain only lists today's contracts. Where daily and intraday prices for expired options come from, how far back, and what breaks when you join them.
Not from a chain endpoint: a chain lists only contracts trading today. Ask an archive for a past trade date or for expired contracts. ThetaData returns every contract's end-of-day row for any date its plan covers, free from June 2023. Massive lists expired contracts with expired=true. Databento and Cboe DataShop sell the OPRA record back to 2013 and 2012. Splits rewrite strikes and symbols, and every vendor's end-of-day is taken at a different time.
The short way
Ask for a past trade date, not for a chain. ThetaData's end-of-day endpoint
takes expiration=* and returns a row for every contract on that underlying, for the date
range you name, whether or not the contract still exists. Its REST API runs on the Theta Terminal,
a Java program on your own machine that holds the login, so the request carries no key:
import io
import pandas as pd
import requests
BASE = "http://127.0.0.1:25503/v3"
params = {"symbol": "AAPL", "expiration": "*", "start_date": "20241104",
"end_date": "20241104", "format": "csv"}
r = requests.get(f"{BASE}/option/history/eod", params=params, timeout=120)
r.raise_for_status()
chain = pd.read_csv(io.StringIO(r.text))
expired = chain[pd.to_datetime(chain["expiration"]) < pd.Timestamp("2026-10-09")]
print(len(chain), "contracts,", len(expired), "since expired")
print(chain[["expiration", "strike", "right", "close", "bid", "ask", "volume", "last_trade"]].head())
That is the vendor's own sample request. Each row has the day's open, high, low, close, volume
and trade count, and the last NBBO bid and ask with sizes, exchanges and conditions. Read the
created and last_trade columns before you use close: OPRA publishes no national end-of-day
report, so ThetaData generates its own at 17:15 Eastern, and close is the last trade, whenever
that was. A free account gets this from 1 June 2023, one day late; Value, Standard and Pro reach
2020, 2016 and 2012.
To find what existed on a date without pulling prices, /v3/option/list/contracts/quote?symbol=AAPL&date=20220930
lists every contract quoted that day. The same call with trade lists every contract that traded.
What the options are
A contract index with a flag for the dead ones. Massive's
/v3/reference/options/contracts lists active contracts by default. expired=true returns the
expired ones, and as_of asks what the index looked like on a past date. Each result carries an
OCC-style ticker such as O:AAPL211119C00085000, the shares_per_contract, and any
additional_underlyings a corporate action added to the deliverable. Prices are then one request
per contract:
import requests
BASE = "https://api.massive.com"
AUTH = {"apiKey": "YOUR_API_KEY"}
listing = requests.get(f"{BASE}/v3/reference/options/contracts", params={
"underlying_ticker": "AAPL", "expiration_date": "2024-11-15",
"expired": "true", "limit": 1000, **AUTH}, timeout=30).json()
for c in listing["results"][:5]:
bars = requests.get(
f"{BASE}/v2/aggs/ticker/{c['ticker']}/range/1/day/2024-10-01/2024-11-15",
params={"adjusted": "false", **AUTH}, timeout=30).json()
print(c["ticker"], c["shares_per_contract"], len(bars.get("results", [])), "daily bars")
The listing pages through next_url at 1,000 contracts. Bars are on every Options plan, built from
trades, so a day with no trade has no bar. Historical quotes need Options Advanced, $199 a month.
History runs two years on Basic and Starter, four on Developer and five-plus on Advanced.
The OPRA record, billed by the byte. Databento's OPRA.PILLAR dataset
has trades, OHLCV bars, statistics, instrument definitions and the one-minute consolidated BBO
from 1 April 2013. Tick-level NBBO and quotes immediately before each trade start on 28 March 2023.
Parent symbology (stype_in="parent", symbols="AAPL.OPT") fetches every contract on an
underlying, and the definition schema says which contracts existed on a given day. There is no
subscription needed for history: it is billed per gigabyte, which makes one underlying for one
month cheap and the whole market for ten years not.
The exchange's own files. Cboe DataShop's Option EOD Summary covers every OPRA series from January 2012, as one file per day. Each row has two snapshots: the NBBO with sizes and the underlying's bid and ask at 15:45 Eastern, and open, high, low, close, volume, VWAP and open interest at the end of the day. IV and greeks are an add-on. In the cart on 13 September 2026, a full-market day was $50, $70 with IV and greeks, and capped at $300 for a calendar month. The whole 2012-to-date archive was $7,200.
A cleaned surface by trade date. ORATS' /datav2/hist/strikes takes a
ticker and a tradeDate back to 2007. It returns every strike listed that day, calls and puts on
one row, with bid, ask, the smoothed IV and greeks. /datav2/hist/strikes/options follows one
contract by expiry and strike. Delisted tickers are kept. The price is ORATS's: quotes captured 14
minutes before the close. One-minute history from August 2020 is the $599 plan.
End of day by the file, with a subscription or without. IVolatility's API reaches January 2005 for over 5,500 underlyings, delisted names included. Its end-of-day price is the mid of the closing best bid and offer, published after 17:00 Eastern, with a 15:45 snapshot kept beside it. Without a subscription, its Data Download tool sells NBBO option prices at $0.40 per ticker per day.
Where this breaks
The chain forgets. A live chain, which is what getting an options chain
covers, is the list of contracts that exist now. Anything that has expired is gone from it. The
same is true of a contract list cached last month. Massive's index needs expired=true to show
dead contracts. ThetaData's expirations and strikes lists are "updated overnight", and its
contracts-by-date list is the reliable way to ask what traded on a past day.
A split rewrites the contract, and sometimes the symbol. OCC adjusts listed options after
corporate actions, case by case, and announces each adjustment in an information memo. For
NVIDIA's 10-for-1 split, effective 10 June 2024, memo #54623 kept the NVDA symbol, divided
strikes by 10 and multiplied the number of contracts by 10. A contract you followed under one
strike on 7 June trades under a strike one tenth as large on 10 June, so a join on the OCC symbol
ends one series and starts another. For the 1-for-3 reverse split of QQQY on 1 August 2024, memo
#54939 changed the symbol to QQQY1. The new deliverable was about 33 shares plus cash, and the
memo priced QQQY1 at 0.333333 times QQQY. Treat a 1 root as an ordinary 100-share contract and
its IV, delta and P&L are all wrong. Massive's shares_per_contract and additional_underlyings
fields are there for this. IVolatility's one-minute data leaves these non-standard series out
altogether.
The symbol is spelled four ways. OPRA's raw symbols are OCC's 21-character format: root
right-padded with spaces to six characters, YYMMDD, C or P, and the strike times 1,000 in
eight digits, as in AAPL 241115C00170000. Massive drops the padding and adds O:. ThetaData
never uses the string at all, and takes symbol, expiration, strike in dollars and right as four
fields. Index options add root splits. ThetaData's symbology page separates SPX from SPXW,
RUT from RUTW and NDX from NDXP by settlement style, and notes that SPXPM stopped being
used on 21 December 2018. Normalise to (root, expiry, right, strike in thousandths), and keep the
root, because SPX and SPXW with the same expiry and strike are different contracts.
"End of day" is four different times. ThetaData's report is generated at 17:15 Eastern with the last NBBO at that moment. Cboe snapshots the quote at 15:45 and the trades at the close. ORATS captures quotes 14 minutes before the close. IVolatility uses the closing NBBO, with a 15:45 snapshot beside it. Put any of these beside a stock's 16:00 close and the implied volatility you solve for absorbs the gap; why implied volatility differs works through how much. Open interest has its own clock. ThetaData documents it as reported by OPRA at about 06:30 Eastern and representing the end of the previous trading day, so the figure on Tuesday's row is Monday's.
Most contracts did not trade. ThetaData's own sample row has a last trade at 15:48 and a report at 17:16. On a deep out-of-the-money contract, zero OHLC means no qualifying trades, not missing data, and Massive produces no bar at all. A daily close series built from trades is a series of stale prints. Use the bid and ask, and keep the timestamp of the price you used.
The volume is OPRA-sized, and so is the bill. Every strike of every expiry of every underlying, every day, is why the history is sold as files and per gigabyte. One trading day of full-market 1-minute quotes on Cboe DataShop was $740 in the cart on 13 September 2026. The personal licences on ThetaData, Massive, IVolatility and Databento's lower plans do not cover a product other people see. OPRA's own redistribution fee is the line that ends side projects, and why options data costs more than stock data and the options data cost calculator put numbers on it.
If you outgrow this
When you need the surface rather than the prices, the question becomes implied volatility at fixed tenors, not contract by contract. How to get historical implied volatility as data covers the vendors that sell it ready-made.
When you need ticks rather than end-of-day, the choice is between Databento's per-gigabyte OPRA record, ThetaData's tick tiers and Cboe's per-day trade files. Plan the storage before the download; storing tick data covers the shapes that work.
When the record is your own trades, the problem is the broker's statement, not an archive. Recording options trades in a journal covers assignment, rolls and expiry.
Everything in the catalogue that touches options is on options data, and the vendors bought for options first are in options data and flow analytics. OPRA and redistribution explain the licence terms behind every price on this page.
The tools that do this
In the order this page recommends trying them. Paid placement does not affect this order.
ThetaData
One request returns a whole past chain's end-of-day rows, expired contracts included. Free from June 2023, $40 to $160 a month back to 2012.
Every OPRA quote and trade, with greeks computed per tick, from $40 a month.
$30/moFree tier
Massive
The contracts index takes expired=true and an as_of date; daily and minute bars per O-ticker follow. Quotes need Options Advanced at $199.
Full-tick US equities, options and futures from a direct exchange feed.
$29/moFree tier
Databento
OPRA.PILLAR from April 2013 for trades, OHLCV and 1-minute NBBO, a whole chain per request via parent symbology. Billed per gigabyte.
Full order book and tick history from exchange feeds, billed by the gigabyte.
$199/mo
Cboe DataShop
The exchange group's own daily files from January 2012, a 15:45 NBBO snapshot plus end-of-day OHLC, volume and open interest. $50 a full-market day.
Per-file OPRA options history from the exchange, priced by dataset, date and symbol.
$50 once
ORATS
Every strike on a past trade date in one call, back to 2007, with smoothed IV. Quotes captured 14 minutes before the close. From $199 a month.
Smoothed options greeks and IV surfaces over REST, end-of-day back to 2007.
$199/mo
IVolatility API
End-of-day history to January 2005 with delisted names, priced at the closing NBBO mid. Files without a subscription at $0.40 per ticker per day.
Options chains with raw IV, greeks and IVX surfaces over REST, US history to 2005.
$79/mo
FAQ
Why can't I get prices for an option that expired last month?
Because you are probably asking a chain endpoint, and a chain is the list of contracts that exist today. Expired contracts are still in the archives, but you have to ask for them by a past date or with an explicit flag. Massive's contracts index defaults to expired=false, and ThetaData's end-of-day endpoint returns whatever contracts traded or were quoted on the date you name.
Is there free historical options data?
Some, end of day only. ThetaData's free account returns end-of-day option rows from 1 June 2023, a day late, one request at a time. Massive's free Options Basic plan returns daily bars over two years at five requests a minute. Intraday history and quotes are paid everywhere on this page, and the free data is licensed for personal use.
Should I use the last trade or the bid and ask as the historical price?
The quote, in most cases. Most contracts do not trade every day, so the last trade can be hours or days old. ThetaData's own sample end-of-day row has a last trade at 15:48 and the closing NBBO at 17:16. ORATS, IVolatility and Cboe DataShop all build their prices from the NBBO rather than from trades.
How do I find the option symbol for a contract that no longer exists?
Build it. The OCC format is 21 characters, the root padded to six, the expiry as YYMMDD, C or P, and the strike times 1,000 in eight digits. Then check it against the vendor's contract list for that date. After a split or a special dividend the strike, the root or both may have been changed by OCC, so the symbol you build from today's terms may not be the one that traded.
Sources
- ThetaData v3 OpenAPI specification (option/history/eod, option/list/contracts, option/history/open_interest) — ThetaData, read
- OHLC & EOD — ThetaData, read
- Symbology — ThetaData, read
- All Contracts — Options REST — Massive, read
- Custom Bars (OHLC) — Options REST — Massive, read
- OPRA improvements coming soon — Databento,
- Equity options introduction — Databento, read
- Option EOD Summary — Cboe Global Markets, read
- Historical Data API — ORATS, read
- Information Memo #54623: NVIDIA Corporation — 10 for 1 Stock Split (as reproduced by BOX Exchange, memo 206578) — The Options Clearing Corporation,
- Information Memo #54939: The Nasdaq 100 Enhanced Options Income ETF — Reverse Split (as reproduced by MIAX) — The Options Clearing Corporation,
The catalogue next door
This page names a handful of cards. The rest of them are in Options Data & Flow Analytics, each filled in against the same schema, with the fields to narrow it yourself.
Last updated . Corrected in place: this is a reference page, not a dated post.