Source code for tensorquantlib.finance.black_scholes

"""
Black-Scholes option pricing — both NumPy (analytic) and Tensor (autograd) versions.

Provides:
    - bs_price_numpy: Pure NumPy analytic pricing (ground truth)
    - bs_price_tensor: Tensor-based pricing that flows through autograd
    - bs_delta, bs_gamma, bs_vega, bs_theta, bs_rho: Analytic Greeks (NumPy)
"""

from __future__ import annotations

from typing import Any, Union

import numpy as np
from scipy.stats import norm

from tensorquantlib.core.tensor import Tensor, tensor_norm_cdf

# ====================================================================== #
# Analytic Black-Scholes (NumPy) — ground truth
# ====================================================================== #


def _d1_d2(
    S: Union[float, np.ndarray],
    K: float,
    T: float,
    r: float,
    sigma: float,
    q: float = 0.0,
) -> tuple[Any, Any]:
    """Compute d1 and d2 for Black-Scholes formula."""
    d1 = (np.log(S / K) + (r - q + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return d1, d2


[docs] def bs_price_numpy( S: Union[float, np.ndarray], K: float, T: float, r: float, sigma: float, q: float = 0.0, option_type: str = "call", ) -> Any: """Analytic Black-Scholes price (pure NumPy). Args: S: Spot price (scalar or array). K: Strike price. T: Time to expiry (years). r: Risk-free rate. sigma: Volatility. q: Continuous dividend yield (default 0). option_type: 'call' or 'put'. Returns: Option price (same shape as S). """ if option_type not in ("call", "put"): raise ValueError(f"option_type must be 'call' or 'put', got {option_type!r}") S_arr = np.asarray(S, dtype=float) if np.any(S_arr <= 0): raise ValueError("Spot price S must be positive") if K <= 0: raise ValueError("Strike K must be positive") if T <= 0: raise ValueError("Time to expiry T must be positive") if sigma <= 0: raise ValueError("Volatility sigma must be positive") d1, d2 = _d1_d2(S, K, T, r, sigma, q) if option_type == "call": return S * np.exp(-q * T) * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2) else: return K * np.exp(-r * T) * norm.cdf(-d2) - S * np.exp(-q * T) * norm.cdf(-d1)
# ====================================================================== # # Analytic Greeks (NumPy) — for validation # ====================================================================== #
[docs] def bs_delta( S: Union[float, np.ndarray], K: float, T: float, r: float, sigma: float, q: float = 0.0, option_type: str = "call", ) -> Any: """Analytic Delta: dV/dS.""" d1, _ = _d1_d2(S, K, T, r, sigma, q) if option_type == "call": return np.exp(-q * T) * norm.cdf(d1) else: return -np.exp(-q * T) * norm.cdf(-d1)
[docs] def bs_gamma( S: Union[float, np.ndarray], K: float, T: float, r: float, sigma: float, q: float = 0.0, ) -> Any: """Analytic Gamma: d²V/dS² (same for call and put).""" d1, _ = _d1_d2(S, K, T, r, sigma, q) return np.exp(-q * T) * norm.pdf(d1) / (S * sigma * np.sqrt(T))
[docs] def bs_vega( S: Union[float, np.ndarray], K: float, T: float, r: float, sigma: float, q: float = 0.0, ) -> Any: """Analytic Vega: dV/dsigma (same for call and put).""" d1, _ = _d1_d2(S, K, T, r, sigma, q) return S * np.exp(-q * T) * norm.pdf(d1) * np.sqrt(T)
[docs] def bs_theta( S: Union[float, np.ndarray], K: float, T: float, r: float, sigma: float, q: float = 0.0, option_type: str = "call", ) -> Any: """Analytic Theta: -dV/dT (time decay per year).""" d1, d2 = _d1_d2(S, K, T, r, sigma, q) term1 = -S * np.exp(-q * T) * norm.pdf(d1) * sigma / (2 * np.sqrt(T)) if option_type == "call": return term1 - r * K * np.exp(-r * T) * norm.cdf(d2) + q * S * np.exp(-q * T) * norm.cdf(d1) else: return ( term1 + r * K * np.exp(-r * T) * norm.cdf(-d2) - q * S * np.exp(-q * T) * norm.cdf(-d1) )
[docs] def bs_rho( S: Union[float, np.ndarray], K: float, T: float, r: float, sigma: float, q: float = 0.0, option_type: str = "call", ) -> Any: """Analytic Rho: dV/dr.""" _, d2 = _d1_d2(S, K, T, r, sigma, q) if option_type == "call": return K * T * np.exp(-r * T) * norm.cdf(d2) else: return -K * T * np.exp(-r * T) * norm.cdf(-d2)
# ====================================================================== # # Tensor-based Black-Scholes (flows through autograd) # ====================================================================== #
[docs] def bs_price_tensor( S: Union[float, Tensor], K: Union[float, Tensor], T: Union[float, Tensor], r: Union[float, Tensor], sigma: Union[float, Tensor], q: float = 0.0, option_type: str = "call", ) -> Tensor: """Black-Scholes price using Tensor operations (supports autograd). All inputs can be Tensor objects. The computation graph is built so that calling .backward() on the result will produce gradients (Delta = dPrice/dS, Vega = dPrice/dsigma, etc.). Args: S: Spot price (Tensor or float). K: Strike price (Tensor or float). T: Time to expiry (Tensor or float). r: Risk-free rate (Tensor or float). sigma: Volatility (Tensor or float). q: Dividend yield (float, default 0). option_type: 'call' or 'put'. Returns: Option price as a Tensor. """ from tensorquantlib.core.tensor import _ensure_tensor S = _ensure_tensor(S) K = _ensure_tensor(K) T = _ensure_tensor(T) r = _ensure_tensor(r) sigma = _ensure_tensor(sigma) q_t = _ensure_tensor(q) sqrt_T = T.sqrt() d1 = ((S / K).log() + (r - q_t + sigma * sigma * _ensure_tensor(0.5)) * T) / (sigma * sqrt_T) d2 = d1 - sigma * sqrt_T discount = (r * T * _ensure_tensor(-1.0)).exp() # exp(-rT) div_discount = (q_t * T * _ensure_tensor(-1.0)).exp() # exp(-qT) if option_type == "call": price = S * div_discount * tensor_norm_cdf(d1) - K * discount * tensor_norm_cdf(d2) else: price = K * discount * tensor_norm_cdf( d2 * _ensure_tensor(-1.0) ) - S * div_discount * tensor_norm_cdf(d1 * _ensure_tensor(-1.0)) return price