Skip to content

API Reference

qsmile

Quantitative Smile Modelling.

XCoord

Bases: Enum

X-coordinate (strike) representations.

YCoord

Bases: Enum

Y-coordinate (value) representations.

DayCount

Bases: Enum

Day-count convention for computing year fractions.

Parameters

value : str Human-readable convention name.

year_fraction(start: pd.Timestamp, end: pd.Timestamp) -> float

Compute the year fraction between two dates.

Parameters

start : pd.Timestamp Start date (valuation date). end : pd.Timestamp End date (expiry date).

Returns:

float Year fraction according to this convention.

SampleDataReader

Read option chain parquet files from a directory.

Parameters

root : str | Path | None Directory containing chains/*.parquet files. Defaults to <project_root>/parquet.

__init__(root: str | Path | None = None) -> None

Create a reader backed by a parquet directory.

Parameters

root : str | Path | None Directory containing chains/*.parquet files. Defaults to <project_root>/parquet.

get_chain(underlying: str, fetch_date: str, expiry_date: str) -> OptionChain

Load an option chain from parquet and return an OptionChain.

Parameters

underlying : str Ticker symbol, e.g. "SPX". fetch_date : str Fetch / pricing date in YYYY-MM-DD format. expiry_date : str Expiry date in YYYY-MM-DD format.

Returns:

OptionChain Fully constructed option chain with metadata and strike data.

SmileMetadata dataclass

Parameters needed by coordinate transforms.

Parameters

date : pd.Timestamp Valuation / pricing date. expiry : pd.Timestamp Expiry date. Must be strictly after date. daycount : DayCount Day-count convention for computing the year fraction. Defaults to DayCount.ACT365. forward : float | None Forward price. Must be positive when provided. discount_factor : float | None Discount factor. Must be positive when provided. sigma_atm : float | None ATM implied volatility. Must be positive when provided. Required for StandardisedStrike transforms.

texpiry: float property

Year fraction derived from (date, expiry, daycount).

__post_init__() -> None

Validate inputs.

OptionChain dataclass

Bid/ask option price chain for a single expiry.

Parameters

strikedata : StrikeArray Strike-indexed columnar data containing at least call_bid, call_ask, put_bid, and put_ask columns. Optional volume and open_interest columns are supported. metadata : SmileMetadata Smile metadata. expiry must be provided. forward and discount_factor are calibrated from put-call parity if None.

strikes: NDArray[np.float64] property

Strike prices.

call_bid: NDArray[np.float64] property

Call bid prices.

call_ask: NDArray[np.float64] property

Call ask prices.

put_bid: NDArray[np.float64] property

Put bid prices.

put_ask: NDArray[np.float64] property

Put ask prices.

volume: NDArray[np.float64] | None property

Per-strike traded volume, or None.

open_interest: NDArray[np.float64] | None property

Per-strike open interest, or None.

call_mid: NDArray[np.float64] property

Midpoint of call bid and ask prices.

put_mid: NDArray[np.float64] property

Midpoint of put bid and ask prices.

__post_init__() -> None

Validate inputs and calibrate forward/DF if needed.

__repr__() -> str

Compact repr with date, expiry, forward, and discount factor.

to_vols() -> VolData

Convert to a VolData with (FixedStrike, Volatility) using delta-blended vols.

Inverts both call and put prices to implied vols at every strike, then blends them using Black76 undiscounted call-delta weights. OTM options dominate in each wing; ATM is approximately equal-weighted.

Strikes where neither call nor put vol can be computed are excluded.

filter() -> OptionChain

Return a cleaned copy with stale and implausible quotes removed.

Applies five filters in sequence:

  1. Zero-bid filter -- removes strikes where either the call or put bid is zero (no genuine two-sided market).
  2. Put-call parity monotonicity -- C_mid - P_mid must be strictly decreasing in strike (since it equals D*(F - K)). Strikes that break monotonicity carry stale or mismarked quotes and are dropped.
  3. Call- and put-mid monotonicity -- call mids must be non-increasing and put mids non-decreasing in strike. Any remaining violations are removed.
  4. Sub-intrinsic filter -- removes strikes where the call or put bid falls below intrinsic value (using the calibrated forward), which indicates illiquid deep-ITM or stale quotes.
  5. Parity residual filter -- removes strikes where the put-call parity residual |C_mid - P_mid - D*(F - K)| exceeds 3x the combined half bid-ask spread, indicating a stale or mispriced deep-ITM quote.
Returns:

OptionChain A new OptionChain with the noisy strikes removed and forward / discount_factor re-calibrated on the clean data.

plot(*, title: str = 'Option Chain Prices', ax=None, **kwargs) -> matplotlib.figure.Figure

Plot bid/ask prices as error bars for calls and puts.

StrikeArray

A mutable collection of named columns indexed by strike price.

Columns are stored in a pd.DataFrame with a two-level MultiIndex on columns (level-0 = category, level-1 = field). Callers address columns directly via tuple[str, str] keys such as ("call", "bid").

When a new column is added whose strike index differs from the current global index, all columns are reindexed to the sorted union of strikes.

strikes: NDArray[np.float64] property

Common strike index as a sorted NDArray.

columns: list[tuple[str, str]] property

Column keys in insertion order.

__init__() -> None

Create an empty StrikeArray.

set(key: tuple[str, str], series: pd.Series) -> None

Add or replace a column, updating the global strike index.

values(key: tuple[str, str]) -> NDArray[np.float64]

Get column values as an NDArray. Raises KeyError if absent.

get_values(key: tuple[str, str]) -> NDArray[np.float64] | None

Get column values as an NDArray, or None if absent.

has(key: tuple[str, str]) -> bool

Check whether a column exists.

__len__() -> int

Return the number of strikes.

filter(mask: NDArray[np.bool_]) -> StrikeArray

Apply a boolean mask to all columns, returning a new StrikeArray.

to_dataframe() -> pd.DataFrame

Return a copy of the internal DataFrame with hierarchical columns.

VolData dataclass

Coordinate-labelled smile data with bid/ask.

Parameters

strikearray : StrikeArray Strike-indexed data containing at least y_bid and y_ask columns. Optional volume and open_interest columns are supported. current_x_coord : XCoord Which X-coordinate system the data is currently expressed in. current_y_coord : YCoord Which Y-coordinate system the data is currently expressed in. metadata : SmileMetadata Parameters needed by coordinate transforms.

native_x_coord: XCoord property

X-coordinate system the data was originally constructed in.

native_y_coord: YCoord property

Y-coordinate system the data was originally constructed in.

x: NDArray[np.float64] property

X-coordinate values in current coordinate system.

y_bid: NDArray[np.float64] property

Y-coordinate bid values in current coordinate system.

y_ask: NDArray[np.float64] property

Y-coordinate ask values in current coordinate system.

volume: NDArray[np.float64] | None property

Per-point traded volume, or None.

open_interest: NDArray[np.float64] | None property

Per-point open interest, or None.

y_mid: NDArray[np.float64] property

Midpoint of bid and ask Y values in current coordinate system.

__post_init__() -> None

Validate inputs and record native coordinates.

transform(target_x: XCoord, target_y: YCoord) -> VolData

Return a copy expressed in the target coordinate system.

This is lightweight: it shares the same underlying StrikeArray and only updates the current coordinate labels. Property accessors apply transforms lazily on access.

Parameters

target_x : XCoord Target X-coordinate system. target_y : YCoord Target Y-coordinate system.

Returns:

VolData New VolData in the target coordinates.

from_mid_vols(strikes: NDArray[np.float64], ivs: NDArray[np.float64], metadata: SmileMetadata) -> VolData classmethod

Create from mid implied vols (setting y_bid = y_ask = ivs).

Parameters

strikes : NDArray[np.float64] Strike prices. ivs : NDArray[np.float64] Mid implied volatilities. metadata : SmileMetadata Smile metadata. metadata.forward must not be None. sigma_atm is always recomputed from the data.

evaluate(x: ArrayLike) -> NDArray[np.float64]

Interpolate mid-smile at arbitrary x in current coordinates.

Uses cubic spline interpolation on y_mid. Returns NaN for points outside the data domain (no extrapolation).

Parameters

x : ArrayLike X values in the current coordinate system.

Returns:

NDArray[np.float64] Interpolated mid Y values.

plot(*, title: str = 'Smile Data', ax=None, color='k', **kwargs) -> matplotlib.figure.Figure

Plot bid/ask Y-values as error bars vs X.

Axis labels are derived from coordinate names.

SmileModel dataclass

Bases: ABC

Abstract base for dataclass-based smile models.

Provides coordinate-aware evaluation, transformation, plotting, and default serialisation. Subclasses must define:

  • Dataclass fields for the fitted parameters
  • native_x_coord, native_y_coord, param_names, bounds ClassVars
  • _evaluate(x) instance method (raw formula in native coordinates)
  • initial_guess(x, y) static method
  • __post_init__() for validation (optional)

params: dict[str, float] property

Parameter name-to-value mapping.

__post_init__() -> None

Set current coords to native if not already set.

to_array() -> NDArray[np.float64]

Pack fitted parameters into a flat array using param_names order.

from_array(x: NDArray[np.float64], *, metadata: SmileMetadata) -> Self classmethod

Reconstruct an instance from a flat parameter array.

Fitted parameters are mapped from x using param_names.

initial_guess(x: NDArray[np.float64], y: NDArray[np.float64]) -> NDArray[np.float64] abstractmethod staticmethod

Compute a heuristic initial guess from observed data.

evaluate(x: ArrayLike) -> NDArray[np.float64] | np.float64

Evaluate at x in current coordinates, transforming as needed.

transform(target_x: XCoord, target_y: YCoord) -> Self

Return a copy expressed in the target coordinate system.

plot(*, title: str = 'Smile Model', n_points: int = 200, std_range: tuple[float, float] = (-5.0, 2.0), ax=None, **kwargs) -> matplotlib.figure.Figure

Plot the model curve in current coordinates.

Parameters

std_range : tuple[float, float] Plot range in standardised-strike units (sigma * sqrt(T)) as (lo, hi). Default (-5.0, 2.0).

SmileResult dataclass

Result of a smile model fit.

Attributes:

model : SmileModel Fitted model instance (coordinate-aware). residuals : NDArray[np.float64] Per-observation residuals (model minus observed values in native coordinates). rmse : float Root mean square error of the fit. success : bool Whether the optimiser converged.

SABRModel dataclass

Bases: SmileModel

SABR model with Hagan (2002) lognormal implied volatility approximation.

The SABR model describes the dynamics of a forward rate F and its stochastic volatility alpha via:

dF = alpha * F^beta * dW_1
dalpha = nu * alpha * dW_2
<dW_1, dW_2> = rho * dt

The Hagan et al. (2002) formula maps these parameters to a closed-form lognormal implied volatility approximation.

Fitted parameters (included in the parameter vector): alpha, beta, rho, nu

Context (provided via metadata): expiry and forward are read from metadata.texpiry and metadata.forward.

Parameters

alpha : float Initial volatility. Must be > 0. beta : float CEV exponent. Must be in [0, 1]. rho : float Correlation between forward and vol. Must be in (-1, 1). nu : float Vol-of-vol. Must be >= 0. metadata : SmileMetadata Market context containing expiry, forward, etc.

__post_init__() -> None

Validate SABR parameter constraints.

initial_guess(x: NDArray[np.float64], y: NDArray[np.float64]) -> NDArray[np.float64] staticmethod

Compute a heuristic initial guess for SABR parameters from market data.

Parameters

x : NDArray[np.float64] Log-moneyness values. y : NDArray[np.float64] Observed implied volatility values.

SVIModel dataclass

Bases: SmileModel

Raw SVI parameterisation: model definition and fitted parameters.

The SVI raw parameterisation models total implied variance as:

w(k) = a + b * (rho * (k - m) + sqrt((k - m)^2 + sigma^2))

where k = ln(K/F) is log-moneyness.

Pass this class to fit() as the model, and receive instances back as fitted parameters::

result = fit(sd, model=SVIModel)
result.params          # → SVIModel instance
result.params.evaluate(k)
Parameters

a : float Vertical translation (overall variance level). b : float Slope of the wings. Must be >= 0. rho : float Correlation / rotation. Must be in (-1, 1). m : float Horizontal translation (log-moneyness shift). sigma : float Curvature at the vertex. Must be > 0.

__post_init__() -> None

Validate SVI parameter constraints.

initial_guess(x: NDArray[np.float64], y: NDArray[np.float64]) -> NDArray[np.float64] staticmethod

Compute a heuristic initial guess for SVI parameters from market data.

Parameters

x : NDArray[np.float64] Log-moneyness values. y : NDArray[np.float64] Observed total variance values.

black76_call(forward: ArrayLike, strike: ArrayLike, discount_factor: ArrayLike, vol: ArrayLike, expiry: float) -> NDArray[np.float64] | np.floating

Compute Black76 call option price.

C = D * [F * Phi(d1) - K * Phi(d2)]

Parameters

forward : ArrayLike Forward price. Must be positive. strike : ArrayLike Strike price. Must be positive. discount_factor : ArrayLike Discount factor. Must be positive. vol : ArrayLike Volatility. Must be non-negative. expiry : float Time to expiry in years. Must be positive.

black76_implied_vol(price: float, forward: float, strike: float, discount_factor: float, expiry: float, *, is_call: bool, tol: float = 1e-12, max_vol: float = 10.0) -> float

Invert Black76 to recover implied volatility.

Uses the explicit closed-form solution of Schadner (2026), "An Explicit Solution to Black-Scholes Implied Volatility" (arXiv:2604.24480), which expresses implied volatility as a direct transform of the option price via the inverse Gaussian quantile function -- no root finding required.

For a call with normalized price c = C / (D F) and forward log-moneyness k = log(K/F)::

sigma = 2 / sqrt(T * F_IG^{-1}((1-c)/m; 2/|k|, 1))

where m = 1 if K > F and m = K/F if K < F. At k = 0 the formula collapses to sigma = (2/sqrt(T)) * Phi^{-1}((c+1)/2). The put case follows from put-call parity.

Parameters

price : float Observed option price. forward : float Forward price. Must be positive. strike : float Strike price. Must be positive. discount_factor : float Discount factor. Must be positive. expiry : float Time to expiry in years. Must be positive. is_call : bool True for call, False for put. tol : float Tolerance for arbitrage-bound checks and the intrinsic-value short-circuit (returns 0.0). max_vol : float Retained for backward compatibility. The closed-form solution does not perform a search, so this argument is unused.

black76_put(forward: ArrayLike, strike: ArrayLike, discount_factor: ArrayLike, vol: ArrayLike, expiry: float) -> NDArray[np.float64] | np.floating

Compute Black76 put option price.

P = D * [K * Phi(-d2) - F * Phi(-d1)]

Parameters

forward : ArrayLike Forward price. Must be positive. strike : ArrayLike Strike price. Must be positive. discount_factor : ArrayLike Discount factor. Must be positive. vol : ArrayLike Volatility. Must be non-negative. expiry : float Time to expiry in years. Must be positive.

delta_blend_ivols(call_bid_ivols: NDArray[np.float64], call_ask_ivols: NDArray[np.float64], put_bid_ivols: NDArray[np.float64], put_ask_ivols: NDArray[np.float64], strikes: NDArray[np.float64], forward: float, expiry: float) -> tuple[NDArray[np.float64], NDArray[np.float64]]

Blend call and put implied vols using Black76 undiscounted call-delta weights.

At each strike K, the blending weight is w(K) = Phi(d1) where d1 = [ln(F/K) + 0.5 * sigma_C^2 * t] / (sigma_C * sqrt(t)) and sigma_C is the mid call-implied vol at that strike.

The blended vol is: sigma = w * sigma_C + (1 - w) * sigma_P. Bid and ask are blended independently with the same weights.

NaN values in input vols indicate inversion failures. At such strikes the blended vol falls back to the available option type. If neither is available, the strike is excluded (NaN in output).

Parameters

call_bid_ivols : NDArray[np.float64] Call-implied bid vols (NaN where inversion failed). call_ask_ivols : NDArray[np.float64] Call-implied ask vols (NaN where inversion failed). put_bid_ivols : NDArray[np.float64] Put-implied bid vols (NaN where inversion failed). put_ask_ivols : NDArray[np.float64] Put-implied ask vols (NaN where inversion failed). strikes : NDArray[np.float64] Strike prices. forward : float Forward price. expiry : float Time to expiry in years.

Returns:

tuple[NDArray[np.float64], NDArray[np.float64]] (blended_bid_ivols, blended_ask_ivols). Strikes where neither call nor put vol is available will have NaN.

fit(chain: VolData, model: type[SmileModel], initial_guess: SmileModel | None = None) -> SmileResult

Fit a smile model to market data.

Parameters

chain : VolData Market data to fit. Uses mid values for fitting. Internally transforms to the model's native coordinates. model : type[SmileModel] A model class (e.g. SVIModel) that defines native coordinates, bounds, evaluation, and initial-guess heuristic. initial_guess : SmileModel, optional Initial parameter guess (e.g. an SVIModel(...) instance). If None, the model's heuristic initial guess is computed from data.

Returns:

SmileResult Fitted model, residuals, RMSE, and convergence status.

black76

Black76 forward option pricing and implied volatility inversion.

black76_call(forward: ArrayLike, strike: ArrayLike, discount_factor: ArrayLike, vol: ArrayLike, expiry: float) -> NDArray[np.float64] | np.floating

Compute Black76 call option price.

C = D * [F * Phi(d1) - K * Phi(d2)]

Parameters

forward : ArrayLike Forward price. Must be positive. strike : ArrayLike Strike price. Must be positive. discount_factor : ArrayLike Discount factor. Must be positive. vol : ArrayLike Volatility. Must be non-negative. expiry : float Time to expiry in years. Must be positive.

black76_put(forward: ArrayLike, strike: ArrayLike, discount_factor: ArrayLike, vol: ArrayLike, expiry: float) -> NDArray[np.float64] | np.floating

Compute Black76 put option price.

P = D * [K * Phi(-d2) - F * Phi(-d1)]

Parameters

forward : ArrayLike Forward price. Must be positive. strike : ArrayLike Strike price. Must be positive. discount_factor : ArrayLike Discount factor. Must be positive. vol : ArrayLike Volatility. Must be non-negative. expiry : float Time to expiry in years. Must be positive.

black76_implied_vol(price: float, forward: float, strike: float, discount_factor: float, expiry: float, *, is_call: bool, tol: float = 1e-12, max_vol: float = 10.0) -> float

Invert Black76 to recover implied volatility.

Uses the explicit closed-form solution of Schadner (2026), "An Explicit Solution to Black-Scholes Implied Volatility" (arXiv:2604.24480), which expresses implied volatility as a direct transform of the option price via the inverse Gaussian quantile function -- no root finding required.

For a call with normalized price c = C / (D F) and forward log-moneyness k = log(K/F)::

sigma = 2 / sqrt(T * F_IG^{-1}((1-c)/m; 2/|k|, 1))

where m = 1 if K > F and m = K/F if K < F. At k = 0 the formula collapses to sigma = (2/sqrt(T)) * Phi^{-1}((c+1)/2). The put case follows from put-call parity.

Parameters

price : float Observed option price. forward : float Forward price. Must be positive. strike : float Strike price. Must be positive. discount_factor : float Discount factor. Must be positive. expiry : float Time to expiry in years. Must be positive. is_call : bool True for call, False for put. tol : float Tolerance for arbitrage-bound checks and the intrinsic-value short-circuit (returns 0.0). max_vol : float Retained for backward compatibility. The closed-form solution does not perform a search, so this argument is unused.

coords

Coordinate system enums for smile data.

XCoord

Bases: Enum

X-coordinate (strike) representations.

YCoord

Bases: Enum

Y-coordinate (value) representations.

daycount

Day-count conventions for year-fraction computation.

DayCount

Bases: Enum

Day-count convention for computing year fractions.

Parameters

value : str Human-readable convention name.

year_fraction(start: pd.Timestamp, end: pd.Timestamp) -> float

Compute the year fraction between two dates.

Parameters

start : pd.Timestamp Start date (valuation date). end : pd.Timestamp End date (expiry date).

Returns:

float Year fraction according to this convention.

maps

Coordinate transform maps and composition.

compose_x_maps(source: XCoord, target: XCoord) -> list[tuple[XCoord, XCoord, XMapFn]]

Return the chain of X-maps needed to go from source to target.

compose_y_maps(source: YCoord, target: YCoord) -> list[tuple[YCoord, YCoord, YMapFn]]

Return the chain of Y-maps needed to go from source to target.

apply_x_chain(x: NDArray[np.float64], chain: list[tuple[XCoord, XCoord, XMapFn]], meta: SmileMetadata) -> NDArray[np.float64]

Apply a chain of X-maps sequentially.

apply_y_chain(y: NDArray[np.float64], x: NDArray[np.float64], chain: list[tuple[YCoord, YCoord, YMapFn]], meta: SmileMetadata, x_coord: XCoord, target_x: XCoord) -> NDArray[np.float64]

Apply a chain of Y-maps sequentially.

For the Price↔Volatility step, X must be in FixedStrike. If needed, temporarily converts X to FixedStrike and back.

plot

Plotting utilities for option chain representations.

plot_bid_ask(x, mid, lower, upper, *, xlabel: str = '', ylabel: str = '', title: str = '', label: str | None = None, color: str | None = None, fmt: str = 'none', ax=None, **kwargs) -> matplotlib.figure.Figure

Plot bid/ask as error bars around mid values.

Parameters

x : array-like X-axis values (e.g., strikes or unitised k). mid : array-like Mid values. lower : array-like Lower bound (bid). upper : array-like Upper bound (ask). xlabel, ylabel, title : str Axis labels and title. label : str, optional Legend label. color : str, optional Color for the series. ax : matplotlib Axes, optional Axes to plot on. If None, creates a new figure.

Returns:

matplotlib.figure.Figure

plot_line(x, y, *, xlabel: str = '', ylabel: str = '', title: str = '', label: str | None = None, color: str | None = None, ax=None, **kwargs) -> matplotlib.figure.Figure

Plot a single curve.

Parameters

x : array-like X-axis values. y : array-like Y-axis values. xlabel, ylabel, title : str Axis labels and title. label : str, optional Legend label. color : str, optional Color for the line. ax : matplotlib Axes, optional Axes to plot on. If None, creates a new figure.

Returns:

matplotlib.figure.Figure

meta

Smile metadata for coordinate transforms.

SmileMetadata dataclass

Parameters needed by coordinate transforms.

Parameters

date : pd.Timestamp Valuation / pricing date. expiry : pd.Timestamp Expiry date. Must be strictly after date. daycount : DayCount Day-count convention for computing the year fraction. Defaults to DayCount.ACT365. forward : float | None Forward price. Must be positive when provided. discount_factor : float | None Discount factor. Must be positive when provided. sigma_atm : float | None ATM implied volatility. Must be positive when provided. Required for StandardisedStrike transforms.

texpiry: float property

Year fraction derived from (date, expiry, daycount).

__post_init__() -> None

Validate inputs.

prices

Bid/ask option price chain with forward/DF calibration.

OptionChain dataclass

Bid/ask option price chain for a single expiry.

Parameters

strikedata : StrikeArray Strike-indexed columnar data containing at least call_bid, call_ask, put_bid, and put_ask columns. Optional volume and open_interest columns are supported. metadata : SmileMetadata Smile metadata. expiry must be provided. forward and discount_factor are calibrated from put-call parity if None.

strikes: NDArray[np.float64] property

Strike prices.

call_bid: NDArray[np.float64] property

Call bid prices.

call_ask: NDArray[np.float64] property

Call ask prices.

put_bid: NDArray[np.float64] property

Put bid prices.

put_ask: NDArray[np.float64] property

Put ask prices.

volume: NDArray[np.float64] | None property

Per-strike traded volume, or None.

open_interest: NDArray[np.float64] | None property

Per-strike open interest, or None.

call_mid: NDArray[np.float64] property

Midpoint of call bid and ask prices.

put_mid: NDArray[np.float64] property

Midpoint of put bid and ask prices.

__post_init__() -> None

Validate inputs and calibrate forward/DF if needed.

__repr__() -> str

Compact repr with date, expiry, forward, and discount factor.

to_vols() -> VolData

Convert to a VolData with (FixedStrike, Volatility) using delta-blended vols.

Inverts both call and put prices to implied vols at every strike, then blends them using Black76 undiscounted call-delta weights. OTM options dominate in each wing; ATM is approximately equal-weighted.

Strikes where neither call nor put vol can be computed are excluded.

filter() -> OptionChain

Return a cleaned copy with stale and implausible quotes removed.

Applies five filters in sequence:

  1. Zero-bid filter -- removes strikes where either the call or put bid is zero (no genuine two-sided market).
  2. Put-call parity monotonicity -- C_mid - P_mid must be strictly decreasing in strike (since it equals D*(F - K)). Strikes that break monotonicity carry stale or mismarked quotes and are dropped.
  3. Call- and put-mid monotonicity -- call mids must be non-increasing and put mids non-decreasing in strike. Any remaining violations are removed.
  4. Sub-intrinsic filter -- removes strikes where the call or put bid falls below intrinsic value (using the calibrated forward), which indicates illiquid deep-ITM or stale quotes.
  5. Parity residual filter -- removes strikes where the put-call parity residual |C_mid - P_mid - D*(F - K)| exceeds 3x the combined half bid-ask spread, indicating a stale or mispriced deep-ITM quote.
Returns:

OptionChain A new OptionChain with the noisy strikes removed and forward / discount_factor re-calibrated on the clean data.

plot(*, title: str = 'Option Chain Prices', ax=None, **kwargs) -> matplotlib.figure.Figure

Plot bid/ask prices as error bars for calls and puts.

delta_blend_ivols(call_bid_ivols: NDArray[np.float64], call_ask_ivols: NDArray[np.float64], put_bid_ivols: NDArray[np.float64], put_ask_ivols: NDArray[np.float64], strikes: NDArray[np.float64], forward: float, expiry: float) -> tuple[NDArray[np.float64], NDArray[np.float64]]

Blend call and put implied vols using Black76 undiscounted call-delta weights.

At each strike K, the blending weight is w(K) = Phi(d1) where d1 = [ln(F/K) + 0.5 * sigma_C^2 * t] / (sigma_C * sqrt(t)) and sigma_C is the mid call-implied vol at that strike.

The blended vol is: sigma = w * sigma_C + (1 - w) * sigma_P. Bid and ask are blended independently with the same weights.

NaN values in input vols indicate inversion failures. At such strikes the blended vol falls back to the available option type. If neither is available, the strike is excluded (NaN in output).

Parameters

call_bid_ivols : NDArray[np.float64] Call-implied bid vols (NaN where inversion failed). call_ask_ivols : NDArray[np.float64] Call-implied ask vols (NaN where inversion failed). put_bid_ivols : NDArray[np.float64] Put-implied bid vols (NaN where inversion failed). put_ask_ivols : NDArray[np.float64] Put-implied ask vols (NaN where inversion failed). strikes : NDArray[np.float64] Strike prices. forward : float Forward price. expiry : float Time to expiry in years.

Returns:

tuple[NDArray[np.float64], NDArray[np.float64]] (blended_bid_ivols, blended_ask_ivols). Strikes where neither call nor put vol is available will have NaN.

strikes

Strike-indexed columnar data with hierarchical MultiIndex columns.

StrikeArray

A mutable collection of named columns indexed by strike price.

Columns are stored in a pd.DataFrame with a two-level MultiIndex on columns (level-0 = category, level-1 = field). Callers address columns directly via tuple[str, str] keys such as ("call", "bid").

When a new column is added whose strike index differs from the current global index, all columns are reindexed to the sorted union of strikes.

strikes: NDArray[np.float64] property

Common strike index as a sorted NDArray.

columns: list[tuple[str, str]] property

Column keys in insertion order.

__init__() -> None

Create an empty StrikeArray.

set(key: tuple[str, str], series: pd.Series) -> None

Add or replace a column, updating the global strike index.

values(key: tuple[str, str]) -> NDArray[np.float64]

Get column values as an NDArray. Raises KeyError if absent.

get_values(key: tuple[str, str]) -> NDArray[np.float64] | None

Get column values as an NDArray, or None if absent.

has(key: tuple[str, str]) -> bool

Check whether a column exists.

__len__() -> int

Return the number of strikes.

filter(mask: NDArray[np.bool_]) -> StrikeArray

Apply a boolean mask to all columns, returning a new StrikeArray.

to_dataframe() -> pd.DataFrame

Return a copy of the internal DataFrame with hierarchical columns.

vols

Unified smile data container with coordinate transforms.

VolData dataclass

Coordinate-labelled smile data with bid/ask.

Parameters

strikearray : StrikeArray Strike-indexed data containing at least y_bid and y_ask columns. Optional volume and open_interest columns are supported. current_x_coord : XCoord Which X-coordinate system the data is currently expressed in. current_y_coord : YCoord Which Y-coordinate system the data is currently expressed in. metadata : SmileMetadata Parameters needed by coordinate transforms.

native_x_coord: XCoord property

X-coordinate system the data was originally constructed in.

native_y_coord: YCoord property

Y-coordinate system the data was originally constructed in.

x: NDArray[np.float64] property

X-coordinate values in current coordinate system.

y_bid: NDArray[np.float64] property

Y-coordinate bid values in current coordinate system.

y_ask: NDArray[np.float64] property

Y-coordinate ask values in current coordinate system.

volume: NDArray[np.float64] | None property

Per-point traded volume, or None.

open_interest: NDArray[np.float64] | None property

Per-point open interest, or None.

y_mid: NDArray[np.float64] property

Midpoint of bid and ask Y values in current coordinate system.

__post_init__() -> None

Validate inputs and record native coordinates.

transform(target_x: XCoord, target_y: YCoord) -> VolData

Return a copy expressed in the target coordinate system.

This is lightweight: it shares the same underlying StrikeArray and only updates the current coordinate labels. Property accessors apply transforms lazily on access.

Parameters

target_x : XCoord Target X-coordinate system. target_y : YCoord Target Y-coordinate system.

Returns:

VolData New VolData in the target coordinates.

from_mid_vols(strikes: NDArray[np.float64], ivs: NDArray[np.float64], metadata: SmileMetadata) -> VolData classmethod

Create from mid implied vols (setting y_bid = y_ask = ivs).

Parameters

strikes : NDArray[np.float64] Strike prices. ivs : NDArray[np.float64] Mid implied volatilities. metadata : SmileMetadata Smile metadata. metadata.forward must not be None. sigma_atm is always recomputed from the data.

evaluate(x: ArrayLike) -> NDArray[np.float64]

Interpolate mid-smile at arbitrary x in current coordinates.

Uses cubic spline interpolation on y_mid. Returns NaN for points outside the data domain (no extrapolation).

Parameters

x : ArrayLike X values in the current coordinate system.

Returns:

NDArray[np.float64] Interpolated mid Y values.

plot(*, title: str = 'Smile Data', ax=None, color='k', **kwargs) -> matplotlib.figure.Figure

Plot bid/ask Y-values as error bars vs X.

Axis labels are derived from coordinate names.

io

Load option chain data from parquet files.

SampleDataReader

Read option chain parquet files from a directory.

Parameters

root : str | Path | None Directory containing chains/*.parquet files. Defaults to <project_root>/parquet.

__init__(root: str | Path | None = None) -> None

Create a reader backed by a parquet directory.

Parameters

root : str | Path | None Directory containing chains/*.parquet files. Defaults to <project_root>/parquet.

get_chain(underlying: str, fetch_date: str, expiry_date: str) -> OptionChain

Load an option chain from parquet and return an OptionChain.

Parameters

underlying : str Ticker symbol, e.g. "SPX". fetch_date : str Fetch / pricing date in YYYY-MM-DD format. expiry_date : str Expiry date in YYYY-MM-DD format.

Returns:

OptionChain Fully constructed option chain with metadata and strike data.

base

SmileModel abstract base class.

SmileModel dataclass

Bases: ABC

Abstract base for dataclass-based smile models.

Provides coordinate-aware evaluation, transformation, plotting, and default serialisation. Subclasses must define:

  • Dataclass fields for the fitted parameters
  • native_x_coord, native_y_coord, param_names, bounds ClassVars
  • _evaluate(x) instance method (raw formula in native coordinates)
  • initial_guess(x, y) static method
  • __post_init__() for validation (optional)

params: dict[str, float] property

Parameter name-to-value mapping.

__post_init__() -> None

Set current coords to native if not already set.

to_array() -> NDArray[np.float64]

Pack fitted parameters into a flat array using param_names order.

from_array(x: NDArray[np.float64], *, metadata: SmileMetadata) -> Self classmethod

Reconstruct an instance from a flat parameter array.

Fitted parameters are mapped from x using param_names.

initial_guess(x: NDArray[np.float64], y: NDArray[np.float64]) -> NDArray[np.float64] abstractmethod staticmethod

Compute a heuristic initial guess from observed data.

evaluate(x: ArrayLike) -> NDArray[np.float64] | np.float64

Evaluate at x in current coordinates, transforming as needed.

transform(target_x: XCoord, target_y: YCoord) -> Self

Return a copy expressed in the target coordinate system.

plot(*, title: str = 'Smile Model', n_points: int = 200, std_range: tuple[float, float] = (-5.0, 2.0), ax=None, **kwargs) -> matplotlib.figure.Figure

Plot the model curve in current coordinates.

Parameters

std_range : tuple[float, float] Plot range in standardised-strike units (sigma * sqrt(T)) as (lo, hi). Default (-5.0, 2.0).

result

Smile fitting engine.

SmileResult dataclass

Result of a smile model fit.

Attributes:

model : SmileModel Fitted model instance (coordinate-aware). residuals : NDArray[np.float64] Per-observation residuals (model minus observed values in native coordinates). rmse : float Root mean square error of the fit. success : bool Whether the optimiser converged.

fit(chain: VolData, model: type[SmileModel], initial_guess: SmileModel | None = None) -> SmileResult

Fit a smile model to market data.

Parameters

chain : VolData Market data to fit. Uses mid values for fitting. Internally transforms to the model's native coordinates. model : type[SmileModel] A model class (e.g. SVIModel) that defines native coordinates, bounds, evaluation, and initial-guess heuristic. initial_guess : SmileModel, optional Initial parameter guess (e.g. an SVIModel(...) instance). If None, the model's heuristic initial guess is computed from data.

Returns:

SmileResult Fitted model, residuals, RMSE, and convergence status.

sabr

SABR stochastic volatility model — Hagan et al. (2002) lognormal approximation.

SABRModel dataclass

Bases: SmileModel

SABR model with Hagan (2002) lognormal implied volatility approximation.

The SABR model describes the dynamics of a forward rate F and its stochastic volatility alpha via:

dF = alpha * F^beta * dW_1
dalpha = nu * alpha * dW_2
<dW_1, dW_2> = rho * dt

The Hagan et al. (2002) formula maps these parameters to a closed-form lognormal implied volatility approximation.

Fitted parameters (included in the parameter vector): alpha, beta, rho, nu

Context (provided via metadata): expiry and forward are read from metadata.texpiry and metadata.forward.

Parameters

alpha : float Initial volatility. Must be > 0. beta : float CEV exponent. Must be in [0, 1]. rho : float Correlation between forward and vol. Must be in (-1, 1). nu : float Vol-of-vol. Must be >= 0. metadata : SmileMetadata Market context containing expiry, forward, etc.

__post_init__() -> None

Validate SABR parameter constraints.

initial_guess(x: NDArray[np.float64], y: NDArray[np.float64]) -> NDArray[np.float64] staticmethod

Compute a heuristic initial guess for SABR parameters from market data.

Parameters

x : NDArray[np.float64] Log-moneyness values. y : NDArray[np.float64] Observed implied volatility values.

svi

SVI (Stochastic Volatility Inspired) raw parameterisation.

SVIModel dataclass

Bases: SmileModel

Raw SVI parameterisation: model definition and fitted parameters.

The SVI raw parameterisation models total implied variance as:

w(k) = a + b * (rho * (k - m) + sqrt((k - m)^2 + sigma^2))

where k = ln(K/F) is log-moneyness.

Pass this class to fit() as the model, and receive instances back as fitted parameters::

result = fit(sd, model=SVIModel)
result.params          # → SVIModel instance
result.params.evaluate(k)
Parameters

a : float Vertical translation (overall variance level). b : float Slope of the wings. Must be >= 0. rho : float Correlation / rotation. Must be in (-1, 1). m : float Horizontal translation (log-moneyness shift). sigma : float Curvature at the vertex. Must be > 0.

__post_init__() -> None

Validate SVI parameter constraints.

initial_guess(x: NDArray[np.float64], y: NDArray[np.float64]) -> NDArray[np.float64] staticmethod

Compute a heuristic initial guess for SVI parameters from market data.

Parameters

x : NDArray[np.float64] Log-moneyness values. y : NDArray[np.float64] Observed total variance values.