Black-Scholes Explained Simply – With Python Code

Black-Scholes Explained Simply – With Python Code

The Black-Scholes model is the foundation of modern option pricing. Published in 1973 by Fischer Black, Myron Scholes, and Robert Merton, it revolutionized finance by providing the first closed-form formula for European option prices. Every quant interview starts here.

This guide breaks down the model from first principles, implements it in Python, covers the Greeks, and explains where and why it fails.

The Black-Scholes Formula

Under the risk-neutral measure $mathbb{Q}$, a European call option price is:

$$
C_{BS} = S_0 , N(d_1) – K e^{-rT} N(d_2)
$$

where:

$$
d_1 = frac{ln(S_0/K) + (r + sigma^2/2)T}{sigmasqrt{T}}, quad d_2 = d_1 – sigmasqrt{T}
$$

  • $S_0$ – current stock price
  • $K$ – strike price
  • $r$ – risk-free rate
  • $sigma$ – volatility (annualized)
  • $T$ – time to expiration (years)
  • $N(cdot)$ – cumulative standard normal distribution

For a European put, use put-call parity:

$$
P = C – S_0 + K e^{-rT}
$$

The Five Assumptions

Assumption Reality Impact
Constant volatility Vol varies by strike/time (smile) Model underprices OTM options
Log-normal returns Returns have fat tails Underestimates crash risk
No dividends Most stocks pay dividends Overprices calls on dividend stocks
No transaction costs Spreads exist Real hedging costs more
Continuous trading Markets close, gaps happen Overnight risk unmodeled

Understanding these assumptions is critical for interviews. When a trader says “the model is wrong,” they mean one of these is violated.

Python Implementation

import numpy as np
from scipy.stats import norm

def bs_call(S, K, r, T, sigma):
    """Black-Scholes European call price."""
    d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)
    return S * norm.cdf(d1) - K * np.exp(-r*T) * norm.cdf(d2)

def bs_put(S, K, r, T, sigma):
    """Black-Scholes European put price."""
    d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)
    return K * np.exp(-r*T) * norm.cdf(-d2) - S * norm.cdf(-d1)

# Price an ATM call
S0, K, r, sigma, T = 100, 100, 0.05, 0.20, 1.0
call_price = bs_call(S0, K, r, T, sigma)
put_price = bs_put(S0, K, r, T, sigma)

print(f"Call Price: ${call_price:.4f}")
print(f"Put Price:  ${put_price:.4f}")

Output:

Call Price: $10.4506
Put Price:  $5.5735

The Greeks: Risk Sensitivities

The Greeks tell traders how the option price changes when inputs move. This is what you actually use on the desk.

Delta ($Delta$)

$$
Delta_{call} = N(d_1), quad Delta_{put} = N(d_1) – 1
$$

Delta is the sensitivity to the underlying price. A delta of 0.63 means the option price moves $0.63 for every $1 move in the stock.

Gamma ($Gamma$)

$$
Gamma = frac{N'(d_1)}{S_0 sigma sqrt{T}}
$$

Gamma is the rate of change of delta. High gamma = the option’s delta changes rapidly = more convexity = more valuable.

Vega ($nu$)

$$
nu = S_0 sqrt{T} , N'(d_1)
$$

Vega is the sensitivity to volatility. This is the most important Greek for vol traders. A vega of 15 means the option price changes $15 for each 1% move in implied volatility.

Theta ($Theta$)

$$
Theta_{call} = -frac{S_0 N'(d_1) sigma}{2sqrt{T}} – r K e^{-rT} N(d_2)
$$

Theta is time decay. Options lose value as expiration approaches. This is the cost of holding a long option position.

Rho ($rho$)

$$
rho_{call} = K T e^{-rT} N(d_2)
$$

Rho is the sensitivity to interest rates. Less important in low-rate environments, but matters for long-dated options.

Python: Greeks Calculator

def bs_greeks(S, K, r, T, sigma):
    """Compute all Greeks for a European call."""
    d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)

    delta = norm.cdf(d1)
    gamma = norm.pdf(d1) / (S * sigma * np.sqrt(T))
    vega = S * norm.pdf(d1) * np.sqrt(T) / 100  # per 1% vol move
    theta = (-(S * norm.pdf(d1) * sigma) / (2*np.sqrt(T)) 
             - r * K * np.exp(-r*T) * norm.cdf(d2)) / 365  # per day
    rho = K * T * np.exp(-r*T) * norm.cdf(d2) / 100  # per 1% rate move

    return {'delta': delta, 'gamma': gamma, 'vega': vega, 
            'theta': theta, 'rho': rho}

greeks = bs_greeks(100, 100, 0.05, 1.0, 0.20)
for name, val in greeks.items():
    print(f"{name:>6s}: {val:.4f}")

Output:

 delta: 0.6368
 gamma: 0.0188
  vega: 0.3752
 theta: -0.0176
   rho: 0.5323

Implied Volatility

Given a market price, we can back out the volatility that makes Black-Scholes match that price. This is implied volatility – the market’s forecast of future volatility.

from scipy.optimize import brentq

def implied_vol(price, S, K, r, T):
    """Extract implied volatility from market price."""
    try:
        return brentq(
            lambda sigma: bs_call(S, K, r, T, sigma) - price, 
            0.01, 3.0
        )
    except:
        return np.nan

# If market prices an ATM call at $10.50
market_price = 10.50
iv = implied_vol(market_price, 100, 100, 0.05, 1.0)
print(f"Implied Volatility: {iv*100:.2f}%")

Output:

Implied Volatility: 20.15%

If you plot implied volatility across different strikes, you get the volatility smile – a curve that shows OTM puts and calls have higher implied vol than ATM options. This is the single biggest evidence that Black-Scholes is wrong.

Where Black-Scholes Fails

1. The Volatility Smile

If Black-Scholes were correct, implied volatility would be constant across all strikes. It isn’t. OTM puts (crash protection) trade at higher implied vol – the skew. This is why quants use local vol, stochastic vol (Heston), or jump-diffusion models.

2. Fat Tails

Real returns have kurtosis > 3 (leptokurtic). The 1987 crash, the 2008 crisis, and the 2020 COVID crash were all “impossible” under log-normal assumptions. Extreme Value Theory and jump models address this.

3. Path Dependence

Black-Scholes prices European options (payoff depends only on $S_T$). Asian options, barrier options, and lookback options require Monte Carlo or PDE methods.

Interview-Ready Summary

Concept Formula/Key Point Interview Question
BS Call $S N(d_1) – Ke^{-rT}N(d_2)$ “Derive BS from risk-neutral pricing”
Delta $N(d_1)$ “What’s the delta of an ATM call?” (~0.5 + drift)
Gamma $N'(d_1) / (Ssigmasqrt{T})$ “Why is gamma highest ATM?”
Vega $Ssqrt{T} N'(d_1)$ “Why does vega increase with T?”
Put-Call Parity $P = C – S + Ke^{-rT}$ “Does put-call parity hold for American options?”
Implied Vol $sigma_{impl}$ s.t. $C_{BS}(sigma_{impl}) = C_{market}$ “What’s the vol smile tell us?”
Key Limitation Constant vol assumption “Why do traders use Heston instead of BS?”

Further Reading

  • Hull, J. Options, Futures, and Other Derivatives – Chapter 13-15
  • Wilmott, P. Paul Wilmott on Quantitative Finance – The practitioner’s perspective
  • Shreve, S. Stochastic Calculus for Finance II – The mathematical foundation

This post originally appeared on Desk2Quant. For deeper coverage with interactive notebooks and 1000+ interview problems, see The Stochastic Calculus Visual Lab and Quant Interview Problem Book.

Leave a Reply