European option pricing, arbitrage bounds, and implied volatility
Call:
$$C = S_0 \, e^{-qT} \, N(d_1) - K \, e^{-rT} \, N(d_2)$$
Put:
$$P = K \, e^{-rT} \, N(-d_2) - S_0 \, e^{-qT} \, N(-d_1)$$
$$d_1 = \frac{\ln\!\left(\frac{S_0}{K}\right) + \left(r - q + \frac{\sigma^2}{2}\right)T}{\sigma\sqrt{T}}$$
$$d_2 = d_1 - \sigma\sqrt{T}$$
| Symbol | Description |
|---|---|
| $S_0$ | Current stock price |
| $K$ | Strike price |
| $T$ | Time to expiration (years) |
| $r$ | Risk-free interest rate (continuous) |
| $q$ | Continuous dividend yield |
| $\sigma$ | Volatility of the underlying |
| $N(\cdot)$ | Standard normal CDF |
Call bounds:
$$\max\!\left(S_0 e^{-qT} - K e^{-rT},\; 0\right) \leq C \leq S_0 e^{-qT}$$
Put bounds:
$$\max\!\left(K e^{-rT} - S_0 e^{-qT},\; 0\right) \leq P \leq K e^{-rT}$$
Find $\sigma_{\text{IV}}$ such that:
$$C_{\text{BS}}(S_0, K, T, r, q, \sigma_{\text{IV}}) = C_{\text{market}}$$
Solved numerically via Brent's method on the interval $\sigma \in [10^{-6}, 10]$.
import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq
S = 100.0 # Spot price
K = 100.0 # Strike price
T = 1.0 # Time to expiry in years
r = 0.05 # Risk-free rate
q = 0.0 # Dividend yield
sigma = 0.20 # Volatility
option_type = "call"
def d1(S, K, T, r, q, sigma):
return (np.log(S / K) + (r - q + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
def d2(S, K, T, r, q, sigma):
return d1(S, K, T, r, q, sigma) - sigma * np.sqrt(T)
def bs_price(S, K, T, r, q, sigma, option_type):
d1_val = d1(S, K, T, r, q, sigma)
d2_val = d2(S, K, T, r, q, sigma)
if option_type == "call":
price = S * np.exp(-q * T) * norm.cdf(d1_val) - K * np.exp(-r * T) * norm.cdf(d2_val)
elif option_type == "put":
price = K * np.exp(-r * T) * norm.cdf(-d2_val) - S * np.exp(-q * T) * norm.cdf(-d1_val)
else:
raise ValueError(f"option_type must be 'call' or 'put', got '{option_type}'")
return price
sigma = 0.5
d1_val = d1(S, K, T, r, q, sigma)
d2_val = d2(S, K, T, r, q, sigma)
Nd1_val = norm.cdf(d1_val)
Nd2_val = norm.cdf(d2_val)
price = bs_price(S, K, T, r, q, sigma, option_type)
print(f"d1 = {d1_val:.6f}")
print(f"d2 = {d2_val:.6f}")
print(f"Nd1 = {Nd1_val:.6f}")
print(f"Nd2 = {Nd2_val:.6f}")
print(f"\n price = ${price:.4f}")
Every valid option price must fall within these no-arbitrage bounds.
def price_bounds(S, K, T, r, q, option_type):
pv_S = S * np.exp(-q * T) # PV of the stock (adjusted for dividends)
pv_K = K * np.exp(-r * T) # PV of the strike
if option_type == "call":
lower = max(pv_S - pv_K, 0.0)
upper = pv_S
elif option_type == "put":
lower = max(pv_K - pv_S, 0.0)
upper = pv_K
else:
raise ValueError(f"option_type must be 'call' or 'put', got '{option_type}'")
return lower, upper
lower, upper = price_bounds(S, K, T, r, q, option_type)
print(f"Lower bound : ${lower:.4f}")
print(f"BS price : ${price:.4f}")
print(f"Upper bound : ${upper:.4f}")
print(f"\nBounds satisfied: {lower <= price <= upper}")
Given a market price, recover $\sigma_{\text{IV}}$ using scipy.optimize.brentq.
def implied_volatility(S, K, T, r, q, market_price, option_type,
sigma_lo=1e-6, sigma_hi=10.0, tol=1e-12):
lower, upper = price_bounds(S, K, T, r, q, option_type)
if market_price < lower:
raise ValueError(f"Market price ${market_price:.4f} < lower bound ${lower:.4f}")
if market_price > upper:
raise ValueError(f"Market price ${market_price:.4f} > upper bound ${upper:.4f}")
def objective(sigma):
return bs_price(S, K, T, r, q, sigma, option_type) - market_price
sigma_iv = brentq(objective, sigma_lo, sigma_hi, xtol=tol)
return sigma_iv
market_price = 8
implied_volatility(S, K, T, r, q, market_price, option_type,
sigma_lo=1e-6, sigma_hi=10.0, tol=1e-12)
With the closed-form price in hand, the natural next step is the sensitivities. The full Option Greeks notebook derives Delta, Gamma, Theta and Vega from the same $d_1$ and $d_2$ helpers used above. For the 3D picture of gamma across moneyness, vol and time, the Gamma Surface walkthrough plots the same formula as an animated surface.
For numerical alternatives to the analytical Black-Scholes price, see the Monte Carlo under GBM notebook (simulation-based) or the CRR Binomial Tree (which converges to Black-Scholes as the number of steps grows and also prices American options).