Black-Scholes Option Pricing Model

European option pricing, arbitrage bounds, and implied volatility

Newsletter

Get exclusive models, prompts and code

One short email when there's something new. finance models, code walkthroughs, agentic prompts. No spam.

Built with
Anthropic Claude Python R ChatGPT GitHub Jupyter Excel PowerPoint VS Code SQL MATLAB

1. Black-Scholes Formulas

Option Price

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)$$

Intermediate Variables

$$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}$$

Parameters

SymbolDescription
$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

Arbitrage Boundaries

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}$$

Implied Volatility

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]$.


2. Implementation

import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq

2.1 Core Functions: $d_1$, $d_2$, and Pricing

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

2.3 Price the Option

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}")

3. Arbitrage Boundary Values

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}")

4. Implied Volatility

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)

Where to go next

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).

David Arias, CFA
Written and Modelled by

David Arias, CFA

Licensed portfolio manager with 4+ years of experience, specializing in emerging markets private debt, derivatives, and quantitative finance.

Let's Connect