Get end of day option spreads
Overview
In this example, we'll use the Historical client to get the end of day spread for an options chain.
First, we'll use the definition schema, which contains instrument definitions and properties, to get information such as expiration, strike_price, and instrument_class. We'll also use the CBBO-1m schema to get the bid/ask spread for each option.
We'll plot the end-of-day midprice and spread in basis-points (BPS) across all strikes for multiple expirations.
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
import datetime as dt
from zoneinfo import ZoneInfo
import databento as db
import matplotlib.pyplot as plt
import pandas as pd
def get_options_data(
client: db.Historical,
symbol: str,
schema: str,
start: dt.datetime,
end: dt.datetime | None = None,
) -> pd.DataFrame:
"""Fetch options data between timestamps"""
return client.timeseries.get_range(
dataset="OPRA.PILLAR",
schema=schema,
stype_in="parent",
symbols=f"{symbol}.OPT",
start=start,
end=end,
).to_df()
def combine_data(
trade_date: dt.date,
df_def: pd.DataFrame,
df_close: pd.DataFrame,
) -> pd.DataFrame:
"""Combine open and close data with proper column naming"""
df_close = df_close.groupby("symbol")[["bid_px_00", "ask_px_00"]].last()
df_def = df_def[["symbol", "instrument_class", "expiration", "strike_price"]]
df_def["dte"] = (df_def["expiration"].dt.tz_localize(None) - pd.Timestamp(trade_date)).dt.days
df_def = df_def.set_index("symbol")
df = df_close.join(df_def, on="symbol", how="left").reset_index().sort_values("symbol")
# Calculate spreads in basis points
df["mid_px"] = (df["bid_px_00"] + df["ask_px_00"]) / 2
df["spread"] = df["ask_px_00"] - df["bid_px_00"]
df["spread_bps"] = (df["spread"] / df["mid_px"]) / 1e-4
df["trade_date"] = trade_date
return df
def plot_spreads(
symbol: str,
df: pd.DataFrame,
target_dtes: tuple[int],
) -> None:
"""Plot bid-ask spreads for selected expirations"""
fig, axes = plt.subplots(len(target_dtes), 1, figsize=(12, 12))
selected_expirations = [
df.loc[(df["dte"] - t).abs().idxmin(), "expiration"] for t in target_dtes
]
subset = df[df["expiration"].isin(selected_expirations)]
y_min = subset["mid_px"].min()
y_max = subset["mid_px"].max()
y_range = y_max - y_min
x_min = subset["strike_price"].min()
x_max = subset["strike_price"].max()
x_padding = (x_max - x_min) * 0.02
y_padding = y_range * 0.1
# 10% of y_range = 1000 bps
y_spread_scale = (y_range * 0.10) / 1000
for idx, (ax, exp) in enumerate(zip(axes, selected_expirations)):
df_exp = df[df["expiration"] == exp].copy()
# Scale spreads for visualization
df_exp["scaled_spread"] = df_exp["spread_bps"].apply(lambda x: x * y_spread_scale)
def plot_options(
instrument_class: db.InstrumentClass,
color: str,
) -> None:
data = df_exp[df_exp["instrument_class"] == instrument_class].sort_values(
"strike_price",
)
if len(data) > 0:
half = data["scaled_spread"] / 2
ax.vlines(
x=data["strike_price"],
ymin=data["mid_px"] - half,
ymax=data["mid_px"] + half,
colors=color,
linewidth=2,
alpha=0.7,
label=instrument_class.name.capitalize(),
)
for y_offset in (half, -half):
ax.hlines(
y=data["mid_px"] + y_offset,
xmin=data["strike_price"] - 0.5,
xmax=data["strike_price"] + 0.5,
colors=color,
linewidth=1.5,
alpha=0.7,
)
ax.plot(data["strike_price"], data["mid_px"], "s", color=color, markersize=3)
plot_options(db.InstrumentClass.CALL, "C0")
plot_options(db.InstrumentClass.PUT, "C1")
ref_x = x_max - (x_max - x_min) * 0.05
ref_y_base = y_min * 0.3
s1000 = y_spread_scale * 1000
ax.plot([ref_x, ref_x], [ref_y_base, ref_y_base + s1000], "k-", linewidth=2)
for y_off, label in [(0, "0 bps"), (s1000, "1,000 bps")]:
ax.hlines(
y=ref_y_base + y_off,
xmin=ref_x - 1,
xmax=ref_x + 1,
colors="black",
linewidth=1.5,
)
ax.text(
ref_x - 3,
ref_y_base + y_off,
label,
fontsize=8,
va="center",
ha="right",
)
ax.annotate(
f"DTE = {df_exp['dte'].iloc[0]} days",
xy=(0.02, 0.98),
xycoords="axes fraction",
fontsize=11,
ha="left",
va="top",
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8),
)
ax.set_xlim(x_min - x_padding, x_max + x_padding)
ax.set_ylim(y_min - y_padding, y_max + y_padding)
ax.set_ylabel("Option Price ($)", fontsize=11)
ax.grid(True, alpha=0.3)
ax.legend(loc="upper right")
fig.suptitle(f"{symbol} Options End of Day NBBO ({df['trade_date'].iloc[0]})")
fig.supxlabel("Strike Price ($)", fontsize=11)
plt.tight_layout()
plt.show()
symbol = "AAPL"
start_date = dt.date(2025, 11, 19)
close_time = dt.time(16, 0, 0, tzinfo=ZoneInfo("US/Eastern"))
close_dt = dt.datetime.combine(start_date, close_time)
client = db.Historical("$YOUR_API_KEY")
df_close = get_options_data(
client,
symbol,
"cbbo-1m",
close_dt - dt.timedelta(minutes=15),
close_dt,
)
df_definition = get_options_data(client, symbol, "definition", start_date)
df = combine_data(start_date, df_definition, df_close)
print(df)
# Plot spreads for targeted DTEs
plot_spreads(symbol, df, target_dtes=(0, 30, 60)) Result
1 2 3 4 5 6 7 8 9 10 11 12 13 14
symbol bid_px_00 ask_px_00 instrument_class expiration ... dte mid_px spread spread_bps trade_date 0 AAPL 251121C00100000 167.30 168.55 C 2025-11-21 00:00:00+00:00 ... 2 167.925 1.25 74.437993 2025-11-19 1 AAPL 251121C00105000 162.30 165.25 C 2025-11-21 00:00:00+00:00 ... 2 163.775 2.95 180.125172 2025-11-19 2 AAPL 251121C00110000 157.25 160.25 C 2025-11-21 00:00:00+00:00 ... 2 158.750 3.00 188.976378 2025-11-19 3 AAPL 251121C00115000 152.25 155.25 C 2025-11-21 00:00:00+00:00 ... 2 153.750 3.00 195.121951 2025-11-19 4 AAPL 251121C00120000 147.25 150.25 C 2025-11-21 00:00:00+00:00 ... 2 148.750 3.00 201.680672 2025-11-19 ... ... ... ... ... ... ... ... ... ... ... ... 2690 AAPL 280121P00490000 219.00 224.00 P 2028-01-21 00:00:00+00:00 ... 793 221.500 5.00 225.733634 2025-11-19 2691 AAPL 280121P00500000 229.00 234.00 P 2028-01-21 00:00:00+00:00 ... 793 231.500 5.00 215.982721 2025-11-19 2692 AAPL 280121P00510000 239.00 244.00 P 2028-01-21 00:00:00+00:00 ... 793 241.500 5.00 207.039337 2025-11-19 2693 AAPL 280121P00520000 249.00 254.00 P 2028-01-21 00:00:00+00:00 ... 793 251.500 5.00 198.807157 2025-11-19 2694 AAPL 280121P00530000 259.00 264.00 P 2028-01-21 00:00:00+00:00 ... 793 261.500 5.00 191.204589 2025-11-19 [2695 rows x 11 columns]