Skip to main content
ProbCurve represents the risk-neutral probability distribution implied by a single-expiry option chain. You construct one using ProbCurve.from_chain, which fits an SVI volatility curve internally and derives the PDF and CDF. You can then query probabilities, moments, and quantiles, export a DataFrame, or render a plot — all without managing the underlying volatility model yourself.
from oipd import ProbCurve

Constructors

ProbCurve.from_chain

The primary way to build a ProbCurve. Accepts a single-expiry option chain DataFrame and market inputs, fits an SVI smile, and derives the risk-neutral distribution.
ProbCurve.from_chain(
    chain,
    market,
    column_mapping=None,
    max_staleness_days=3,
    cdf_violation_policy="warn",
)
chain
pd.DataFrame
required
Option chain DataFrame for a single expiry. Must contain 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.
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 filtered out before fitting.
cdf_violation_policy
Literal['warn', 'raise']
default:"\"warn\""
Policy for handling CDF monotonicity violations during materialization. "warn" repairs the CDF and records a diagnostic warning; "raise" raises a CalculationError on material violations.
Raises: ValueError if the chain contains multiple expiries. CalculationError if volatility calibration fails.

ProbCurve.from_arrays

Advanced constructor. Builds a ProbCurve directly from precomputed price, PDF, and CDF arrays. Use this when you have already computed the distribution elsewhere and want to use OIPD’s query and plotting API.
ProbCurve.from_arrays(
    resolved_market=resolved_market,
    metadata=metadata,
    prices=prices,
    pdf_values=pdf_values,
    cdf_values=cdf_values,
)
resolved_market
ResolvedMarket
required
Fully resolved market snapshot for the slice. You can obtain one via oipd.resolve_market(market_inputs).
metadata
dict[str, Any]
required
Slice metadata dictionary. May include keys like expiry and time_to_expiry_years.
prices
np.ndarray
required
Strike price grid. Must be aligned with pdf_values and cdf_values.
pdf_values
np.ndarray
required
Probability density values aligned with prices.
cdf_values
np.ndarray
required
Cumulative distribution values aligned with prices.

Methods

Evaluate the Probability Density Function at one or more price levels.
prob.pdf(price)
price
float | np.ndarray
required
Price level or array of price levels to evaluate.
returns
float | np.ndarray
PDF value(s). Returns a scalar float when price is a scalar, or an ndarray otherwise. Values outside the fitted domain return 0.0.
ProbCurve is also callable: prob(price) is an alias for prob.pdf(price).
Probability that the asset price at expiry is strictly below price.
prob.prob_below(price)
price
float
required
Upper bound price level.
returns
float
P(S < price), interpolated from the CDF. Returns 0.0 below the domain and 1.0 above.
Probability that the asset price at expiry is at or above price.
prob.prob_above(price)
price
float
required
Lower bound price level.
returns
float
P(S >= price). Computed as 1 - prob_below(price).
Probability that the asset price at expiry falls in the interval [low, high).
prob.prob_between(low, high)
low
float
required
Lower bound of the interval.
high
float
required
Upper bound of the interval. Must be greater than or equal to low.
returns
float
P(low <= S < high).
Raises: ValueError if low > high.
Expected value of the asset price under the fitted PDF.
prob.mean()
returns
float
E[S], computed by numerical integration of price × pdf over the domain.
Variance of the asset price under the fitted PDF.
prob.variance()
returns
float
Var[S] = E[(S - mean)²].
Skewness (third standardized moment) of the fitted PDF.
prob.skew()
returns
float
Skew = E[(S - μ)³] / σ³. Negative values indicate a fat left tail, which is typical for equity distributions.
Excess kurtosis (fourth standardized moment minus 3) of the fitted PDF.
prob.kurtosis()
returns
float
Excess kurtosis = E[(S - μ)⁴] / σ⁴ - 3. Zero implies a normal distribution; positive values indicate fat tails.
Inverse CDF: returns the price level at which the cumulative probability equals q.
prob.quantile(q)
q
float
required
Target probability level. Must be in the open interval (0, 1).
returns
float
Price S such that P(Asset < S) = q.
Raises: ValueError if q is not in (0, 1).
Export the fitted distribution as a DataFrame for analysis or custom plotting.
prob.density_results(domain=None, points=200, full_domain=False)
domain
tuple[float, float] | None
default:"None"
Optional explicit export domain as (min_price, max_price). When provided, the native distribution is resampled onto this range. Takes precedence over full_domain.
points
int
default:"200"
Number of output rows when resampling to a compact or explicit domain. Ignored when full_domain=True and no domain is specified.
full_domain
bool
default:"False"
When True and domain is not set, returns the full native distribution arrays without resampling.
returns
pd.DataFrame
DataFrame with columns price, pdf, and cdf.
Render the risk-neutral probability distribution as a matplotlib figure.
prob.plot(
    kind="both",
    figsize=(10, 5),
    title=None,
    xlim=None,
    ylim=None,
    points=800,
    full_domain=False,
)
kind
Literal['pdf', 'cdf', 'both']
default:"\"both\""
Which distribution curve(s) to render.
figsize
tuple[float, float]
default:"(10, 5)"
Figure size as (width, height) in inches.
title
str | None
default:"None"
Custom title. Auto-generated from market metadata when omitted.
xlim
tuple[float, float] | None
default:"None"
Optional explicit x-axis (price) limits.
ylim
tuple[float, float] | None
default:"None"
Optional explicit y-axis limits.
points
int
default:"800"
Number of display points for plot resampling.
full_domain
bool
default:"False"
When True and xlim is not set, plots across the full native probability domain instead of the compact default view domain.
returns
matplotlib.figure.Figure
The rendered matplotlib figure.

Properties

prices
np.ndarray
The default price grid used for standard visualization. Accessing this property triggers lazy distribution materialization on first call.
pdf_values
np.ndarray
Probability densities over the stored price grid, in decimal form.
cdf_values
np.ndarray
Cumulative probabilities over the stored price grid, in decimal form.
warning_diagnostics
WarningDiagnostics
Structured diagnostic events recorded during fitting and materialization. Inspect .warning_diagnostics.events for details on data-quality issues, model-risk warnings, or CDF repairs.
resolved_market
ResolvedMarket
Immutable snapshot of the market inputs used during calibration, including the resolved underlying price, risk-free rate, and valuation date.
metadata
dict[str, Any]
Metadata captured during estimation, including the expiry timestamp, time to expiry in years, diagnostics, and domain information.

Example

from oipd import ProbCurve, 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. Build the probability curve
prob = ProbCurve.from_chain(chain, market)

# 3. Query probabilities
print(prob.prob_below(500))        # P(SPY < 500 at expiry)
print(prob.prob_between(490, 520)) # P(490 <= SPY < 520)
print(prob.prob_above(550))        # P(SPY >= 550)

# 4. Moments
print(f"Mean: {prob.mean():.2f}")
print(f"Skew: {prob.skew():.4f}")
print(f"Excess kurtosis: {prob.kurtosis():.4f}")

# 5. Quantiles
print(f"5th percentile: {prob.quantile(0.05):.2f}")
print(f"95th percentile: {prob.quantile(0.95):.2f}")

# 6. Export as DataFrame
df = prob.density_results(points=300)
print(df.head())

# 7. Plot
fig = prob.plot(kind="both", title="SPY Risk-Neutral Distribution")
fig.savefig("prob_curve.png", dpi=150, bbox_inches="tight")