Support

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

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

                     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]

Options venues volume