Skip to main content
ProbSurface extends the single-expiry ProbCurve concept to multiple maturities. You construct it from a fitted VolSurface, or use the convenience ProbSurface.from_chain shortcut to go directly from an option chain. Once built, you can evaluate the PDF or CDF within the fitted maturity range, extract individual ProbCurve slices, export a long-format DataFrame, and visualize the distribution with a fan chart.
from oipd import ProbSurface

Constructors

ProbSurface(vol_surface, grid_points, cdf_violation_policy)

Builds a ProbSurface from an already-fitted VolSurface. This is the primary constructor when you want independent control over volatility fitting and probability derivation.
ProbSurface(
    vol_surface=vol_surface,
    grid_points=None,
    cdf_violation_policy="warn",
)
vol_surface
VolSurface
required
A fitted VolSurface instance. Must have at least two expiries and a built interpolator. Call VolSurface.fit before passing it here.
grid_points
int | None
default:"None"
Native probability grid size per slice. None uses the adaptive default policy. This controls the resolution of internal materialization, not the export size; use density_results(points=...) to control output resolution.
cdf_violation_policy
Literal['warn', 'raise']
default:"\"warn\""
Policy for CDF monotonicity violations during lazy slice materialization. "warn" repairs and records a diagnostic warning; "raise" fails on material violations.
Raises: ValueError if vol_surface has not been fitted.

ProbSurface.from_chain

Convenience shortcut that fits the underlying VolSurface internally and returns a ready-to-query ProbSurface.
ProbSurface.from_chain(
    chain,
    market,
    column_mapping=None,
    max_staleness_days=3,
    failure_policy="skip_warn",
    cdf_violation_policy="warn",
)
chain
pd.DataFrame
required
Multi-expiry option chain DataFrame. Must contain an expiry column and at least two unique expiry dates.
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.
max_staleness_days
int
default:"3"
Maximum age of option quotes in calendar days. Rows older than this threshold are filtered before fitting each slice.
failure_policy
Literal['skip_warn', 'raise']
default:"\"skip_warn\""
Controls how individual expiry calibration failures are handled. "skip_warn" skips the failing expiry and continues; "raise" propagates the error immediately.
cdf_violation_policy
Literal['warn', 'raise']
default:"\"warn\""
Policy for CDF monotonicity violations. "warn" repairs and warns; "raise" fails on material violations.
Raises: ValueError if failure_policy is not a supported value. CalculationError if fewer than two expiries remain after filtering, or if calibration fails.

Methods

Evaluate the PDF at given price level(s) and maturity.
surface.pdf(price, t)
price
float | np.ndarray
required
Price level or array of price levels to evaluate.
t
float | str | date | pd.Timestamp
required
Maturity. Pass a year-fraction float (e.g., 45/365) or a date-like value (e.g., "2025-06-20"). The surface interpolates to any maturity within the fitted range.
returns
np.ndarray
Interpolated PDF values at price for the given maturity. Values outside the domain return 0.0.
Evaluate the CDF at given price level(s) and maturity.
surface.cdf(price, t)
price
float | np.ndarray
required
Price level or array of price levels to evaluate.
t
float | str | date | pd.Timestamp
required
Maturity as a year-fraction float or date-like value.
returns
np.ndarray
Cumulative probability values. Returns 0.0 below the domain and 1.0 above.
Inverse CDF: returns the price level at quantile q for maturity t.
surface.quantile(q, t)
q
float
required
Target probability in the open interval (0, 1).
t
float | str | date | pd.Timestamp
required
Maturity as a year-fraction float or date-like value.
returns
float
Price S such that P(Asset < S) = q at maturity t.
Raises: ValueError if q is not in (0, 1).
Extract a single-expiry ProbCurve for a maturity within the fitted range. If expiry matches a fitted pillar exactly, the method returns a curve built from that pillar’s data. Otherwise, it builds a synthetic curve via total-variance interpolation.
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.
returns
ProbCurve
Probability curve for the requested maturity.
Raises: ValueError if the surface is empty or the maturity is outside the fitted range.
Export a long-format DataFrame of probability slices across expiries.
surface.density_results(
    domain=None,
    points=200,
    start=None,
    end=None,
    step_days=1,
    full_domain=False,
)
domain
tuple[float, float] | None
default:"None"
Optional export price domain as (min_price, max_price). Takes precedence over full_domain.
points
int
default:"200"
Number of price points per expiry slice in the output.
start
str | date | pd.Timestamp | None
default:"None"
Lower expiry bound for the export. Defaults to the first fitted pillar expiry.
end
str | date | pd.Timestamp | None
default:"None"
Upper expiry bound for the export. Defaults to the last fitted pillar expiry.
step_days
int | None
default:"1"
Calendar-day sampling interval between exported slices. Fitted pillar expiries are always included. Pass None to export fitted pillars only.
full_domain
bool
default:"False"
When True and domain is not set, each slice exports its full native domain without resampling.
returns
pd.DataFrame
Long-format DataFrame with columns expiry, price, pdf, and cdf.
Plot a fan chart of risk-neutral quantiles across all expiries.
surface.plot_fan(figsize=(10, 6), title=None)
figsize
tuple[float, float]
default:"(10, 6)"
Figure size as (width, height) in inches.
title
str | None
default:"None"
Custom title. Auto-generated when omitted.
returns
matplotlib.figure.Figure
The rendered fan chart figure.
Raises: ValueError if the surface has no fitted expiries, or if no valid slices remain after skipping invalid fan slices.

Properties

expiries
tuple[pd.Timestamp, ...]
All fitted pillar maturities as a tuple of pd.Timestamp objects, in ascending order.
warning_diagnostics
WarningDiagnostics
Structured diagnostic events accumulated across all surface operations. Inspect .warning_diagnostics.events for data-quality, model-risk, and workflow events.

Example

from oipd import ProbSurface, VolSurface, MarketInputs, sources

# 1. Fetch a multi-expiry 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. Build the probability surface directly from the chain
surface = ProbSurface.from_chain(chain, market)

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

# 3. Query PDF at a specific maturity
import numpy as np
prices = np.linspace(450, 600, 50)
pdf_vals = surface.pdf(prices, t=45 / 365)
print("PDF shape:", pdf_vals.shape)

# 4. CDF and quantile queries
target_expiry = surface.expiries[0]
p_below_500 = surface.cdf(500, t=target_expiry)
q95 = surface.quantile(0.95, t=target_expiry)
print(f"P(SPY < 500): {p_below_500}")
print(f"95th pct: {q95:.2f}")

# 5. Extract a single-expiry slice
curve = surface.slice(target_expiry)
print(f"Slice mean: {curve.mean():.2f}")
print(f"Slice skew: {curve.skew():.4f}")

# 6. Export long-format density results
df = surface.density_results(points=150, step_days=7)
print(df.head())

# 7. Fan chart
fig = surface.plot_fan(title="SPY Risk-Neutral Distribution Fan")
fig.savefig("prob_fan.png", dpi=150, bbox_inches="tight")