Skip to main content
VolSurface calibrates individual SVI smiles for each expiry in a multi-expiry option chain and builds a total-variance interpolator across maturities. Once fitted, you can retrieve a VolCurve for maturities within the fitted range, including dates that fall between fitted pillars. You can also export the surface as a long-format DataFrame or derive a ProbSurface of risk-neutral distributions.
from oipd import VolSurface

Constructor

VolSurface(
    method="svi",
    pricing_engine="black76",
    price_method="mid",
    max_staleness_days=3,
)
method
str
default:"\"svi\""
Calibration algorithm. Currently "svi" is the supported method.
pricing_engine
Literal['black76', 'bs']
default:"\"black76\""
Pricing model used for implied volatility inversion. Use "black76" for options on futures or forwards, and "bs" for spot options (Black-Scholes).
price_method
Literal['mid', 'last']
default:"\"mid\""
Quote type to fit against per slice. "mid" uses the bid-ask midpoint, falling back to last_price when the mid is unavailable. "last" fits against last_price.
max_staleness_days
int
default:"3"
Maximum age of option quotes in calendar days, relative to the valuation date. Stale rows are excluded before fitting each slice.

Methods

Calibrate all expiries in the chain and build the surface interpolator. Returns self for chaining.
surface.fit(
    chain,
    market,
    column_mapping=None,
    horizon=None,
    failure_policy="skip_warn",
)
chain
pd.DataFrame
required
Multi-expiry option chain DataFrame. Must contain an expiry column and at least two unique expiry dates after filtering.
market
MarketInputs
required
Market inputs providing the risk-free rate, valuation date, and underlying price.
column_mapping
dict[str, str] | None
default:"None"
Optional mapping in the form {"dataframe_column": "oipd_column"}. See Standard columns for the target names.
horizon
str | date | pd.Timestamp | None
default:"None"
Optional fit horizon that filters out expiries beyond the cutoff. Accepts human-readable strings like "30d" or "12m", or an explicit date. Expiries after the cutoff are dropped before fitting.
failure_policy
Literal['skip_warn', 'raise']
default:"\"skip_warn\""
Slice-level failure handling policy. "skip_warn" skips failing expiries and continues; "raise" propagates the first error immediately.
returns
VolSurface
The fitted VolSurface instance (self).
Raises: ValueError if failure_policy is not a supported value. CalculationError if the expiry column is missing or invalid, if fewer than two unique expiries remain after filtering, or if calibration fails.
fit requires at least two unique expiries. Pass a single-expiry chain to VolCurve.fit instead.
Return a VolCurve for the requested maturity. If expiry matches a fitted pillar exactly, the original parametric curve is returned. Otherwise, a synthetic curve is derived from the total-variance interpolator.
surface.slice(expiry)
expiry
str | date | pd.Timestamp
required
Target expiry. Accepts ISO date strings like "2025-06-20", Python date objects, or pd.Timestamp. Interpolated maturities between fitted pillars are supported.
returns
VolCurve
A VolCurve for the requested maturity.
Raises: ValueError if fit has not been called or if the maturity is outside the fitted range.
Return a long-format DataFrame of fitted IV results across expiries.
surface.iv_results(
    domain=None,
    points=200,
    include_observed=True,
    start=None,
    end=None,
    step_days=1,
)
domain
tuple[float, float] | None
default:"None"
Optional (min_strike, max_strike) range per slice.
points
int
default:"200"
Number of fitted-curve evaluation points per expiry slice.
include_observed
bool
default:"True"
Whether to include observed market IV columns.
start
str | date | pd.Timestamp | None
default:"None"
Lower expiry bound. Defaults to the first fitted pillar.
end
str | date | pd.Timestamp | None
default:"None"
Upper expiry bound. Defaults to the last fitted pillar.
step_days
int | None
default:"1"
Calendar-day sampling interval. Fitted pillar expiries are always included. Pass None to export fitted pillars only.
returns
pd.DataFrame
Long-format DataFrame with an expiry column and per-slice IV data.
Return total variance at strike K and maturity t.
surface.total_variance(100, t=45 / 365)
K
float
required
Strike price.
t
float | str | date | pd.Timestamp
required
Maturity as a year fraction or date-like value within the fitted range.
returns
float
Total variance, defined as implied volatility squared times time to expiry.
Return the interpolated implied volatility at strike K and maturity t. surface(K, t) is a callable alias.
surface.implied_vol(100, t=45 / 365)
returns
float
Implied volatility in decimal form.
Return the interpolated forward price at maturity t.
surface.forward_price(t=45 / 365)
returns
float
Forward price at the requested maturity.
Calculate theoretical option prices from the fitted surface.
surface.price([95, 100, 105], t=45 / 365, call_or_put="call")
strikes
float | Sequence[float] | np.ndarray
required
One or more strike prices.
call_or_put
Literal['call', 'put']
default:"\"call\""
Option type.
returns
np.ndarray
Theoretical option prices.
Return the at-the-money implied volatility at maturity t, where ATM is defined at the forward price.
surface.atm_vol(t=45 / 365)
returns
float
Interpolated ATM implied volatility.
Derive a ProbSurface from the fitted volatility surface.
surface.implied_distribution(grid_points=None, cdf_violation_policy="warn")
returns
ProbSurface
Risk-neutral probability surface.
Compute Greeks at a strike grid and maturity.
surface.delta(strikes, t=45 / 365, call_or_put="call")
surface.gamma(strikes, t=45 / 365)
surface.vega(strikes, t=45 / 365)
surface.theta(strikes, t=45 / 365, call_or_put="call")
surface.rho(strikes, t=45 / 365, call_or_put="call")
surface.greeks(strikes, t=45 / 365, call_or_put="call")
returns
np.ndarray | pd.DataFrame
Individual Greek methods return arrays. greeks returns a DataFrame with strike, delta, gamma, vega, theta, and rho.
Render surface plots.
surface.plot()
surface.plot_3d()
surface.plot_term_structure()
returns
matplotlib.figure.Figure
The rendered Matplotlib figure.

Properties

expiries
tuple[pd.Timestamp, ...]
Sorted tuple of fitted pillar expiry timestamps. Returns an empty tuple if fit has not been called.
warning_diagnostics
WarningDiagnostics
Structured diagnostic events accumulated during fitting. Inspect .warning_diagnostics.events for data-quality issues (e.g., stale quotes, price fallbacks), model-risk warnings (butterfly arbitrage), and skipped expiries.
params
dict[pd.Timestamp, Any]
Fitted parameter dictionary for each expiry pillar.

Example

import numpy as np
from oipd import VolSurface, MarketInputs, sources

# 1. Fetch a multi-expiry option chain
chain, snapshot = sources.fetch_chain("SPY", horizon="6m")

market = MarketInputs(
    risk_free_rate=0.053,
    valuation_date=snapshot.asof,
    underlying_price=snapshot.underlying_price,
)

# 2. Fit the surface
surface = VolSurface(method="svi", pricing_engine="black76")
surface.fit(chain, market, failure_policy="skip_warn")

print("Fitted expiries:", surface.expiries)

# 3. Slice at a fitted pillar
expiry = surface.expiries[0]
curve = surface.slice(expiry)
print("ATM vol:", curve.atm_vol)
print("SVI params:", curve.params)

# 4. Slice at an interpolated maturity between fitted pillars
midpoint = surface.expiries[0] + (surface.expiries[1] - surface.expiries[0]) / 2
interp_curve = surface.slice(midpoint)
print("Interpolated ATM vol:", interp_curve.atm_vol)

# 5. Export long-format IV results (pillar expiries only)
df = surface.iv_results(step_days=None, include_observed=False)
print(df.head())

# 6. Derive the probability surface
prob_surface = surface.implied_distribution()
print("Prob surface expiries:", prob_surface.expiries)

# 7. Review diagnostics
diag = surface.warning_diagnostics
print("Total warning events:", diag.summary.total_events)
for event in diag.events:
    print(event.event_type, event.severity, event.message)