Skip to content

Calibration

The calibration package provides tools for fitting financial models to market data, estimating parameters from historical data, and calculating implied volatility surfaces.

BSMIVSolver

BSMIVSolver(max_iter: int = 20, tolerance: float = 1e-06)

High-performance, vectorized Newton-Raphson solver for BSM implied volatility.

This solver is designed to calculate the implied volatility for a large number of options simultaneously, leveraging NumPy for vectorized operations.

Initializes the BSM implied volatility solver.

Parameters:

Name Type Description Default
max_iter int

The maximum number of iterations for the Newton-Raphson method, by default 20.

20
tolerance float

The error tolerance to determine convergence, by default 1e-6.

1e-06
Source code in src/optpricing/calibration/vectorized_bsm_iv.py
def __init__(
    self,
    max_iter: int = 20,
    tolerance: float = 1e-6,
):
    """
    Initializes the BSM implied volatility solver.

    Parameters
    ----------
    max_iter : int, optional
        The maximum number of iterations for the Newton-Raphson method,
        by default 20.
    tolerance : float, optional
        The error tolerance to determine convergence, by default 1e-6.
    """
    self.max_iter = max_iter
    self.tolerance = tolerance

solve

solve(
    target_prices: ndarray,
    options: DataFrame,
    stock: Stock,
    rate: Rate,
) -> np.ndarray

Calculates implied volatility for an array of options.

Parameters:

Name Type Description Default
target_prices ndarray

An array of market prices for which to find the implied volatility.

required
options DataFrame

A DataFrame of option contracts, must include 'strike', 'maturity', and 'optionType' columns.

required
stock Stock

The underlying asset's properties.

required
rate Rate

The risk-free rate structure.

required

Returns:

Type Description
ndarray

An array of calculated implied volatilities corresponding to the target prices.

Source code in src/optpricing/calibration/vectorized_bsm_iv.py
def solve(
    self,
    target_prices: np.ndarray,
    options: pd.DataFrame,
    stock: Stock,
    rate: Rate,
) -> np.ndarray:
    """
    Calculates implied volatility for an array of options.

    Parameters
    ----------
    target_prices : np.ndarray
        An array of market prices for which to find the implied volatility.
    options : pd.DataFrame
        A DataFrame of option contracts, must include 'strike', 'maturity',
        and 'optionType' columns.
    stock : Stock
        The underlying asset's properties.
    rate : Rate
        The risk-free rate structure.

    Returns
    -------
    np.ndarray
        An array of calculated implied volatilities corresponding to the
        target prices.
    """
    S, q = stock.spot, stock.dividend
    K, T = options["strike"].values, options["maturity"].values
    r = rate.get_rate(T)  # Use get_rate for term structure
    is_call = options["optionType"].values == "call"
    iv = np.full_like(target_prices, 0.20)

    for _ in range(self.max_iter):
        with np.errstate(all="ignore"):
            sqrt_T = np.sqrt(T)
            d1 = (np.log(S / K) + (r - q + 0.5 * iv**2) * T) / (iv * sqrt_T)
            d2 = d1 - iv * sqrt_T
            vega = S * np.exp(-q * T) * sqrt_T * norm.pdf(d1)
            call_prices = S * np.exp(-q * T) * norm.cdf(d1) - K * np.exp(
                -r * T
            ) * norm.cdf(d2)
            put_prices = K * np.exp(-r * T) * norm.cdf(-d2) - S * np.exp(
                -q * T
            ) * norm.cdf(-d1)
        model_prices = np.where(is_call, call_prices, put_prices)
        error = model_prices - target_prices
        if np.all(np.abs(error) < self.tolerance):
            break
        iv = iv - error / np.maximum(vega, 1e-8)
    return iv

Calibrator

Calibrator(
    model: BaseModel,
    market_data: DataFrame,
    stock: Stock,
    rate: Rate,
)

A generic class for calibrating financial models to market data.

This class orchestrates the process of finding the model parameters that minimize the difference between model prices and observed market prices.

Initializes the Calibrator.

Parameters:

Name Type Description Default
model BaseModel

The financial model to be calibrated (e.g., HestonModel).

required
market_data DataFrame

A DataFrame containing market prices of options. Must include 'strike', 'maturity', 'optionType', and 'marketPrice' columns.

required
stock Stock

The underlying asset's properties.

required
rate Rate

The risk-free rate structure.

required
Source code in src/optpricing/calibration/calibrator.py
def __init__(
    self,
    model: BaseModel,
    market_data: pd.DataFrame,
    stock: Stock,
    rate: Rate,
):
    """
    Initializes the Calibrator.

    Parameters
    ----------
    model : BaseModel
        The financial model to be calibrated (e.g., HestonModel).
    market_data : pd.DataFrame
        A DataFrame containing market prices of options. Must include
        'strike', 'maturity', 'optionType', and 'marketPrice' columns.
    stock : Stock
        The underlying asset's properties.
    rate : Rate
        The risk-free rate structure.
    """
    self.model = model
    self.market_data = market_data
    self.stock = stock
    self.rate = rate
    self.technique = select_fastest_technique(model)
    _class_name = self.technique.__class__.__name__
    print(f"Calibrator using '{_class_name}' for model '{model.name}'.")

fit

fit(
    initial_guess: dict[str, float],
    bounds: dict[str, tuple],
    frozen_params: dict[str, float] = None,
) -> dict[str, float]

Performs the calibration using an optimization algorithm.

This method uses scipy.optimize.minimize (or minimize_scalar for a single parameter) to find the optimal set of parameters that minimizes the objective function.

Parameters:

Name Type Description Default
initial_guess dict[str, float]

A dictionary of initial guesses for the parameters to be fitted.

required
bounds dict[str, tuple]

A dictionary mapping parameter names to their (min, max) bounds.

required
frozen_params dict[str, float] | None

A dictionary of parameters to hold constant during the optimization. Defaults to None.

None

Returns:

Type Description
dict[str, float]

A dictionary containing the full set of calibrated and frozen parameters.

Source code in src/optpricing/calibration/calibrator.py
def fit(
    self,
    initial_guess: dict[str, float],
    bounds: dict[str, tuple],
    frozen_params: dict[str, float] = None,
) -> dict[str, float]:
    """
    Performs the calibration using an optimization algorithm.

    This method uses `scipy.optimize.minimize` (or `minimize_scalar` for
    a single parameter) to find the optimal set of parameters that
    minimizes the objective function.

    Parameters
    ----------
    initial_guess : dict[str, float]
        A dictionary of initial guesses for the parameters to be fitted.
    bounds : dict[str, tuple]
        A dictionary mapping parameter names to their (min, max) bounds.
    frozen_params : dict[str, float] | None, optional
        A dictionary of parameters to hold constant during the optimization.
        Defaults to None.

    Returns
    -------
    dict[str, float]
        A dictionary containing the full set of calibrated and frozen parameters.
    """
    frozen_params = frozen_params or {}
    params_to_fit_names = [p for p in initial_guess if p not in frozen_params]
    print(f"Fitting parameters: {params_to_fit_names}")
    if not params_to_fit_names:
        return frozen_params

    fit_bounds = [bounds.get(p) for p in params_to_fit_names]
    initial_values = [initial_guess[p] for p in params_to_fit_names]

    if len(params_to_fit_names) == 1:
        # scalar minimizer for one parameter
        res = minimize_scalar(
            lambda x: self._objective_function(
                np.array([x]), params_to_fit_names, frozen_params
            ),
            bounds=fit_bounds[0],
            method="bounded",
        )
        final_params = {**frozen_params, params_to_fit_names[0]: res.x}
        print(f"Scalar optimization finished. Final loss: {res.fun:.6f}")
    else:
        # gradient-based optimizer for multiple parameters
        res = minimize(
            fun=self._objective_function,
            x0=initial_values,
            args=(params_to_fit_names, frozen_params),
            method="L-BFGS-B",
            bounds=fit_bounds,
        )
        final_params = {**frozen_params, **dict(zip(params_to_fit_names, res.x))}
        print(f"Multivariate optimization finished. Final loss: {res.fun:.6f}")
    return final_params

VectorizedIntegrationIVSolver

VectorizedIntegrationIVSolver(
    max_iter: int = 20,
    tolerance: float = 1e-07,
    upper_bound: float = 200.0,
)

A high-performance, vectorized Secant method solver for implied volatility for any model that supports a characteristic function.

Initializes the vectorized IV solver.

Parameters:

Name Type Description Default
max_iter int

Maximum number of iterations for the Secant method, by default 20.

20
tolerance float

Error tolerance for convergence, by default 1e-7.

1e-07
upper_bound float

The upper limit for the numerical integration, by default 200.0.

200.0
Source code in src/optpricing/calibration/vectorized_integration_iv.py
def __init__(
    self,
    max_iter: int = 20,
    tolerance: float = 1e-7,
    upper_bound: float = 200.0,
):
    """
    Initializes the vectorized IV solver.

    Parameters
    ----------
    max_iter : int, optional
        Maximum number of iterations for the Secant method, by default 20.
    tolerance : float, optional
        Error tolerance for convergence, by default 1e-7.
    upper_bound : float, optional
        The upper limit for the numerical integration, by default 200.0.
    """
    self.max_iter = max_iter
    self.tolerance = tolerance
    self.upper_bound = upper_bound

solve

solve(
    target_prices: ndarray,
    options: DataFrame,
    model: BaseModel,
    rate: Rate,
) -> np.ndarray

Calculates implied volatility for an array of options and prices.

This method uses a vectorized Secant root-finding algorithm. The pricing at each step is performed using a vectorized version of the Gil-Pelaez inversion formula.

Parameters:

Name Type Description Default
target_prices ndarray

An array of market prices for which to find the implied volatility.

required
options DataFrame

A DataFrame of option contracts.

required
model BaseModel

The financial model to use for pricing.

required
rate Rate

The risk-free rate structure.

required

Returns:

Type Description
ndarray

An array of calculated implied volatilities.

Source code in src/optpricing/calibration/vectorized_integration_iv.py
def solve(
    self,
    target_prices: np.ndarray,
    options: pd.DataFrame,
    model: BaseModel,
    rate: Rate,
) -> np.ndarray:
    """
    Calculates implied volatility for an array of options and prices.

    This method uses a vectorized Secant root-finding algorithm. The pricing
    at each step is performed using a vectorized version of the Gil-Pelaez
    inversion formula.

    Parameters
    ----------
    target_prices : np.ndarray
        An array of market prices for which to find the implied volatility.
    options : pd.DataFrame
        A DataFrame of option contracts.
    model : BaseModel
        The financial model to use for pricing.
    rate : Rate
        The risk-free rate structure.

    Returns
    -------
    np.ndarray
        An array of calculated implied volatilities.
    """
    iv0 = np.full_like(target_prices, 0.20)
    iv1 = np.full_like(target_prices, 0.25)

    p0 = self._price_vectorized(iv0, options, model, rate)
    p1 = self._price_vectorized(iv1, options, model, rate)

    f0 = p0 - target_prices
    f1 = p1 - target_prices

    for _ in range(self.max_iter):
        if np.all(np.abs(f1) < self.tolerance):
            break
        denom = f1 - f0
        denom[np.abs(denom) < 1e-12] = 1e-12
        iv_next = iv1 - f1 * (iv1 - iv0) / denom
        iv_next = np.clip(iv_next, 1e-4, 5.0)
        iv0, iv1 = iv1, iv_next
        f0 = f1
        p1 = self._price_vectorized(iv1, options, model, rate)
        f1 = p1 - target_prices

    return iv1

VolatilitySurface

VolatilitySurface(option_data: DataFrame)

A class to compute and hold market and model-implied volatility surfaces.

This class takes a DataFrame of option market data and provides methods to calculate the Black-Scholes implied volatility (IV) for each option, either from its market price or from a price generated by a financial model.

Initializes the VolatilitySurface.

Parameters:

Name Type Description Default
option_data DataFrame

A DataFrame containing market prices of options. Must include 'strike', 'maturity', 'marketPrice', 'optionType', and 'expiry' columns.

required

Raises:

Type Description
ValueError

If any of the required columns are missing from option_data.

Source code in src/optpricing/calibration/iv_surface.py
def __init__(self, option_data: pd.DataFrame):
    """
    Initializes the VolatilitySurface.

    Parameters
    ----------
    option_data : pd.DataFrame
        A DataFrame containing market prices of options. Must include
        'strike', 'maturity', 'marketPrice', 'optionType', and 'expiry' columns.

    Raises
    ------
    ValueError
        If any of the required columns are missing from `option_data`.
    """
    required_cols = ["strike", "maturity", "marketPrice", "optionType", "expiry"]
    if not all(col in option_data.columns for col in required_cols):
        msg = (
            "Input option_data is missing one "
            f"of the required columns: {required_cols}"
        )
        raise ValueError(msg)

    self.data = option_data[required_cols].copy()
    self.surface: pd.DataFrame | None = None
    self.iv_solver = BSMIVSolver()

calculate_market_iv

calculate_market_iv(
    stock: Stock, rate: Rate
) -> VolatilitySurface

Calculates the market implied volatility surface from market prices.

This method inverts the Black-Scholes formula for each option's market price to find the corresponding implied volatility. The results are stored in the self.surface DataFrame.

Parameters:

Name Type Description Default
stock Stock

The underlying asset's properties.

required
rate Rate

The risk-free rate structure.

required

Returns:

Type Description
VolatilitySurface

The same instance of the class, allowing for method chaining.

Source code in src/optpricing/calibration/iv_surface.py
def calculate_market_iv(
    self,
    stock: Stock,
    rate: Rate,
) -> VolatilitySurface:
    """
    Calculates the market implied volatility surface from market prices.

    This method inverts the Black-Scholes formula for each option's market
    price to find the corresponding implied volatility. The results are
    stored in the `self.surface` DataFrame.

    Parameters
    ----------
    stock : Stock
        The underlying asset's properties.
    rate : Rate
        The risk-free rate structure.

    Returns
    -------
    VolatilitySurface
        The same instance of the class, allowing for method chaining.
    """
    print("Calculating market implied volatility surface...")
    market_ivs = self._calculate_ivs(stock, rate, self.data["marketPrice"])
    self.surface = self.data.copy()
    self.surface["iv"] = market_ivs
    self.surface.dropna(inplace=True)
    self.surface = self.surface[
        (self.surface["iv"] > 1e-4) & (self.surface["iv"] < 2.0)
    ]
    return self

calculate_model_iv

calculate_model_iv(
    stock: Stock,
    rate: Rate,
    model: BaseModel,
    technique: BaseTechnique = None,
) -> VolatilitySurface

Calculates a model's implied volatility surface.

This method first prices every option in the dataset using the provided model and technique. It then inverts the Black-Scholes formula for each of these model prices to generate the model-implied volatility surface.

Parameters:

Name Type Description Default
stock Stock

The underlying asset's properties.

required
rate Rate

The risk-free rate structure.

required
model BaseModel

The financial model to generate prices from.

required
technique BaseTechnique

The pricing technique to use with the model.

None

Returns:

Type Description
VolatilitySurface

The same instance of the class, allowing for method chaining.

Source code in src/optpricing/calibration/iv_surface.py
def calculate_model_iv(
    self,
    stock: Stock,
    rate: Rate,
    model: BaseModel,
    technique: BaseTechnique = None,
) -> VolatilitySurface:
    """
    Calculates a model's implied volatility surface.

    This method first prices every option in the dataset using the provided
    model and technique. It then inverts the Black-Scholes formula for each
    of these model prices to generate the model-implied volatility surface.

    Parameters
    ----------
    stock : Stock
        The underlying asset's properties.
    rate : Rate
        The risk-free rate structure.
    model : BaseModel
        The financial model to generate prices from.
    technique : BaseTechnique
        The pricing technique to use with the model.

    Returns
    -------
    VolatilitySurface
        The same instance of the class, allowing for method chaining.
    """
    print(f"Calculating {model.name} implied volatility surface...")

    model_prices = price_options_vectorized(
        options_df=self.data,
        stock=stock,
        model=model,
        rate=rate,
        **model.params,
    )

    model_ivs = self._calculate_ivs(
        stock, rate, pd.Series(model_prices, index=self.data.index)
    )
    self.surface = self.data.copy()
    self.surface["iv"] = model_ivs
    self.surface.dropna(inplace=True)
    return self

find_atm_options

find_atm_options(
    calls: DataFrame, puts: DataFrame, spot: float
) -> pd.DataFrame

Finds the closest at-the-money (ATM) call-put pair for each expiry.

Parameters:

Name Type Description Default
calls DataFrame

A DataFrame of call options.

required
puts DataFrame

A DataFrame of put options.

required
spot float

The current spot price of the underlying.

required

Returns:

Type Description
DataFrame

A DataFrame containing the merged ATM call-put pairs.

Source code in src/optpricing/calibration/fit_market_params.py
def find_atm_options(
    calls: pd.DataFrame,
    puts: pd.DataFrame,
    spot: float,
) -> pd.DataFrame:
    """
    Finds the closest at-the-money (ATM) call-put pair for each expiry.

    Parameters
    ----------
    calls : pd.DataFrame
        A DataFrame of call options.
    puts : pd.DataFrame
        A DataFrame of put options.
    spot : float
        The current spot price of the underlying.

    Returns
    -------
    pd.DataFrame
        A DataFrame containing the merged ATM call-put pairs.
    """
    merged = pd.merge(
        calls, puts, on=["strike", "maturity"], suffixes=("_call", "_put"), how="inner"
    )
    if merged.empty:
        return pd.DataFrame()
    merged["moneyness_dist"] = abs(merged["strike"] - spot)
    atm_indices = merged.groupby("maturity")["moneyness_dist"].idxmin()
    return merged.loc[atm_indices]

fit_jump_params_from_history

fit_jump_params_from_history(
    log_returns: Series, threshold_stds: float = 3.0
) -> dict

Estimates jump parameters and diffusion volatility from historical returns.

This function separates historical log returns into a "diffusion" component (normal daily movements) and a "jump" component (extreme movements) based on a standard deviation threshold. It then calculates the annualized parameters for a jump-diffusion model like Merton's.

Parameters:

Name Type Description Default
log_returns Series

A pandas Series of daily log returns.

required
threshold_stds float

The number of standard deviations to use as a threshold for identifying jumps, by default 3.0.

3.0

Returns:

Type Description
dict

A dictionary containing the estimated parameters: 'sigma', 'lambda', 'mu_j', and 'sigma_j'.

Source code in src/optpricing/calibration/fit_jump_parameters.py
def fit_jump_params_from_history(
    log_returns: pd.Series,
    threshold_stds: float = 3.0,
) -> dict:
    """
    Estimates jump parameters and diffusion volatility from historical returns.

    This function separates historical log returns into a "diffusion" component
    (normal daily movements) and a "jump" component (extreme movements) based
    on a standard deviation threshold. It then calculates the annualized
    parameters for a jump-diffusion model like Merton's.

    Parameters
    ----------
    log_returns : pd.Series
        A pandas Series of daily log returns.
    threshold_stds : float, optional
        The number of standard deviations to use as a threshold for identifying
        jumps, by default 3.0.

    Returns
    -------
    dict
        A dictionary containing the estimated parameters: 'sigma', 'lambda',
        'mu_j', and 'sigma_j'.
    """
    print("Fitting jump parameters from historical returns...")

    std_dev = log_returns.std()
    jump_threshold = threshold_stds * std_dev

    diffusion_returns = log_returns[abs(log_returns) < jump_threshold]
    jump_returns = log_returns[abs(log_returns) >= jump_threshold]

    # Annualize daily std dev by multiplying by sqrt(252 trading days)
    sigma_est = diffusion_returns.std() * np.sqrt(252)

    if len(jump_returns) > 2:
        lambda_est = len(jump_returns) / len(log_returns) * 252
        mu_j_est = jump_returns.mean()
        sigma_j_est = jump_returns.std()
    else:
        lambda_est, mu_j_est, sigma_j_est = 0.1, 0.0, 0.0
        print("  -> Warning: Not enough jumps detected. Using default jump parameters.")

    # Use the key 'sigma' to match the parameter name in the Merton/Kou models.
    fitted_params = {
        "sigma": sigma_est,
        "lambda": lambda_est,
        "mu_j": mu_j_est,
        "sigma_j": sigma_j_est,
    }

    formatted = ", ".join(f"{k}: {v:.4f}" for k, v in fitted_params.items())
    print(f"  -> Estimated Historical Params: {{{formatted}}}")
    return fitted_params

fit_rate_and_dividend

fit_rate_and_dividend(
    calls: DataFrame,
    puts: DataFrame,
    spot: float,
    r_fixed: float | None = None,
    q_fixed: float | None = None,
) -> tuple[float, float]

Fits the risk-free rate (r) and dividend yield (q) from put-call parity.

This function uses the prices of at-the-money (ATM) call-put pairs to solve for the r and q that minimize the parity pricing error. Parameters can be held fixed or fitted.

Parameters:

Name Type Description Default
calls DataFrame

A DataFrame of call options with 'strike', 'maturity', 'marketPrice'.

required
puts DataFrame

A DataFrame of put options with 'strike', 'maturity', 'marketPrice'.

required
spot float

The current spot price of the underlying.

required
r_fixed float | None

If provided, the risk-free rate is held fixed at this value. Defaults to None.

None
q_fixed float | None

If provided, the dividend yield is held fixed at this value. Defaults to None.

None

Returns:

Type Description
tuple[float, float]

A tuple containing the estimated (or fixed) risk-free rate and dividend yield.

Source code in src/optpricing/calibration/fit_market_params.py
def fit_rate_and_dividend(
    calls: pd.DataFrame,
    puts: pd.DataFrame,
    spot: float,
    r_fixed: float | None = None,
    q_fixed: float | None = None,
) -> tuple[float, float]:
    """
    Fits the risk-free rate (r) and dividend yield (q) from put-call parity.

    This function uses the prices of at-the-money (ATM) call-put pairs to
    solve for the `r` and `q` that minimize the parity pricing error.
    Parameters can be held fixed or fitted.

    Parameters
    ----------
    calls : pd.DataFrame
        A DataFrame of call options with 'strike', 'maturity', 'marketPrice'.
    puts : pd.DataFrame
        A DataFrame of put options with 'strike', 'maturity', 'marketPrice'.
    spot : float
        The current spot price of the underlying.
    r_fixed : float | None, optional
        If provided, the risk-free rate is held fixed at this value.
        Defaults to None.
    q_fixed : float | None, optional
        If provided, the dividend yield is held fixed at this value.
        Defaults to None.

    Returns
    -------
    tuple[float, float]
        A tuple containing the estimated (or fixed) risk-free rate and dividend yield.
    """
    atm_pairs = find_atm_options(calls, puts, spot)
    if atm_pairs.empty:
        print("Warning: No ATM pairs found. Using default r=0.05, q=0.0.")
        return 0.05, 0.0

    free_param_indices, initial_guess, bounds = [], [], []
    if r_fixed is None:
        free_param_indices.append(0)
        initial_guess.append(0.05)
        bounds.append((0.0, 0.15))
    if q_fixed is None:
        free_param_indices.append(1)
        initial_guess.append(0.01)
        bounds.append((0.0, 0.10))

    if not free_param_indices:
        return r_fixed, q_fixed

    def parity_error(x: np.ndarray) -> float:
        r = r_fixed if 0 not in free_param_indices else x[free_param_indices.index(0)]
        q = q_fixed if 1 not in free_param_indices else x[free_param_indices.index(1)]
        parity_rhs = spot * np.exp(-q * atm_pairs["maturity"]) - atm_pairs[
            "strike"
        ] * np.exp(-r * atm_pairs["maturity"])
        parity_lhs = atm_pairs["marketPrice_call"] - atm_pairs["marketPrice_put"]
        return np.sum((parity_lhs - parity_rhs) ** 2)

    solution = minimize(
        fun=parity_error, x0=initial_guess, bounds=bounds, method="L-BFGS-B"
    )
    r_est = (
        r_fixed
        if 0 not in free_param_indices
        else solution.x[free_param_indices.index(0)]
    )
    q_est = (
        q_fixed
        if 1 not in free_param_indices
        else solution.x[free_param_indices.index(1)]
    )
    print(f"Fitted market params -> r: {r_est:.4f}, q: {q_est:.4f}")
    return float(r_est), float(q_est)

price_options_vectorized

price_options_vectorized(
    options_df: DataFrame,
    stock: Stock,
    model: BaseModel,
    rate: Rate,
    *,
    upper_bound: float = 200.0,
    **kwargs: Any,
) -> np.ndarray

Vectorised integral pricer (Carr-Madan representation).

Parameters:

Name Type Description Default
options_df DataFrame

Must contain strike, maturity and optionType. The index is reset internally to guarantee safe positional writes.

required
stock Stock

Underlying description.

required
model BaseModel

Any model exposing a cf(t, spot, r, q) callable.

required
rate Rate

Continuous zero-curve.

required
upper_bound float

Integration truncation limit (works for double precision).

200

Returns:

Type Description
ndarray

Model prices - aligned with the row order of options_df.

Source code in src/optpricing/calibration/vectorized_pricer.py
def price_options_vectorized(
    options_df: pd.DataFrame,
    stock: Stock,
    model: BaseModel,
    rate: Rate,
    *,
    upper_bound: float = 200.0,
    **kwargs: Any,
) -> np.ndarray:
    """
    Vectorised integral pricer (Carr-Madan representation).

    Parameters
    ----------
    options_df : pd.DataFrame
        Must contain `strike`, `maturity` and `optionType`.
        The index is reset internally to guarantee safe positional writes.
    stock : Stock
        Underlying description.
    model : BaseModel
        Any model exposing a `cf(t, spot, r, q)` callable.
    rate : Rate
        Continuous zero-curve.
    upper_bound : float, default 200
        Integration truncation limit (works for double precision).

    Returns
    -------
    np.ndarray
        Model prices - aligned with the row order of options_df.
    """
    options_df = options_df.reset_index(drop=True)

    n_opts = len(options_df)
    prices = np.empty(n_opts)

    S = stock.spot
    q = stock.dividend

    for T, grp in options_df.groupby("maturity", sort=False):
        loc = grp.index.to_numpy()
        K = grp["strike"].to_numpy()
        is_call = grp["optionType"].to_numpy() == "call"

        r = rate.get_rate(T)
        phi = model.cf(t=T, spot=S, r=r, q=q, **kwargs)
        lnK = np.log(K)

        def _integrand_p2(u: float) -> np.ndarray:
            return (np.exp(-1j * u * lnK) * phi(u)).imag / u

        def _integrand_p1(u: float) -> np.ndarray:
            return (np.exp(-1j * u * lnK) * phi(u - 1j)).imag / u

        # Vectorised quad once per maturity
        p2, _ = integrate.quad_vec(_integrand_p2, 1e-15, upper_bound)
        p1, _ = integrate.quad_vec(_integrand_p1, 1e-15, upper_bound)

        denom = np.real(phi(-1j))
        denom = 1.0 if abs(denom) < 1e-12 else denom

        P1 = 0.5 + p1 / (np.pi * denom)
        P2 = 0.5 + p2 / np.pi

        call_vals = S * np.exp(-q * T) * P1 - K * np.exp(-r * T) * P2
        put_vals = K * np.exp(-r * T) * (1.0 - P2) - S * np.exp(-q * T) * (1.0 - P1)

        prices[loc] = np.where(is_call, call_vals, put_vals)

    return prices

select_fastest_technique

select_fastest_technique(model: BaseModel)

Selects the fastest available pricing technique for a given model.

The function checks the capabilities of the model in a specific order of preference, which generally corresponds to computational speed.

The order of preference is: 1. Closed-Form 2. Fast Fourier Transform (FFT) 3. Monte Carlo

Parameters:

Name Type Description Default
model BaseModel

The financial model for which to select a technique.

required

Returns:

Type Description
BaseTechnique

An instance of the fastest suitable pricing technique.

Raises:

Type Description
TypeError

If no suitable pricing technique can be found for the model.

Source code in src/optpricing/calibration/technique_selector.py
def select_fastest_technique(model: BaseModel):
    """
    Selects the fastest available pricing technique for a given model.

    The function checks the capabilities of the model in a specific order of
    preference, which generally corresponds to computational speed.

    The order of preference is:
    1. Closed-Form
    2. Fast Fourier Transform (FFT)
    3. Monte Carlo

    Parameters
    ----------
    model : BaseModel
        The financial model for which to select a technique.

    Returns
    -------
    BaseTechnique
        An instance of the fastest suitable pricing technique.

    Raises
    ------
    TypeError
        If no suitable pricing technique can be found for the model.
    """
    if model.has_closed_form:
        return ClosedFormTechnique()
    if model.supports_cf:
        return FFTTechnique(n=12)
    if model.supports_sde:
        return MonteCarloTechnique(n_paths=5000, n_steps=50, antithetic=True)
    raise TypeError(f"No suitable pricing technique found for model '{model.name}'")