get all open interest and greeks by strike by expiry
import os
import requests
import polars as pl
from rich import print
We will tackle this data collect in three steps. First, we will collect all the expiration dates for a given ticker using the "Expiry Breakdown" endpoint:
https://api.unusualwhales.com/docs#/operations/PublicApi.OptionContractController.expiry_breakdown
Once we have our list of expiration dates, our second step will use the "Greeks" endpoint to collect the greeks for each strike contained in each expiry:
https://api.unusualwhales.com/docs#/operations/PublicApi.TickerController.greeks
Our final step will use the "Option Contracts" endpoint to collect the open interest for each strike contained in each expiry:
https://api.unusualwhales.com/docs#/operations/PublicApi.OptionContractController.option_contracts
Let's get started using AAPL as an example:
uw_token = os.environ['UW_TOKEN']
headers = {'Accept': 'application/json, text/plain', 'Authorization': uw_token}
ticker = 'AAPL'
expiry_url = f'https://api.unusualwhales.com/api/stock/{ticker}/expiry-breakdown'
expiry_rsp = requests.get(expiry_url, headers=headers)
expiry_rsp.status_code
200
Cool, expiry_rsp.json()
gives us a collection of
dictionaries like this:
{'data': [{'chains': 94,
'expires': '2025-04-17',
'open_interest': 126905,
'volume': '14231'},
{'chains': 90,
'expires': '2025-01-24',
'open_interest': 33185,
'volume': '6566'},
...
{'chains': 130,
'expires': '2025-06-20',
'open_interest': 509670,
'volume': '11906'}]}
So let's grab the expires
values for now and use
those to collect data from the "Greeks" endpoint shortly after,
which will give us (by expiry) a collection of directionaries
like this:
{'data': [{'call_charm': '0.0000000000000000137155316518',
'call_delta': '1',
'call_gamma': '0',
'call_rho': '0.000411816279807205',
'call_theta': '0',
'call_vanna': '-0.0000000000000000019712257766',
'call_vega': '0',
'call_volatility': '1.394776602024026',
'date': '2024-12-17',
'expiry': '2024-12-20',
'put_charm': '0',
'put_delta': '0',
'put_gamma': '0',
'put_rho': '0',
'put_theta': '0',
'put_vanna': '0',
'put_vega': '0',
'put_volatility': '1.022728627372261',
'strike': '5'},
...
To make this data easier to slice and dice, I will compile polars DataFrames for each expiry then combine them into one:
expiration_dates = [d['expires'] for d in expiry_rsp.json()['data']]
greeks_url = f'https://api.unusualwhales.com/api/stock/{ticker}/greeks'
dfs = []
for exp_date in expiration_dates:
params = {'expiry': exp_date}
greeks_rsp = requests.get(greeks_url, headers=headers, params=params)
desired_column_order = [
'date', 'expiry', 'strike',
'call_charm', 'call_delta', 'call_gamma', 'call_rho', 'call_theta', 'call_vanna', 'call_vega', 'call_volatility',
'put_charm', 'put_delta', 'put_gamma', 'put_rho', 'put_theta', 'put_vanna', 'put_vega', 'put_volatility'
]
raw_df = pl.DataFrame(greeks_rsp.json()['data'])
dfs.append(raw_df.select(desired_column_order))
dfs[0].head(5)
date | expiry | strike | call_charm | call_delta | call_gamma | call_rho | call_theta | call_vanna | call_vega | call_volatility | put_charm | put_delta | put_gamma | put_rho | put_theta | put_vanna | put_vega | put_volatility |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str |
"2024-12-17" | "2025-04-17" | "110" | "-0.00819380435… | "0.970405797093… | "0.000517543704… | "0.333672095211… | "-0.03612717187… | "0.048662473371… | "0.098247520241… | "0.890166551468… | "-0.00106144588… | "-0.00422319160… | "0.000144888947… | "-0.00396126044… | "-0.00440997170… | "-0.06451123277… | "0.018145090485… | "0.588342992809… |
"2024-12-17" | "2025-04-17" | "115" | "-0.00834647235… | "0.967749147436… | "0.000577890942… | "0.347721900129… | "-0.03730091581… | "0.048462855114… | "0.105397115497… | "0.856775652353… | "-0.00106289194… | "-0.00485460674… | "0.000170031148… | "-0.00454538895… | "-0.00481942603… | "-0.07339708574… | "0.020568044513… | "0.567232648069… |
"2024-12-17" | "2025-04-17" | "120" | "-0.00846467288… | "0.965058197901… | "0.000641560236… | "0.361727043208… | "-0.03833737241… | "0.047654788174… | "0.112583805691… | "0.824371113404… | "-0.00112892459… | "-0.00570207501… | "0.000203489880… | "-0.00532168150… | "-0.00536173935… | "-0.08230634255… | "0.023710890533… | "0.547408773611… |
"2024-12-17" | "2025-04-17" | "125" | "-0.00861218502… | "0.961955038038… | "0.000712745973… | "0.375278820655… | "-0.03966551493… | "0.046852778125… | "0.120752452733… | "0.795208511890… | "-0.00120641724… | "-0.00664354329… | "0.000241397127… | "-0.00618635060… | "-0.00592517117… | "-0.09185955390… | "0.027148141686… | "0.528341487933… |
"2024-12-17" | "2025-04-17" | "130" | "-0.00872444722… | "0.958726756848… | "0.000791962464… | "0.388809977744… | "-0.04075300924… | "0.045219628894… | "0.128966874453… | "0.764993693819… | "-0.00127317635… | "-0.00764818236… | "0.000283684277… | "-0.00710522739… | "-0.00646390179… | "-0.10290160210… | "0.030743440474… | "0.508989296257… |
Now we have compiled each expiration's Call and Put greeks by
strike into individual DataFrames, so next I want to combine
them all together, convert strings into floats for delta and
gamma, drop the unnecessary greek values, and create an "option
symbol"-format column, like AAPL241217C00110000
for
the Dec24 110C:
raw_df = pl.concat(dfs)
desired_columns = [
'date', 'expiry', 'strike',
'call_delta', 'call_gamma', 'put_delta', 'put_gamma'
]
desired_column_order = [
'date', 'expiry', 'strike', 'call_option_symbol', 'put_option_symbol',
'call_delta', 'call_gamma', 'put_delta', 'put_gamma'
]
greeks_df = (
raw_df
.select(desired_columns)
.with_columns(
pl.lit(ticker).alias('ticker')
)
.with_columns(
pl.col('call_delta').cast(pl.Float64),
pl.col('call_gamma').cast(pl.Float64),
pl.col('put_delta').cast(pl.Float64),
pl.col('put_gamma').cast(pl.Float64)
)
.with_columns(
pl.col('strike').str.replace_all(r'\.', '')
.cast(pl.Int64)
.mul(1000)
.map_elements(lambda x: f'{x:08}')
.alias('contract_strike')
)
.with_columns(
pl.col('expiry').str.strptime(pl.Date, '%Y-%m-%d')
.dt.strftime('%y%m%d')
.alias('contract_expiry')
)
.with_columns(
pl.concat_str(
[
pl.col('ticker'),
pl.col('contract_expiry'),
pl.lit('C'),
pl.col('contract_strike')
]
).alias('call_option_symbol'),
pl.concat_str(
[
pl.col('ticker'),
pl.col('contract_expiry'),
pl.lit('P'),
pl.col('contract_strike')
]
).alias('put_option_symbol')
)
.select(desired_column_order)
.sort('expiry')
)
greeks_df
date | expiry | strike | call_option_symbol | put_option_symbol | call_delta | call_gamma | put_delta | put_gamma |
---|---|---|---|---|---|---|---|---|
str | str | str | str | str | f64 | f64 | f64 | f64 |
"2024-12-17" | "2024-12-20" | "5" | "AAPL241220C000… | "AAPL241220P000… | 1.0 | 0.0 | 0.0 | 0.0 |
"2024-12-17" | "2024-12-20" | "10" | "AAPL241220C000… | "AAPL241220P000… | 1.0 | 0.0 | 0.0 | 0.0 |
"2024-12-17" | "2024-12-20" | "15" | "AAPL241220C000… | "AAPL241220P000… | 1.0 | 0.0 | 0.0 | 0.0 |
"2024-12-17" | "2024-12-20" | "20" | "AAPL241220C000… | "AAPL241220P000… | 1.0 | 0.0 | 0.0 | 0.0 |
"2024-12-17" | "2024-12-20" | "25" | "AAPL241220C000… | "AAPL241220P000… | 1.0 | 0.0 | 0.0 | 0.0 |
… | … | … | … | … | … | … | … | … |
"2024-12-17" | "2027-01-15" | "410" | "AAPL270115C004… | "AAPL270115P004… | 0.124266 | 0.002244 | -0.987318 | 0.000629 |
"2024-12-17" | "2027-01-15" | "420" | "AAPL270115C004… | "AAPL270115P004… | 0.11151 | 0.002075 | -0.98784 | 0.000582 |
"2024-12-17" | "2027-01-15" | "430" | "AAPL270115C004… | "AAPL270115P004… | 0.100097 | 0.001916 | -0.988231 | 0.000545 |
"2024-12-17" | "2027-01-15" | "440" | "AAPL270115C004… | "AAPL270115P004… | 0.088896 | 0.001759 | -0.988506 | 0.000514 |
"2024-12-17" | "2027-01-15" | "450" | "AAPL270115C004… | "AAPL270115P004… | 0.078587 | 0.001608 | -0.988238 | 0.000503 |
Nice! We are in the home stretch, all that's left to obtain is the is the open interest by contract:
https://api.unusualwhales.com/docs#/operations/PublicApi.OptionContractController.option_contracts
Which will give us a response that looks like this:
{'data': [{'ask_volume': 51140,
'avg_price': '0.9626352941176470588235294113',
'bid_volume': 33953,
'floor_volume': 0,
'high_price': '1.30',
'implied_volatility': '0.2002082604759955',
'last_price': '1.17',
'low_price': '0.41',
'mid_volume': 9682,
'multi_leg_volume': 3553,
'nbbo_ask': '1.20',
'nbbo_bid': '1.14',
'no_side_volume': 0,
'open_interest': 35917,
'option_symbol': 'AAPL241220C00255000',
'prev_oi': 29454,
'stock_multi_leg_volume': 83,
'sweep_volume': 4010,
'total_premium': '9123376.00',
'volume': 94775},
The endpoint is capped at 500 records per response though, so we will iterate through expiry-by-expiry to make sure we collect all the open interest that we need for the final gamma exposure and delta exposure calculations.
And while some of this information will likely be valuable to
you and your client, for now I'm going to chop it all the way
down and just keep the option_symbol
(to join with
our earlier data compilation) and the
open_interest
(since that is what the gamma
exposure and delta exposure calculations will be based on):
option_contracts_url = f'https://api.unusualwhales.com/api/stock/{ticker}/option-contracts'
option_contracts_dfs = []
for exp_date in expiration_dates:
params = {'expiry': exp_date}
option_contracts_rsp = requests.get(option_contracts_url, headers=headers, params=params)
option_contracts_dfs.append(pl.DataFrame(option_contracts_rsp.json()['data']))
option_contracts_dfs[0].head(5)
ask_volume | avg_price | bid_volume | floor_volume | high_price | implied_volatility | last_price | low_price | mid_volume | multi_leg_volume | nbbo_ask | nbbo_bid | no_side_volume | open_interest | option_symbol | prev_oi | stock_multi_leg_volume | sweep_volume | total_premium | volume |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
i64 | str | i64 | i64 | str | str | str | str | i64 | i64 | str | str | i64 | i64 | str | i64 | i64 | i64 | str | i64 |
429 | "13.46424751243… | 2804 | 0 | "14.02" | "0.252700758209… | "13.89" | "11.95" | 1591 | 4568 | "14.30" | "13.80" | 0 | 5649 | "AAPL250417C002… | 5730 | 0 | 2 | "6495153.00" | 4824 |
743 | "7.331100386100… | 601 | 0 | "7.57" | "0.236887892937… | "7.55" | "6.00" | 210 | 238 | "7.70" | "7.30" | 0 | 7658 | "AAPL250417C002… | 7788 | 0 | 17 | "1139253.00" | 1554 |
1232 | "0.459975669099… | 0 | 0 | "0.46" | "0.383749786739… | "0.46" | "0.43" | 1 | 1 | "0.46" | "0.43" | 0 | 420 | "AAPL250417P001… | 420 | 0 | 0 | "56715.00" | 1233 |
292 | "6.515502717391… | 83 | 0 | "7.09" | "0.210339259626… | "6.40" | "6.30" | 361 | 71 | "6.45" | "6.30" | 0 | 2561 | "AAPL250417P002… | 2494 | 0 | 0 | "479541.00" | 736 |
530 | "0.648126126126… | 25 | 0 | "0.65" | "0.336462362621… | "0.61" | "0.61" | 0 | 8 | "0.66" | "0.62" | 0 | 1276 | "AAPL250417P001… | 1274 | 0 | 6 | "35971.00" | 555 |
raw_option_contracts_df = pl.concat(option_contracts_dfs)
option_contracts_df = (
raw_option_contracts_df
.select(['option_symbol', 'open_interest'])
)
option_contracts_df
option_symbol | open_interest |
---|---|
str | i64 |
"AAPL250417C002… | 5649 |
"AAPL250417C002… | 7658 |
"AAPL250417P001… | 420 |
"AAPL250417P002… | 2561 |
"AAPL250417P001… | 1276 |
… | … |
"AAPL250620P000… | 325 |
"AAPL250620C001… | 1315 |
"AAPL250620P001… | 2099 |
"AAPL250620C001… | 3048 |
"AAPL250620P000… | 1023 |
And that's it! I will leave the matching of the
option_symbol
column inside the
option_contracts_df
to the corresponding
greeks_df
columns
call_option_symbol
or
put_option_symbol
(could even further simplify with
a melt() on the greeks_df
) in your capable hands as
you calculate the gamma and delta exposure for the symbols of
interest.