Skip to main content
VolCurve fits an implied volatility smile for a single option expiry using the SVI parametrization. After calling fit, you can query implied volatilities, total variance, option prices, and all standard Greeks across any set of strikes. You can also derive the full risk-neutral probability distribution from the fitted smile via implied_distribution.
from oipd import VolCurve

Constructor

VolCurve(
    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). The choice affects how Greeks are computed and how the forward price is resolved.
price_method
Literal['mid', 'last']
default:"\"mid\""
Quote type to fit against. "mid" uses the midpoint of bid and ask, with last_price as a fallback when available. "last" fits against last_price.
max_staleness_days
int
default:"3"
Maximum age of option quotes in calendar days, relative to the valuation date. Rows older than this threshold are excluded before fitting.

Methods

Calibrate the SVI smile to market data. Must be called before any query method. Returns self for chaining.
vol.fit(chain, market, column_mapping=None)
chain
pd.DataFrame
required
Option chain DataFrame containing a single expiry. Must include an expiry column and at least one supported price column (last_price, or bid/ask).
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"}. For example, {"type": "option_type"} means your DataFrame has a type column and OIPD should treat it as option_type. See Standard columns for the target names.
returns
VolCurve
The fitted VolCurve instance (self), enabling method chaining.
Raises: ValueError if the expiry column is missing, invalid, or contains multiple expiries. CalculationError if calibration fails.
Return implied volatilities for the given strikes. vol(strikes) is a callable alias.
vol.implied_vol(strikes)
strikes
float | Sequence[float] | np.ndarray
required
One or more strike prices to evaluate.
returns
np.ndarray
Implied volatilities in decimal form (e.g., 0.20 for 20%).
Raises: ValueError if fit has not been called.
Return total variance w = σ² × T for the given strikes.
vol.total_variance(strikes)
strikes
float | Sequence[float] | np.ndarray
required
One or more strike prices.
returns
np.ndarray
Total variance values.
Return a DataFrame comparing the fitted smile against market observations. Useful for inspecting calibration quality.
vol.iv_results(domain=None, points=200, include_observed=True)
domain
tuple[float, float] | None
default:"None"
Optional (min_strike, max_strike) range. Inferred from the data when omitted.
points
int
default:"200"
Number of strike points to sample for the fitted curve.
include_observed
bool
default:"True"
Whether to include observed market IV columns (bid IV, ask IV, last IV) in the output.
returns
pd.DataFrame
DataFrame with columns including strike and fitted_iv, plus optional observed market IV columns when include_observed=True.
Plot the fitted implied volatility smile.
vol.plot(
    x_axis="strike",
    y_axis="iv",
    include_observed=True,
    figsize=(10, 5),
    title=None,
    xlim=None,
    ylim=None,
)
x_axis
Literal['strike', 'log_moneyness']
default:"\"strike\""
X-axis mode. "log_moneyness" requires a positive forward price in fit metadata.
y_axis
Literal['iv', 'total_variance']
default:"\"iv\""
Metric on the y-axis.
include_observed
bool
default:"True"
Whether to overlay observed market data points.
figsize
tuple[float, float]
default:"(10, 5)"
Figure size as (width, height) in inches.
title
str | None
default:"None"
Custom title. Auto-generated from fit metadata when omitted.
xlim
tuple[float, float] | None
default:"None"
Optional x-axis limits.
ylim
tuple[float, float] | None
default:"None"
Optional y-axis limits.
returns
matplotlib.figure.Figure
The rendered matplotlib figure.
Calculate theoretical option prices using the fitted volatility and the configured pricing engine.
vol.price(strikes, 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. Put prices are derived via put-call parity.
returns
np.ndarray
Theoretical option prices.
Calculate Delta (∂V/∂S for Black-Scholes, or ∂V/∂F for Black-76) at the given strikes.
vol.delta(strikes, 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
Delta values.
Calculate Gamma (∂²V/∂S²) at the given strikes. Gamma is the same for calls and puts.
vol.gamma(strikes)
strikes
float | Sequence[float] | np.ndarray
required
One or more strike prices.
returns
np.ndarray
Gamma values.
Calculate Vega (∂V/∂σ) at the given strikes. Vega is the same for calls and puts.
vol.vega(strikes)
strikes
float | Sequence[float] | np.ndarray
required
One or more strike prices.
returns
np.ndarray
Vega values.
Calculate Theta (∂V/∂t, per year) at the given strikes.
vol.theta(strikes, 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
Theta values. Negative values represent time decay.
Calculate Rho (∂V/∂r) at the given strikes.
vol.rho(strikes, 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
Rho values.
Calculate all Greeks at once and return them as a single DataFrame.
vol.greeks(strikes, 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 for directional Greeks (delta, theta, rho).
returns
pd.DataFrame
DataFrame with columns strike, delta, gamma, vega, theta, and rho.
Derive the risk-neutral probability distribution from the fitted smile. Returns a ProbCurve.
vol.implied_distribution(grid_points=None, cdf_violation_policy="warn")
grid_points
int | None
default:"None"
Native probability grid size. None uses the adaptive default policy.
cdf_violation_policy
Literal['warn', 'raise']
default:"\"warn\""
Policy for CDF monotonicity violations. "warn" repairs and warns; "raise" fails on material violations.
returns
ProbCurve
The implied risk-neutral probability distribution.
Raises: ValueError if fit has not been called.

Properties

atm_vol
float
At-the-money implied volatility, defined at Strike = Forward Price. Returned in decimal form (e.g., 0.20 for 20%).
forward_price
float | None
The parity-implied forward price used in calibration. Returns None if not available.
params
dict
Fitted SVI parameters: a, b, rho, m, and sigma.
diagnostics
dict
Calibration diagnostics captured during fitting, such as {"rmse": 0.0012}.
expiries
tuple
A 1-element tuple containing the fitted expiry timestamp. Returns an empty tuple if the curve has not been fitted.
resolved_market
ResolvedMarket
Immutable snapshot of the market inputs used during calibration.
warning_diagnostics
WarningDiagnostics
Structured diagnostic events for this curve. Inspect .warning_diagnostics.events for data-quality and model-risk details.

Example

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

# 1. Fetch a single-expiry option chain
expiries = sources.list_expiry_dates("SPY")
chain, snapshot = sources.fetch_chain("SPY", expiries=expiries[0])

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

# 2. Fit the vol smile
vol = VolCurve(method="svi", pricing_engine="black76")
vol.fit(chain, market)

# 3. Query implied vols
strikes = np.arange(480, 560, 5)
ivs = vol.implied_vol(strikes)
print("ATM vol:", vol.atm_vol)
print("Forward:", vol.forward_price)
print("SVI params:", vol.params)
print("Diagnostics:", vol.diagnostics)

# 4. Greeks
greeks_df = vol.greeks(strikes, call_or_put="call")
print(greeks_df)

# 5. Theoretical prices
prices = vol.price(strikes, call_or_put="call")
print("Call prices:", prices)

# 6. Plot the smile
fig = vol.plot(include_observed=True, title="SPY Vol Smile")
fig.savefig("vol_curve.png", dpi=150, bbox_inches="tight")

# 7. Derive the probability distribution
prob = vol.implied_distribution()
print(f"Mean: {prob.mean():.2f}")
print(f"Prob below 500: {prob.prob_below(500):.4f}")