Skip to content

Models

The models package contains all financial models for valuing options and rates.

It provides the abstract BaseModel and a suite of concrete implementations, from the standard Black-Scholes-Merton to advanced stochastic volatility and jump-diffusion models.

BSMModel

BSMModel(params: dict[str, float] | None = None)

Bases: BaseModel

Black-Scholes-Merton (BSM) model for pricing European options.

This model assumes the underlying asset follows a geometric Brownian motion with constant volatility and risk-free rate.

Initializes the BSM model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used. Must contain 'sigma'.

None
Source code in src/optpricing/models/bsm.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the BSM model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
        Must contain 'sigma'.
    """
    super().__init__(params or self.default_params)

delta_analytic

delta_analytic(*, spot: float, strike: float, r: float, q: float, t: float, call: bool = True) -> float

Analytic delta for the BSM model.

Source code in src/optpricing/models/bsm.py
def delta_analytic(
    self,
    *,
    spot: float,
    strike: float,
    r: float,
    q: float,
    t: float,
    call: bool = True,
) -> float:
    """Analytic delta for the BSM model."""
    sigma = self.params["sigma"]
    d1 = (np.log(spot / strike) + (r - q + 0.5 * sigma**2) * t) / (
        sigma * np.sqrt(t)
    )
    df_div = np.exp(-q * t)
    return df_div * ndtr(d1) if call else -df_div * ndtr(-d1)

gamma_analytic

gamma_analytic(*, spot: float, strike: float, r: float, q: float, t: float) -> float

Analytic gamma for the BSM model.

Source code in src/optpricing/models/bsm.py
def gamma_analytic(
    self,
    *,
    spot: float,
    strike: float,
    r: float,
    q: float,
    t: float,
) -> float:
    """Analytic gamma for the BSM model."""
    sigma = self.params["sigma"]
    sqrt_t = np.sqrt(t)
    d1 = (np.log(spot / strike) + (r - q + 0.5 * sigma**2) * t) / (sigma * sqrt_t)
    df_div = np.exp(-q * t)
    return df_div * _npdf(d1) / (spot * sigma * sqrt_t)

moment_strip

moment_strip() -> tuple[float, float]

Log-normal: every moment is finite.

Source code in src/optpricing/models/bsm.py
def moment_strip(self) -> tuple[float, float]:
    """Log-normal: every moment is finite."""
    return -np.inf, np.inf

rho_analytic

rho_analytic(*, spot: float, strike: float, r: float, q: float, t: float, call: bool = True) -> float

Analytic rho for the BSM model.

Source code in src/optpricing/models/bsm.py
def rho_analytic(
    self,
    *,
    spot: float,
    strike: float,
    r: float,
    q: float,
    t: float,
    call: bool = True,
) -> float:
    """Analytic rho for the BSM model."""
    sigma = self.params["sigma"]
    sqrt_t = np.sqrt(t)
    d2 = (np.log(spot / strike) + (r - q - 0.5 * sigma**2) * t) / (sigma * sqrt_t)
    df_rate = np.exp(-r * t)
    return strike * t * df_rate * (ndtr(d2) if call else -ndtr(-d2))

theta_analytic

theta_analytic(*, spot: float, strike: float, r: float, q: float, t: float, call: bool = True) -> float

Analytic theta for the BSM model.

Source code in src/optpricing/models/bsm.py
def theta_analytic(
    self,
    *,
    spot: float,
    strike: float,
    r: float,
    q: float,
    t: float,
    call: bool = True,
) -> float:
    """Analytic theta for the BSM model."""
    sigma = self.params["sigma"]
    sqrt_t = np.sqrt(t)
    d1 = (np.log(spot / strike) + (r - q + 0.5 * sigma**2) * t) / (sigma * sqrt_t)
    d2 = d1 - sigma * sqrt_t
    term1 = -spot * np.exp(-q * t) * _npdf(d1) * sigma / (2 * sqrt_t)
    if call:
        term2 = q * spot * np.exp(-q * t) * ndtr(d1)
        term3 = -r * strike * np.exp(-r * t) * ndtr(d2)
    else:
        term2 = -q * spot * np.exp(-q * t) * ndtr(-d1)
        term3 = r * strike * np.exp(-r * t) * ndtr(-d2)
    return term1 + term2 + term3

vega_analytic

vega_analytic(*, spot: float, strike: float, r: float, q: float, t: float) -> float

Analytic vega for the BSM model.

Source code in src/optpricing/models/bsm.py
def vega_analytic(
    self,
    *,
    spot: float,
    strike: float,
    r: float,
    q: float,
    t: float,
) -> float:
    """Analytic vega for the BSM model."""
    sigma = self.params["sigma"]
    sqrt_t = np.sqrt(t)
    d1 = (np.log(spot / strike) + (r - q + 0.5 * sigma**2) * t) / (sigma * sqrt_t)
    return spot * np.exp(-q * t) * _npdf(d1) * sqrt_t

BaseModel

BaseModel(params: dict[str, float])

Bases: ABC

Abstract base class for all financial pricing models.

This class defines a common interface for all models, including parameter validation, metadata flags for supported features (e.g., characteristic function, SDE), and methods for creating modified model instances.

Attributes:

Name Type Description
name str

A string identifier for the model (e.g., "Black-Scholes-Merton").

params dict[str, float]

A dictionary holding the model's parameters.

supports_cf bool

Flag indicating if the model implements a characteristic function.

supports_sde bool

Flag indicating if the model implements an SDE simulation path.

supports_pde bool

Flag indicating if the model provides PDE coefficients.

has_closed_form bool

Flag indicating if a closed-form solution is available.

vectorized_closed_form bool

Flag indicating that price_closed_form accepts NumPy arrays for strike and call (same maturity), returning an array of prices.

has_variance_process bool

Flag for stochastic volatility models (e.g., Heston, SABR).

is_pure_levy bool

Flag for pure Levy models where the terminal value can be sampled directly.

has_jumps bool

Flag for models that include a jump component.

Initializes the model and validates its parameters.

Parameters:

Name Type Description Default
params dict[str, float]

A dictionary of parameter names to values for the model.

required
Source code in src/optpricing/models/base/base_model.py
def __init__(self, params: dict[str, float]) -> None:
    """
    Initializes the model and validates its parameters.

    Parameters
    ----------
    params : dict[str, float]
        A dictionary of parameter names to values for the model.
    """
    self.params = params
    self._validate_params()

__eq__

__eq__(other: object) -> bool

Check for equality based on class type and parameters.

Source code in src/optpricing/models/base/base_model.py
def __eq__(self, other: object) -> bool:
    """
    Check for equality based on class type and parameters.
    """
    if not isinstance(other, self.__class__):
        return NotImplemented
    return self.params == other.params

__hash__

__hash__() -> int

Provide a hash for the model, making it usable in sets and dict keys.

Source code in src/optpricing/models/base/base_model.py
def __hash__(self) -> int:
    """
    Provide a hash for the model, making it usable in sets and dict keys.
    """
    return hash((self.__class__, tuple(sorted(self.params.items()))))

__repr__

__repr__() -> str

Provide a formal string representation of the model.

Source code in src/optpricing/models/base/base_model.py
def __repr__(self) -> str:
    """Provide a formal string representation of the model."""
    param_str = ", ".join(f"{k}={v:.4f}" for k, v in self.params.items())
    return f"{self.__class__.__name__}({param_str})"

cf

cf(**kwargs: Any) -> CF

Return the characteristic function of the log-price process.

Raises:

Type Description
NotImplementedError

If the model does not support a characteristic function.

Source code in src/optpricing/models/base/base_model.py
def cf(self, **kwargs: Any) -> CF:
    """
    Return the characteristic function of the log-price process.

    Raises
    ------
    NotImplementedError
        If the model does not support a characteristic function.
    """
    if not self.supports_cf:
        raise NotImplementedError(
            f"{self.name} does not support characteristic functions."
        )
    return self._cf_impl(**kwargs)

get_pde_coeffs

get_pde_coeffs(**kwargs: Any) -> PDECoeffs

Return the coefficients for the pricing PDE.

Raises:

Type Description
NotImplementedError

If the model does not support PDE solving.

Source code in src/optpricing/models/base/base_model.py
def get_pde_coeffs(self, **kwargs: Any) -> PDECoeffs:
    """
    Return the coefficients for the pricing PDE.

    Raises
    ------
    NotImplementedError
        If the model does not support PDE solving.
    """
    if not self.supports_pde:
        raise NotImplementedError(f"{self.name} does not support PDE solving.")
    return self._pde_impl(**kwargs)

get_sde_sampler

get_sde_sampler(**kwargs: Any) -> Callable

Return a function that can be used to sample paths from the model's SDE.

Raises:

Type Description
NotImplementedError

If the model does not support SDE sampling.

Source code in src/optpricing/models/base/base_model.py
def get_sde_sampler(self, **kwargs: Any) -> Callable:
    """
    Return a function that can be used to sample paths from the model's SDE.

    Raises
    ------
    NotImplementedError
        If the model does not support SDE sampling.
    """
    if not self.supports_sde:
        raise NotImplementedError(f"{self.name} does not support SDE sampling.")
    return self._sde_impl(**kwargs)

moment_strip

moment_strip() -> tuple[float, float] | None

Open interval of s with E[(S_t / S_0)^s] < inf for every t, or None if unknown or maturity-dependent (e.g. Heston's moment explosion).

Fourier techniques use it to stay inside the region where cf at imaginary arguments is a genuine moment: beyond it the closed forms are continued analytically and can return plausible-looking values (past Kou's pole the "moments" stay positive and log-convex when the jump intensity is small).

Source code in src/optpricing/models/base/base_model.py
def moment_strip(self) -> tuple[float, float] | None:
    """
    Open interval of ``s`` with ``E[(S_t / S_0)^s] < inf`` for every ``t``,
    or None if unknown or maturity-dependent (e.g. Heston's moment
    explosion).

    Fourier techniques use it to stay inside the region where ``cf`` at
    imaginary arguments is a genuine moment: beyond it the closed forms are
    continued analytically and can return plausible-looking values (past
    Kou's pole the "moments" stay positive and log-convex when the jump
    intensity is small).
    """
    return None

price_closed_form

price_closed_form(*args, **kwargs) -> float

Compute the option price using a closed-form solution, if available.

Raises:

Type Description
NotImplementedError

If the model does not have a closed-form solution.

Source code in src/optpricing/models/base/base_model.py
def price_closed_form(self, *args, **kwargs) -> float:
    """
    Compute the option price using a closed-form solution, if available.

    Raises
    ------
    NotImplementedError
        If the model does not have a closed-form solution.
    """
    if not self.has_closed_form:
        raise NotImplementedError(
            f"{self.name} does not have a closed-form solution."
        )
    return self._closed_form_impl(*args, **kwargs)

with_params

with_params(**updated_params: float) -> BaseModel

Create a new model instance with updated parameters.

This is useful for calibration and sensitivity analysis.

Parameters:

Name Type Description Default
**updated_params float

Keyword arguments for the parameters to update.

{}

Returns:

Type Description
BaseModel

A new instance of the model with the updated parameters.

Source code in src/optpricing/models/base/base_model.py
def with_params(self, **updated_params: float) -> BaseModel:
    """
    Create a new model instance with updated parameters.

    This is useful for calibration and sensitivity analysis.

    Parameters
    ----------
    **updated_params : float
        Keyword arguments for the parameters to update.

    Returns
    -------
    BaseModel
        A new instance of the model with the updated parameters.
    """
    new_params = {**self.params, **updated_params}
    return self.__class__(params=new_params)

BatesModel

BatesModel(params: dict[str, float] | None = None)

Bases: BaseModel

Bates stochastic volatility jump-diffusion model.

This model combines the Heston stochastic volatility process with Merton's log-normal jump-diffusion process, providing a rich framework for capturing both volatility smiles and sudden market shocks.

Initializes the Bates model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used.

None
Source code in src/optpricing/models/bates.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the Bates model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
    """
    super().__init__(params or self.default_params)

get_sde_stepper

get_sde_stepper() -> Callable

Returns the SDE stepper function for the Bates model.

Source code in src/optpricing/models/bates.py
def get_sde_stepper(self) -> Callable:
    """Returns the SDE stepper function for the Bates model."""
    p = self.params
    kappa, theta, _, vol_of_vol = (
        p["kappa"],
        p["theta"],
        p["rho"],
        p["vol_of_vol"],
    )
    lambda_, mu_j, sigma_j = p["lambda"], p["mu_j"], p["sigma_j"]
    compensator = lambda_ * (np.exp(mu_j + 0.5 * sigma_j**2) - 1)

    def stepper(
        log_s_t: np.ndarray,
        v_t: np.ndarray,
        r: float,
        q: float,
        dt: float,
        dw_s: np.ndarray,
        dw_v: np.ndarray,
        jump_counts: np.ndarray,
        rng: np.random.Generator,
    ) -> tuple[np.ndarray, np.ndarray]:
        v_t_pos = np.maximum(v_t, 0)
        v_sqrt = np.sqrt(v_t_pos)
        # Evolve variance
        v_t_next = v_t + kappa * (theta - v_t_pos) * dt + vol_of_vol * v_sqrt * dw_v
        # Evolve log-spot (Heston part + jump compensator)
        s_drift = (r - q - 0.5 * v_t_pos - compensator) * dt
        next_log_s = log_s_t + s_drift + v_sqrt * dw_s
        # Add jumps
        paths_with_jumps = np.where(jump_counts > 0)[0]
        for path_idx in paths_with_jumps:
            num_jumps = jump_counts[path_idx]
            jump_size = np.sum(rng.normal(loc=mu_j, scale=sigma_j, size=num_jumps))
            next_log_s[path_idx] += jump_size
        return next_log_s, np.maximum(v_t_next, 0)

    return stepper

BlacksApproxModel

BlacksApproxModel(params: dict[str, float])

Bases: BaseModel

Black's approximation for an American call on a stock with discrete dividends.

Initializes the Black's Approximation model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used. Must contain 'sigma'.

required
Source code in src/optpricing/models/blacks_approx.py
def __init__(self, params: dict[str, float]):
    """
    Initializes the Black's Approximation model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
        Must contain 'sigma'.
    """
    super().__init__(params)
    self.bsm_solver = BSMModel(params={"sigma": self.params["sigma"]})

CEVModel

CEVModel(params: dict[str, float] | None = None)

Bases: BaseModel

Constant Elasticity of Variance (CEV) model, dS = (r-q) S dt + sigma S^gamma dW.

European options have the Cox (1975) / Schroder (1989) closed form in terms of the noncentral chi-squared distribution (price_closed_form); the terminal spot can be sampled exactly (sample_terminal_spot). For gamma < 1 the origin is absorbing, which both methods honour.

Initializes the CEV model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used. Must contain 'sigma' and 'gamma'. Defaults to None.

None
Source code in src/optpricing/models/cev.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the CEV model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
        Must contain 'sigma' and 'gamma'. Defaults to None.
    """
    super().__init__(params or self.default_params)

sample_terminal_spot

sample_terminal_spot(S0: float, r: float, T: float, size: int, q: float = 0.0, rng: Generator | None = None) -> np.ndarray

Exact simulation of the terminal spot of dS = (r-q)S dt + sigma S^gamma dW.

The drifted CEV process is a deterministic time change of the driftless one, S_T = exp((r-q)T) F_tau with tau = (exp(2(r-q)(gamma-1)T) - 1) / (2(r-q)(gamma-1)), and X = F^(2(1-gamma)) / ((1-gamma)^2 sigma^2) is a squared Bessel process of dimension delta = (1-2 gamma)/(1-gamma).

  • gamma < 1 (delta < 2): the origin is attainable and absorbing; X_tau is drawn from the absorbed BESQ law (Makarov & Glew 2010): with lam = X_0 / (2 tau) and a = 1 - delta/2, draw G ~ Gamma(a, 1); the path is absorbed iff G >= lam (probability Q(a, lam)), otherwise N ~ Poisson(lam - G) and X_tau ~ Gamma(N + 1, 2 tau). This reproduces Cox's (1975) / Schroder's (1989) pricing formula and keeps S a true martingale.
  • gamma > 1 (delta > 2): the origin is unattainable and X_tau / tau is noncentral chi-squared with delta degrees of freedom and non-centrality X_0 / tau.
  • gamma == 1 reduces to geometric Brownian motion.

Parameters:

Name Type Description Default
S0 float

The initial spot price of the asset.

required
r float

The continuously compounded risk-free rate.

required
T float

The time to maturity, in years.

required
size int

The number of samples to generate.

required
q float

The continuously compounded dividend yield, by default 0.0.

0.0
rng Generator | None

Random number generator; a fresh default generator is used if None.

None

Returns:

Type Description
ndarray

An array of simulated terminal spot prices (zeros for absorbed paths).

Source code in src/optpricing/models/cev.py
def sample_terminal_spot(
    self,
    S0: float,
    r: float,
    T: float,
    size: int,
    q: float = 0.0,
    rng: np.random.Generator | None = None,
) -> np.ndarray:
    """
    Exact simulation of the terminal spot of ``dS = (r-q)S dt + sigma S^gamma dW``.

    The drifted CEV process is a deterministic time change of the driftless
    one, ``S_T = exp((r-q)T) F_tau`` with
    ``tau = (exp(2(r-q)(gamma-1)T) - 1) / (2(r-q)(gamma-1))``, and
    ``X = F^(2(1-gamma)) / ((1-gamma)^2 sigma^2)`` is a squared Bessel
    process of dimension ``delta = (1-2 gamma)/(1-gamma)``.

    * ``gamma < 1`` (``delta < 2``): the origin is attainable and absorbing;
      ``X_tau`` is drawn from the absorbed BESQ law (Makarov & Glew 2010):
      with ``lam = X_0 / (2 tau)`` and ``a = 1 - delta/2``, draw
      ``G ~ Gamma(a, 1)``; the path is absorbed iff ``G >= lam`` (probability
      ``Q(a, lam)``), otherwise ``N ~ Poisson(lam - G)`` and
      ``X_tau ~ Gamma(N + 1, 2 tau)``. This reproduces Cox's (1975) /
      Schroder's (1989) pricing formula and keeps ``S`` a true martingale.
    * ``gamma > 1`` (``delta > 2``): the origin is unattainable and
      ``X_tau / tau`` is noncentral chi-squared with ``delta`` degrees of
      freedom and non-centrality ``X_0 / tau``.
    * ``gamma == 1`` reduces to geometric Brownian motion.

    Parameters
    ----------
    S0 : float
        The initial spot price of the asset.
    r : float
        The continuously compounded risk-free rate.
    T : float
        The time to maturity, in years.
    size : int
        The number of samples to generate.
    q : float, optional
        The continuously compounded dividend yield, by default 0.0.
    rng : np.random.Generator | None, optional
        Random number generator; a fresh default generator is used if None.

    Returns
    -------
    np.ndarray
        An array of simulated terminal spot prices (zeros for absorbed paths).
    """
    from scipy.stats import ncx2

    rng = np.random.default_rng() if rng is None else rng
    p = self.params
    sigma, gamma = p["sigma"], p["gamma"]
    mu = r - q

    # gamma -> 1 is geometric Brownian motion
    if abs(1.0 - gamma) < 1e-6:
        drift = (mu - 0.5 * sigma**2) * T
        diffusion = sigma * np.sqrt(T) * rng.standard_normal(size)
        return S0 * np.exp(drift + diffusion)

    exponent = 2.0 * mu * (gamma - 1.0) * T
    tau = T if abs(exponent) < 1e-12 else np.expm1(exponent) / (exponent / T)

    n = 2.0 * (1.0 - gamma)
    scale = (1.0 - gamma) ** 2 * sigma**2
    x0 = S0**n / scale
    delta = (1.0 - 2.0 * gamma) / (1.0 - gamma)

    if gamma < 1.0:
        lam = x0 / (2.0 * tau)
        a = 1.0 - 0.5 * delta
        g = rng.gamma(shape=a, scale=1.0, size=size)
        alive = g < lam
        n_jumps = rng.poisson(np.maximum(lam - g, 0.0))
        x = np.zeros(size)
        x[alive] = rng.gamma(shape=n_jumps[alive] + 1.0, scale=2.0 * tau)
    else:
        x = tau * ncx2.rvs(df=delta, nc=x0 / tau, size=size, random_state=rng)

    f_tau = (scale * x) ** (1.0 / n)
    return np.exp(mu * T) * f_tau

CGMYModel

CGMYModel(params: dict[str, float] | None = None)

Bases: BaseModel

CGMY (Carr, Geman, Madan, Yor, 2002) pure-jump Levy model.

This is a flexible four-parameter model that can capture skewness and kurtosis. It has a known characteristic function for all valid parameters (Y < 2), including the limiting cases Y = 0 (difference of two gamma processes, i.e. Variance Gamma) and Y = 1. Monte Carlo sampling is exact for Y = 0 and uses inverse-transform sampling of the terminal characteristic function otherwise.

Initializes the CGMY model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used. Defaults to None.

None
Source code in src/optpricing/models/cgmy.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the CGMY model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
        Defaults to None.
    """
    super().__init__(params or self.default_params)

moment_strip

moment_strip() -> tuple[float, float]

-G < s < M (Carr, Geman, Madan & Yor 2002).

Source code in src/optpricing/models/cgmy.py
def moment_strip(self) -> tuple[float, float]:
    """``-G < s < M`` (Carr, Geman, Madan & Yor 2002)."""
    return float(-self.params["G"]), float(self.params["M"])

raw_cf

raw_cf(*, t: float) -> Callable

Returns the CF of the raw CGMY process, without drift or spot.

This function represents the characteristic function of the process before the risk-neutral drift adjustment is applied.

Parameters:

Name Type Description Default
t float

The time horizon.

required

Returns:

Type Description
Callable

The raw characteristic function.

Source code in src/optpricing/models/cgmy.py
def raw_cf(self, *, t: float) -> Callable:
    """
    Returns the CF of the raw CGMY process, without drift or spot.

    This function represents the characteristic function of the process
    before the risk-neutral drift adjustment is applied.

    Parameters
    ----------
    t : float
        The time horizon.

    Returns
    -------
    Callable
        The raw characteristic function.
    """
    p = self.params
    C, G, M, Y = p["C"], p["G"], p["M"], p["Y"]

    # Levy exponent psi(u) with Gamma(-Y) poles at Y = 0 and Y = 1 replaced
    # by their limits: Y -> 0 gives the Variance-Gamma (gamma difference)
    # exponent, Y -> 1 gives the x*log(x) form.
    if np.isclose(Y, 0.0):

        def psi(u):
            return C * (np.log(M / (M - 1j * u)) + np.log(G / (G + 1j * u)))

    elif np.isclose(Y, 1.0):

        def psi(u):
            return C * (
                (M - 1j * u) * np.log(M - 1j * u)
                - M * np.log(M)
                + (G + 1j * u) * np.log(G + 1j * u)
                - G * np.log(G)
            )

    else:
        gamma_minus_y = gamma_func(-Y)

        def psi(u):
            return (
                C
                * gamma_minus_y
                * ((M - 1j * u) ** Y - M**Y + (G + 1j * u) ** Y - G**Y)
            )

    def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
        return np.exp(t * psi(u))

    return phi_raw

sample_terminal_log_return

sample_terminal_log_return(T: float, size: int, rng: Generator) -> np.ndarray

Generates samples of the terminal log-return of the raw CGMY process.

For Y = 0 the process is the difference of two independent gamma processes with shape C*T and rates M and G (exact). For any other Y the terminal law is sampled by inverse transform of the characteristic function (COS expansion), see :func:optpricing.models.base.sample_from_cf.

Parameters:

Name Type Description Default
T float

The time to maturity, in years.

required
size int

The number of samples to generate.

required
rng Generator

A random number generator instance for reproducibility.

required

Returns:

Type Description
ndarray

An array of simulated terminal log-returns.

Source code in src/optpricing/models/cgmy.py
def sample_terminal_log_return(
    self,
    T: float,
    size: int,
    rng: np.random.Generator,
) -> np.ndarray:
    """
    Generates samples of the terminal log-return of the raw CGMY process.

    For ``Y = 0`` the process is the difference of two independent gamma
    processes with shape ``C*T`` and rates ``M`` and ``G`` (exact). For any
    other ``Y`` the terminal law is sampled by inverse transform of the
    characteristic function (COS expansion), see
    :func:`optpricing.models.base.sample_from_cf`.

    Parameters
    ----------
    T : float
        The time to maturity, in years.
    size : int
        The number of samples to generate.
    rng : np.random.Generator
        A random number generator instance for reproducibility.

    Returns
    -------
    np.ndarray
        An array of simulated terminal log-returns.
    """
    p = self.params
    C, G, M, Y = p["C"], p["G"], p["M"], p["Y"]

    if np.isclose(Y, 0.0):
        g_up = rng.gamma(shape=C * T, scale=1 / M, size=size)
        g_down = rng.gamma(shape=C * T, scale=1 / G, size=size)
        return g_up - g_down

    return sample_from_cf(self.raw_cf(t=T), size, rng)

CIRModel

CIRModel(params: dict[str, float] | None = None)

Bases: BaseModel

Cox-Ingersoll-Ross (1985) mean-reverting short rate model. dr_t = kappa * (theta - r_t) * dt + sigma * sqrt(r_t) * dW_t

Initializes the CIR model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used.

None
Source code in src/optpricing/models/cir.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the CIR model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
    """
    super().__init__(params or self.default_params)

DupireLocalVolModel

DupireLocalVolModel(params: dict[str, Any] | None = None)

Bases: BaseModel

Dupire Local Volatility model.

Initializes the Dupire Local Volatility model.

The key parameter for this model is 'vol_surface', a scalar function sigma(t, S) of time and spot returning the local volatility (the Dupire surface sigma(T, K) evaluated along the path at T = t, K = S). Monte Carlo compiles it with numba.njit, so it must be nopython-compatible (plain arithmetic, math, NumPy scalars).

Parameters:

Name Type Description Default
params dict[str, Any] | None

A dictionary of model parameters. If None, a default constant volatility surface is used. Defaults to None.

None
Source code in src/optpricing/models/dupire_local.py
def __init__(self, params: dict[str, Any] | None = None):
    """
    Initializes the Dupire Local Volatility model.

    The key parameter for this model is 'vol_surface', a scalar function
    ``sigma(t, S)`` of time and spot returning the local volatility (the
    Dupire surface sigma(T, K) evaluated along the path at T = t, K = S).
    Monte Carlo compiles it with ``numba.njit``, so it must be
    nopython-compatible (plain arithmetic, ``math``, NumPy scalars).

    Parameters
    ----------
    params : dict[str, Any] | None, optional
        A dictionary of model parameters. If None, a default constant
        volatility surface is used. Defaults to None.
    """
    super().__init__(params or self.default_params)

__repr__

__repr__() -> str

Custom representation to handle the vol_surface function.

Source code in src/optpricing/models/dupire_local.py
def __repr__(self) -> str:
    """Custom representation to handle the vol_surface function."""
    # Get the name of the function or show its type
    vol_surface_repr = getattr(
        self.params["vol_surface"],
        "__name__",
        str(type(self.params["vol_surface"])),
    )
    return f"{self.__class__.__name__}(vol_surface={vol_surface_repr})"

HestonModel

HestonModel(params: dict[str, float] | None = None)

Bases: BaseModel

Heston stochastic volatility model.

This model describes the evolution of an asset's price where the volatility itself is a random process, following a Cox-Ingersoll-Ross (CIR) process. It is widely used as it can capture volatility smiles and skews.

Initializes the Heston model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used.

None
Source code in src/optpricing/models/heston.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the Heston model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
    """
    super().__init__(params or self.default_params)

get_sde_stepper

get_sde_stepper() -> Callable

Returns the SDE stepper function for the Heston model.

This is not used by the JIT-compiled kernel but is kept for potential future use with non-compiled or more complex simulation loops.

Source code in src/optpricing/models/heston.py
def get_sde_stepper(self) -> Callable:
    """
    Returns the SDE stepper function for the Heston model.

    This is not used by the JIT-compiled kernel but is kept for potential
    future use with non-compiled or more complex simulation loops.
    """
    p = self.params
    kappa, theta, _, vol_of_vol = (
        p["kappa"],
        p["theta"],
        p["rho"],
        p["vol_of_vol"],
    )

    def stepper(
        log_s_t: np.ndarray,
        v_t: np.ndarray,
        r: float,
        q: float,
        dt: float,
        dw_s: np.ndarray,
        dw_v: np.ndarray,
    ) -> tuple[np.ndarray, np.ndarray]:
        v_t_pos = np.maximum(v_t, 0)
        v_sqrt = np.sqrt(v_t_pos)
        log_s_t_next = log_s_t + (r - q - 0.5 * v_t_pos) * dt + v_sqrt * dw_s
        v_t_next = v_t + kappa * (theta - v_t_pos) * dt + vol_of_vol * v_sqrt * dw_v
        return log_s_t_next, np.maximum(v_t_next, 0)  # Reflection scheme

    return stepper

HyperbolicModel

HyperbolicModel(params: dict[str, float] | None = None)

Bases: BaseModel

Hyperbolic Levy motion (Eberlein & Keller 1995).

X_1 follows the hyperbolic distribution (generalized hyperbolic with lambda = 1) with parameters alpha, beta, delta, mu; the process is the Levy process generated by that law, so phi_t(u) = phi_1(u)^t. Note that X_t is not hyperbolic for t != 1 (the class is not closed under convolution), hence terminal sampling for t != 1 inverts the characteristic function numerically.

Initializes the Hyperbolic model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used. Defaults to None.

None
Source code in src/optpricing/models/hyperbolic.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the Hyperbolic model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
        Defaults to None.
    """
    super().__init__(params or self.default_params)

moment_strip

moment_strip() -> tuple[float, float]

|beta + s| < alpha (Eberlein & Keller 1995).

Source code in src/optpricing/models/hyperbolic.py
def moment_strip(self) -> tuple[float, float]:
    """``|beta + s| < alpha`` (Eberlein & Keller 1995)."""
    alpha, beta = self.params["alpha"], self.params["beta"]
    return float(-alpha - beta), float(alpha - beta)

raw_cf

raw_cf(*, t: float) -> Callable

Returns the CF of the raw Hyperbolic process, without drift or spot.

This function represents the characteristic function of the process before the risk-neutral drift adjustment is applied.

Parameters:

Name Type Description Default
t float

The time horizon.

required

Returns:

Type Description
Callable

The raw characteristic function.

Source code in src/optpricing/models/hyperbolic.py
def raw_cf(self, *, t: float) -> Callable:
    """
    Returns the CF of the raw Hyperbolic process, without drift or spot.

    This function represents the characteristic function of the process
    before the risk-neutral drift adjustment is applied.

    Parameters
    ----------
    t : float
        The time horizon.

    Returns
    -------
    Callable
        The raw characteristic function.
    """
    p = self.params
    alpha, beta, delta, mu = p["alpha"], p["beta"], p["delta"], p["mu"]
    gamma_0 = np.sqrt(alpha**2 - beta**2)
    log_k1_0 = np.log(kve(1, delta * gamma_0)) - delta * gamma_0

    def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
        # Levy exponent psi_1(u) = log phi_1(u) of the hyperbolic law,
        # phi_1(u) = e^{iu mu} (g_0/g_u) K_1(delta g_u) / K_1(delta g_0).
        # log K_1 is evaluated as log(kve) - z so that the dominant, linearly
        # growing phase -delta*gamma_u never passes through a complex log
        # (branch-safe), which matters once it is multiplied by t.
        gamma_u = np.sqrt(alpha**2 - (beta + 1j * u) ** 2)
        z = delta * gamma_u
        psi = (
            1j * u * mu
            + np.log(gamma_0)
            - np.log(gamma_u)
            + np.log(kve(1, z))
            - z
            - log_k1_0
        )
        return np.exp(t * psi)

    return phi_raw

sample_terminal_log_return

sample_terminal_log_return(T: float, size: int, rng: Generator) -> np.ndarray

Generates samples of the terminal log-return of the raw process.

For T == 1 the law is the hyperbolic distribution itself and scipy.stats.genhyperbolic is sampled exactly (scipy's shape parameters are the dimensionless a = alpha*delta and b = beta*delta). For any other horizon the terminal law has no closed form and is sampled by inverse transform of the characteristic function, see :func:optpricing.models.base.sample_from_cf.

Parameters:

Name Type Description Default
T float

The time to maturity, in years.

required
size int

The number of samples to generate.

required
rng Generator

A random number generator instance for reproducibility.

required

Returns:

Type Description
ndarray

An array of simulated terminal log-returns.

Source code in src/optpricing/models/hyperbolic.py
def sample_terminal_log_return(
    self,
    T: float,
    size: int,
    rng: np.random.Generator,
) -> np.ndarray:
    """
    Generates samples of the terminal log-return of the raw process.

    For ``T == 1`` the law is the hyperbolic distribution itself and
    `scipy.stats.genhyperbolic` is sampled exactly (scipy's shape parameters
    are the dimensionless ``a = alpha*delta`` and ``b = beta*delta``). For any
    other horizon the terminal law has no closed form and is sampled by
    inverse transform of the characteristic function, see
    :func:`optpricing.models.base.sample_from_cf`.

    Parameters
    ----------
    T : float
        The time to maturity, in years.
    size : int
        The number of samples to generate.
    rng : np.random.Generator
        A random number generator instance for reproducibility.

    Returns
    -------
    np.ndarray
        An array of simulated terminal log-returns.
    """
    p = self.params
    if np.isclose(T, 1.0):
        return genhyperbolic.rvs(
            p=1.0,
            a=p["alpha"] * p["delta"],
            b=p["beta"] * p["delta"],
            loc=p["mu"],
            scale=p["delta"],
            size=size,
            random_state=rng,
        )
    return sample_from_cf(self.raw_cf(t=T), size, rng)

KouModel

KouModel(params: dict[str, float] | None = None)

Bases: BaseModel

Kou double-exponential jump-diffusion model.

Initializes the Kou Double-Exponential Jump model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used.

None
Source code in src/optpricing/models/kou.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the Kou Double-Exponential Jump model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
    """
    super().__init__(params or self.default_params)

moment_strip

moment_strip() -> tuple[float, float]

-eta2 < s < eta1 (Kou 2002): the double-exponential jumps.

Source code in src/optpricing/models/kou.py
def moment_strip(self) -> tuple[float, float]:
    """``-eta2 < s < eta1`` (Kou 2002): the double-exponential jumps."""
    return float(-self.params["eta2"]), float(self.params["eta1"])

MertonJumpModel

MertonJumpModel(params: dict[str, float])

Bases: BaseModel

Merton Jump-Diffusion model, implemented as a Poisson-weighted sum of BSM prices.

Initializes the Merton Jump-Diffusion model. Uses BSMModel instance for the core diffusion

Parameters:

Name Type Description Default
params dict[str, float]

A dictionary of model parameters.

required
Source code in src/optpricing/models/merton_jump.py
def __init__(self, params: dict[str, float]):
    """
    Initializes the Merton Jump-Diffusion model.
    Uses BSMModel instance for the core diffusion

    Parameters
    ----------
    params : dict[str, float]
        A dictionary of model parameters.
    """
    super().__init__(params)
    self.bsm_solver = BSMModel(params={"sigma": self.params["sigma"]})

moment_strip

moment_strip() -> tuple[float, float]

Gaussian diffusion and log-normal jumps: every moment is finite.

Source code in src/optpricing/models/merton_jump.py
def moment_strip(self) -> tuple[float, float]:
    """Gaussian diffusion and log-normal jumps: every moment is finite."""
    return -math.inf, math.inf

NIGModel

NIGModel(params: dict[str, float] | None = None)

Bases: BaseModel

Normal Inverse Gaussian (NIG) model, a pure-jump Levy process.

Initializes the NIG model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used.

None
Source code in src/optpricing/models/nig.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the NIG model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
    """
    super().__init__(params or self.default_params)

moment_strip

moment_strip() -> tuple[float, float]

|beta + s| < alpha (Barndorff-Nielsen 1997).

Source code in src/optpricing/models/nig.py
def moment_strip(self) -> tuple[float, float]:
    """``|beta + s| < alpha`` (Barndorff-Nielsen 1997)."""
    alpha, beta = self.params["alpha"], self.params["beta"]
    return float(-alpha - beta), float(alpha - beta)

raw_cf

raw_cf(*, t: float) -> Callable

Returns the CF of the raw NIG process, without drift or spot.

This function represents the characteristic function of the process before the risk-neutral drift adjustment is applied.

Parameters:

Name Type Description Default
t float

The time horizon.

required

Returns:

Type Description
Callable

The raw characteristic function.

Source code in src/optpricing/models/nig.py
def raw_cf(self, *, t: float) -> Callable:
    """
    Returns the CF of the raw NIG process, without drift or spot.

    This function represents the characteristic function of the process
    before the risk-neutral drift adjustment is applied.

    Parameters
    ----------
    t : float
        The time horizon.

    Returns
    -------
    Callable
        The raw characteristic function.
    """
    p = self.params
    alpha, beta, delta = p["alpha"], p["beta"], p["delta"]

    def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
        term = (
            delta
            * t
            * (
                np.sqrt(alpha**2 - beta**2)
                - np.sqrt(alpha**2 - (beta + 1j * u) ** 2)
            )
        )
        return np.exp(term)

    return phi_raw

sample_terminal_log_return

sample_terminal_log_return(T: float, size: int, rng: Generator) -> np.ndarray

Generates exact samples of the terminal log-return for the NIG process.

This leverages the representation of the NIG process as a Brownian motion with drift, time-changed by an Inverse Gaussian process.

Parameters:

Name Type Description Default
T float

The time to maturity, in years.

required
size int

The number of samples to generate.

required
rng Generator

A random number generator instance for reproducibility.

required

Returns:

Type Description
ndarray

An array of simulated terminal log-returns.

Source code in src/optpricing/models/nig.py
def sample_terminal_log_return(
    self,
    T: float,
    size: int,
    rng: np.random.Generator,
) -> np.ndarray:
    """
    Generates exact samples of the terminal log-return for the NIG process.

    This leverages the representation of the NIG process as a Brownian
    motion with drift, time-changed by an Inverse Gaussian process.

    Parameters
    ----------
    T : float
        The time to maturity, in years.
    size : int
        The number of samples to generate.
    rng : np.random.Generator
        A random number generator instance for reproducibility.

    Returns
    -------
    np.ndarray
        An array of simulated terminal log-returns.
    """
    from scipy.stats import invgauss

    p = self.params
    alpha, beta, delta = p["alpha"], p["beta"], p["delta"]
    gamma = np.sqrt(alpha**2 - beta**2)
    # The NIG subordinator at horizon T is IG(mean = delta*T/gamma,
    # shape = (delta*T)^2) (Barndorff-Nielsen 1997). scipy parametrises
    # invgauss(mu, scale) as IG(mean = mu*scale, shape = scale).
    shape = (delta * T) ** 2
    ig_time = invgauss.rvs(
        mu=1.0 / (gamma * delta * T), scale=shape, size=size, random_state=rng
    )
    bm_drift = beta * ig_time
    bm_diffusion = np.sqrt(ig_time) * rng.standard_normal(size=size)
    return bm_drift + bm_diffusion

ParamValidator

A utility class containing static methods for model parameter validation.

This class is not meant to be instantiated. It serves as a namespace for common validation logic used by BaseModel subclasses.

bounded staticmethod

bounded(params: dict[str, float], key: str, low: float, high: float, *, model: str) -> None

Check if a parameter is within a specified inclusive range.

Parameters:

Name Type Description Default
params dict[str, float]

The dictionary of parameters to validate.

required
key str

The name of the parameter to check.

required
low float

The lower bound of the valid range.

required
high float

The upper bound of the valid range.

required
model str

The name of the model performing the validation, for error messages.

required

Raises:

Type Description
ValueError

If the parameter is outside the [low, high] range.

Source code in src/optpricing/models/base/validators.py
@staticmethod
def bounded(
    params: dict[str, float], key: str, low: float, high: float, *, model: str
) -> None:
    """
    Check if a parameter is within a specified inclusive range.

    Parameters
    ----------
    params : dict[str, float]
        The dictionary of parameters to validate.
    key : str
        The name of the parameter to check.
    low : float
        The lower bound of the valid range.
    high : float
        The upper bound of the valid range.
    model : str
        The name of the model performing the validation, for error messages.

    Raises
    ------
    ValueError
        If the parameter is outside the [low, high] range.
    """
    val = params.get(key)
    if val is None or not (low <= val <= high):
        raise ValueError(
            f"{model}: parameter '{key}' must be in [{low}, {high}], got {val}"
        )

positive staticmethod

positive(params: dict[str, float], keys: list[str], *, model: str) -> None

Check if specified parameters are strictly positive.

Parameters:

Name Type Description Default
params dict[str, float]

The dictionary of parameters to validate.

required
keys list[str]

A list of parameter names that must be positive.

required
model str

The name of the model performing the validation, for error messages.

required

Raises:

Type Description
ValueError

If any of the specified parameters are not strictly positive.

Source code in src/optpricing/models/base/validators.py
@staticmethod
def positive(params: dict[str, float], keys: list[str], *, model: str) -> None:
    """
    Check if specified parameters are strictly positive.

    Parameters
    ----------
    params : dict[str, float]
        The dictionary of parameters to validate.
    keys : list[str]
        A list of parameter names that must be positive.
    model : str
        The name of the model performing the validation, for error messages.

    Raises
    ------
    ValueError
        If any of the specified parameters are not strictly positive.
    """
    nonpos = [k for k in keys if params.get(k, 0.0) <= 0.0]
    if nonpos:
        raise ValueError(
            f"{model}: parameters must be positive: {', '.join(nonpos)}"
        )

require staticmethod

require(params: dict[str, float], required: list[str], *, model: str) -> None

Check for the presence of required parameters.

Parameters:

Name Type Description Default
params dict[str, float]

The dictionary of parameters to validate.

required
required list[str]

A list of parameter names that must be present in params.

required
model str

The name of the model performing the validation, for error messages.

required

Raises:

Type Description
ValueError

If any of the required parameters are missing.

Source code in src/optpricing/models/base/validators.py
@staticmethod
def require(params: dict[str, float], required: list[str], *, model: str) -> None:
    """
    Check for the presence of required parameters.

    Parameters
    ----------
    params : dict[str, float]
        The dictionary of parameters to validate.
    required : list[str]
        A list of parameter names that must be present in `params`.
    model : str
        The name of the model performing the validation, for error messages.

    Raises
    ------
    ValueError
        If any of the required parameters are missing.
    """
    missing = [k for k in required if k not in params]
    if missing:
        raise ValueError(
            f"{model}: missing required parameters: {', '.join(missing)}"
        )

PerpetualPutModel

PerpetualPutModel(params: dict[str, float])

Bases: BaseModel

Provides a closed-form price for a perpetual American put option.

A perpetual option has no expiry date. This model assumes the holder will exercise optimally. The risk-free rate and volatility are considered intrinsic parameters of the model itself.

Initializes the model with its parameters.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary requiring 'sigma' and 'rate'. If None, default_params are used.

required
Source code in src/optpricing/models/perpetual_put.py
def __init__(self, params: dict[str, float]) -> None:
    """
    Initializes the model with its parameters.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary requiring 'sigma' and 'rate'. If None, `default_params`
        are used.
    """
    super().__init__(params=params)

SABRJumpModel

SABRJumpModel(params: dict[str, float] | None = None)

Bases: BaseModel

SABR forward dynamics (see :class:~optpricing.models.SABRModel) with compensated log-normal jumps on the forward:

dF/F- = -lambda k dt + sigma_t F^(beta-1) dW + (e^J - 1) dN,
J ~ N(mu_j, sigma_j^2),  k = E[e^J] - 1.

Monte Carlo only; the three-parameter legacy form (alpha as vol of vol) is mapped to nu = alpha with a DeprecationWarning.

Initializes the SABR with Jumps model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used. Defaults to None.

None
Source code in src/optpricing/models/sabr_jump.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the SABR with Jumps model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
        Defaults to None.
    """
    params = dict(params) if params is not None else dict(self.default_params)
    if "nu" not in params and "alpha" in params:
        warnings.warn(
            "SABRJumpModel without 'nu' is deprecated: 'alpha' used to be the "
            "volatility of volatility. Interpreting nu=alpha; pass 'alpha' "
            "(initial volatility) and 'nu' (vol of vol) explicitly.",
            DeprecationWarning,
            stacklevel=2,
        )
        params["nu"] = params["alpha"]
    super().__init__(params)

SABRModel

SABRModel(params: dict[str, float] | None = None)

Bases: BaseModel

SABR model of Hagan, Kumar, Lesniewski & Woodward (2002).

The forward F and its volatility follow

dF = sigma_t F^beta dW,   d sigma_t = nu sigma_t dZ,   d<W, Z> = rho dt,

with sigma_0 = alpha. Spot inputs are converted to the forward F = S exp((r - q) T); an optional shift prices F + shift (shifted SABR, for negative or near-zero forwards).

Pricing: the closed form is Black-76 with Hagan's lognormal implied volatility (an asymptotic expansion, accurate for moderate nu^2 T and strikes not too far from the forward); Monte Carlo simulates the forward with an Euler step for F and the exact lognormal step for sigma_t.

Notes

Before v2.5 the model had three parameters and used alpha as the volatility of volatility (the initial volatility defaulted to the same number). Such parameter sets are still accepted with a DeprecationWarning and mapped to nu = alpha.

Initializes the SABR model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used. Keys:

  • alpha: initial volatility sigma_0.
  • beta: CEV exponent in [0, 1] (0 = normal, 1 = lognormal).
  • rho: correlation between forward and volatility in (-1, 1).
  • nu: volatility of volatility.
  • shift (optional): displacement for shifted SABR (default 0).
None
Source code in src/optpricing/models/sabr.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the SABR model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
        Keys:

        - ``alpha``: initial volatility ``sigma_0``.
        - ``beta``: CEV exponent in ``[0, 1]`` (0 = normal, 1 = lognormal).
        - ``rho``: correlation between forward and volatility in ``(-1, 1)``.
        - ``nu``: volatility of volatility.
        - ``shift`` (optional): displacement for shifted SABR (default 0).
    """
    params = dict(params) if params is not None else dict(self.default_params)
    if "nu" not in params and "alpha" in params:
        warnings.warn(
            "SABRModel without 'nu' is deprecated: 'alpha' used to be the "
            "volatility of volatility. Interpreting nu=alpha; pass 'alpha' "
            "(initial volatility) and 'nu' (vol of vol) explicitly.",
            DeprecationWarning,
            stacklevel=2,
        )
        params["nu"] = params["alpha"]
    super().__init__(params)

implied_volatility

implied_volatility(forward: float | ndarray, strike: float | ndarray, t: float) -> float | np.ndarray

Hagan lognormal implied volatility for (shifted) forward and strike(s).

Parameters:

Name Type Description Default
forward float | ndarray

Forward price(s) (unshifted).

required
strike float | ndarray

Strike(s) (unshifted).

required
t float

Time to expiry in years.

required

Returns:

Type Description
float | ndarray

Black implied volatility of the shifted forward F + shift.

Raises:

Type Description
ValueError

If the (shifted) forward or strike is not positive, or the expansion yields a non-positive / non-finite volatility.

Source code in src/optpricing/models/sabr.py
def implied_volatility(
    self,
    forward: float | np.ndarray,
    strike: float | np.ndarray,
    t: float,
) -> float | np.ndarray:
    """
    Hagan lognormal implied volatility for (shifted) forward and strike(s).

    Parameters
    ----------
    forward : float | np.ndarray
        Forward price(s) (unshifted).
    strike : float | np.ndarray
        Strike(s) (unshifted).
    t : float
        Time to expiry in years.

    Returns
    -------
    float | np.ndarray
        Black implied volatility of the shifted forward ``F + shift``.

    Raises
    ------
    ValueError
        If the (shifted) forward or strike is not positive, or the expansion
        yields a non-positive / non-finite volatility.
    """
    p = self.params
    f_shifted = np.asarray(forward, dtype=float) + self.shift
    k_shifted = np.asarray(strike, dtype=float) + self.shift
    if np.any(f_shifted <= 0.0) or np.any(k_shifted <= 0.0):
        raise ValueError(
            f"{self.name}: shifted forward and strike must be positive "
            "(increase 'shift' for negative or zero forwards)."
        )
    vol = hagan_lognormal_vol(
        f_shifted,
        k_shifted,
        t,
        alpha=p["alpha"],
        beta=p["beta"],
        rho=p["rho"],
        nu=p["nu"],
    )
    if not np.all(np.isfinite(vol)) or np.any(np.asarray(vol) <= 0.0):
        raise ValueError(
            f"{self.name}: Hagan's expansion yields a non-positive or undefined "
            "implied volatility for these inputs; use Monte Carlo instead."
        )
    return vol

VarianceGammaModel

VarianceGammaModel(params: dict[str, float] | None = None)

Bases: BaseModel

Variance Gamma (VG) model, a pure-jump Levy process.

Initializes the Variance Gamma (VG) model.

Parameters:

Name Type Description Default
params dict[str, float]

A dictionary of model parameters.

None
Source code in src/optpricing/models/vg.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the Variance Gamma (VG) model.

    Parameters
    ----------
    params : dict[str, float]
        A dictionary of model parameters.
    """
    super().__init__(params or self.default_params)

moment_strip

moment_strip() -> tuple[float, float]

Roots of 1 - theta nu s - sigma^2 nu s^2 / 2 (Madan, Carr & Chang 1998): the moment generating function is that base to -t/nu.

Source code in src/optpricing/models/vg.py
def moment_strip(self) -> tuple[float, float]:
    """Roots of ``1 - theta nu s - sigma^2 nu s^2 / 2`` (Madan, Carr & Chang
    1998): the moment generating function is that base to ``-t/nu``."""
    sigma, nu, theta = self.params["sigma"], self.params["nu"], self.params["theta"]
    root = np.sqrt(theta**2 + 2.0 * sigma**2 / nu)
    return float((-theta - root) / sigma**2), float((-theta + root) / sigma**2)

raw_cf

raw_cf(*, t: float) -> Callable

Returns the raw characteristic function of the VG process itself.

This function represents the characteristic function of the process before the risk-neutral drift adjustment is applied.

Parameters:

Name Type Description Default
t float

The time horizon.

required

Returns:

Type Description
Callable

The raw characteristic function.

Source code in src/optpricing/models/vg.py
def raw_cf(self, *, t: float) -> Callable:
    """
    Returns the raw characteristic function of the VG process itself.

    This function represents the characteristic function of the process
    before the risk-neutral drift adjustment is applied.

    Parameters
    ----------
    t : float
        The time horizon.

    Returns
    -------
    Callable
        The raw characteristic function.
    """
    p = self.params
    sigma, nu, theta = p["sigma"], p["nu"], p["theta"]

    def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
        return (1 - 1j * u * theta * nu + 0.5 * u**2 * sigma**2 * nu) ** (-t / nu)

    return phi_raw

sample_terminal_log_return

sample_terminal_log_return(T: float, size: int, rng: Generator) -> np.ndarray

Generates exact samples of the terminal log-return for the VG process.

This leverages the representation of the VG process as a Brownian motion with drift, time-changed by a Gamma process.

Parameters:

Name Type Description Default
T float

The time to maturity, in years.

required
size int

The number of samples to generate.

required
rng Generator

A random number generator instance for reproducibility.

required

Returns:

Type Description
ndarray

An array of simulated terminal log-returns.

Source code in src/optpricing/models/vg.py
def sample_terminal_log_return(
    self,
    T: float,
    size: int,
    rng: np.random.Generator,
) -> np.ndarray:
    """
    Generates exact samples of the terminal log-return for the VG process.

    This leverages the representation of the VG process as a Brownian
    motion with drift, time-changed by a Gamma process.

    Parameters
    ----------
    T : float
        The time to maturity, in years.
    size : int
        The number of samples to generate.
    rng : np.random.Generator
        A random number generator instance for reproducibility.

    Returns
    -------
    np.ndarray
        An array of simulated terminal log-returns.
    """

    p = self.params
    sigma, nu, theta = p["sigma"], p["nu"], p["theta"]
    gamma_time = rng.gamma(shape=T / nu, scale=nu, size=size)
    bm_drift = theta * gamma_time
    bm_diffusion = sigma * np.sqrt(gamma_time) * rng.standard_normal(size=size)
    return bm_drift + bm_diffusion

VasicekModel

VasicekModel(params: dict[str, float] | None = None)

Bases: BaseModel

Vasicek mean-reverting short rate model. dr_t = kappa * (theta - r_t) * dt + sigma * dW_t

Initializes the Vasicek model.

Parameters:

Name Type Description Default
params dict[str, float] | None

A dictionary of model parameters. If None, default_params are used. Defaults to None.

None
Source code in src/optpricing/models/vasicek.py
def __init__(self, params: dict[str, float] | None = None):
    """
    Initializes the Vasicek model.

    Parameters
    ----------
    params : dict[str, float] | None, optional
        A dictionary of model parameters. If None, `default_params` are used.
        Defaults to None.
    """
    super().__init__(params or self.default_params)