Listed Option-Chain Analytics

OptionChainAnalytics connects a normalized listed option chain to implied-volatility, liquidity, and model-comparison diagnostics: smiles, surfaces, skew, term structure, rich/cheap tables, and open-interest heatmaps. This notebook uses a deterministic offline provider so it runs without network access.

Sections:

  1. Build a deterministic option-chain analytics object

  2. IV smile, skew, term structure, rich/cheap, open interest

  3. Visualizations

Setup

import abaquant
print(f"AbaQuant version: {abaquant.__version__}")
AbaQuant version: 1.0.0rc1
import pandas as pd

from abaquant.marketdata import get_ticker
from abaquant.visualization import VisualizationError
class DeterministicMarketDataProvider:
    """Offline provider with a synthetic three-expiry option chain."""

    name = "deterministic-example"

    def fast_info(self, symbol):
        return {"lastPrice": 100.0}

    def info(self, symbol):
        return {"currency": "USD", "marketCap": 600.0, "symbol": symbol}

    def history(self, symbol, **kwargs):
        import numpy as np
        dates = pd.date_range("2025-01-02", periods=36, freq="B")
        return pd.DataFrame({"Close": 100.0 + np.linspace(0, 9, len(dates))}, index=dates)

    def history_many(self, symbols, **kwargs):
        return self.history(None).reindex(columns=list(symbols), method="nearest")

    def option_expirations(self, symbol):
        return ["2027-01-15", "2027-06-18", "2028-01-21"]

    def option_chain(self, symbol, expiry):
        shift = {"2027-01-15": 0.00, "2027-06-18": 0.025, "2028-01-21": 0.045}.get(expiry, 0.0)
        strikes = [80.0, 90.0, 100.0, 110.0, 120.0]
        calls = pd.DataFrame({
            "contractSymbol": [f"{symbol}C{int(k)}" for k in strikes], "strike": strikes,
            "lastPrice": [22.0, 14.5, 8.0, 4.5, 2.4],
            "bid": [21.5, 14.0, 7.6, 4.1, 2.0], "ask": [22.5, 15.0, 8.4, 4.9, 2.8],
            "impliedVolatility": [0.31 + shift, 0.27 + shift, 0.23 + shift, 0.25 + shift, 0.29 + shift],
            "openInterest": [120, 240, 520, 310, 180], "volume": [12, 28, 65, 34, 16],
        })
        puts = pd.DataFrame({
            "contractSymbol": [f"{symbol}P{int(k)}" for k in strikes], "strike": strikes,
            "lastPrice": [2.2, 4.1, 7.8, 13.9, 21.0],
            "bid": [1.9, 3.8, 7.4, 13.4, 20.4], "ask": [2.5, 4.4, 8.2, 14.4, 21.6],
            "impliedVolatility": [0.35 + shift, 0.30 + shift, 0.24 + shift, 0.26 + shift, 0.32 + shift],
            "openInterest": [210, 330, 610, 270, 155], "volume": [18, 36, 70, 29, 14],
        })
        return calls, puts

    def income_statement(self, symbol, *, period="annual"):
        return pd.DataFrame({"2025-12-31": {"Total Revenue": 450.0}})

    def balance_sheet(self, symbol, *, period="annual"):
        return pd.DataFrame({"2025-12-31": {"Total Debt": 120.0}})

    def cash_flow_statement(self, symbol, *, period="annual"):
        return pd.DataFrame({"2025-12-31": {"Operating Cash Flow": 70.0}})

1. Build a deterministic option-chain analytics object

provider = DeterministicMarketDataProvider()
ticker = get_ticker("DEMO", provider=provider, financial_cache="memory")
analytics = ticker.options.analytics(expiry="2027-01-15")

2. IV smile, skew, term structure, rich/cheap, open interest

  • iv_smile — implied volatility by strike/moneyness for one expiry

  • skew — linear IV skew against log-moneyness

  • term_structure — implied volatility across expirations at a fixed strike

  • rich_cheap_table — listed prices vs. a chosen model (default BSM)

  • open_interest_grid — open interest by expiry, strike, and option type

call_smile = analytics.iv_smile(option_type="call")
put_skew = analytics.skew(option_type="put")
term_structure = analytics.term_structure(
    option_type="call", strike=100.0, expiries=["2027-01-15", "2027-06-18", "2028-01-21"]
)
rich_cheap = analytics.rich_cheap_table(model="bsm", risk_free_rate=0.04, option_type="call")
open_interest = analytics.open_interest_grid(
    option_type="put", expiries=["2027-01-15", "2027-06-18", "2028-01-21"]
)

print(f"Call smile rows:            {len(call_smile)}")
print(f"Put skew slope:             {put_skew.slope:.6f}")
print(f"Term-structure rows:        {len(term_structure)}")
print(f"Largest rich strike:        {float(rich_cheap.iloc[0]['strike'])}")
print(f"Total put open interest:    {float(open_interest['open_interest'].sum())}")
Call smile rows:            5
Put skew slope:             0.113225
Term-structure rows:        3
Largest rich strike:        100.0
Total put open interest:    4725.0
rich_cheap.head()
expiry option_type strike moneyness market_price implied_volatility model_volatility model_value rich_cheap rich_cheap_pct rich_cheap_label open_interest volume
0 2027-01-15 call 100.0 1.000000 8.0 0.23 0.23 6.527586 1.472414 0.225568 rich 520 65
1 2027-01-15 call 110.0 0.909091 4.5 0.25 0.25 3.192121 1.307879 0.409721 rich 310 34
2 2027-01-15 call 90.0 1.111111 14.5 0.27 0.27 13.628060 0.871940 0.063981 rich 240 28
3 2027-01-15 call 120.0 0.833333 2.4 0.29 0.29 1.929744 0.470256 0.243688 rich 180 16
4 2027-01-15 call 80.0 1.250000 22.0 0.31 0.31 22.164575 -0.164575 -0.007425 cheap 120 12

3. Visualizations

IV smile, IV surface, term structure, rich/cheap, and an open-interest heatmap.

try:
    figures = {
        "iv_smile": analytics.visualize(chart="iv_smile", option_type="call"),
        "iv_surface": analytics.visualize(chart="iv_surface", option_type="call"),
        "term_structure": analytics.visualize(
            chart="term_structure", option_type="call", strike=100.0
        ),
        "rich_cheap": analytics.visualize(
            chart="rich_cheap", option_type="call", risk_free_rate=0.04
        ),
        "open_interest_heatmap": analytics.visualize(
            chart="open_interest_heatmap", option_type="put"
        ),
    }
    print(f"Created {len(figures)} figures: {list(figures)}")
except VisualizationError as exc:
    print(f"Visualization skipped (optional dependency missing): {exc}")
Created 5 figures: ['iv_smile', 'iv_surface', 'term_structure', 'rich_cheap', 'open_interest_heatmap']
../../_images/50e3dd06c7be7b5abf77a20067d2eb1a3f75a0a6b1a120f75792215a31fe2665.png ../../_images/c9225d1e5f5bcc8b81373113003631c21a4f382957e5773952b28d4646ae0048.png ../../_images/36dbfa209e60147063777ea90067fcf0e88ec3d1413055f457313ad3f66544a3.png ../../_images/fe41073c959be8371736584944f0940fb7bbbef795d95b6a5af7dedbb8b5e920.png ../../_images/dfee75ec26406c83ff8206ee613cdb7d5d2bb6e820bd6d2c60404fffd98f328f.png

Takeaway

OptionChainAnalytics is provider-independent: swap the deterministic provider here for Yahoo or another live source and every method above keeps working unchanged. See notebook 22 — Derivative Calibration for fitting BSM/SABR/Heston parameters directly to a chain like this one.