19  Chapter 16. Black–Scholes Formula

19.1 Learning Goals

By the end of this chapter, students should be able to:

  • state the Black–Scholes formulas for European calls and puts;
  • derive the call formula from risk-neutral lognormal integration;
  • explain the probabilistic meaning of \(d_1\) and \(d_2\);
  • derive put-call parity and use it as a formula check;
  • compute and interpret the main Greeks: delta, gamma, theta, vega, and rho;
  • distinguish option price, intrinsic value, time value, and discounted strike;
  • implement the formula carefully in Python;
  • invert the formula numerically to obtain implied volatility;
  • understand why implied volatility is a model-based quote rather than directly observed volatility;
  • use AI tools to audit formulas, test code, and explain parameter sensitivities.

The Black–Scholes formula is the closed-form solution of the Black–Scholes pricing problem for European call and put options. Under the risk-neutral measure \(\mathbb Q\), the stock follows

\[ dS_t=rS_t\,dt+\sigma S_t\,dW_t^{\mathbb Q}. \]

For a European call with strike \(K\) and maturity \(T\), the time-zero price is

\[ C_0=S_0N(d_1)-Ke^{-rT}N(d_2), \]

where

\[ d_1=\frac{\log(S_0/K)+(r+\frac12\sigma^2)T}{\sigma\sqrt T}, \qquad d_2=d_1-\sigma\sqrt T. \]

The formula is not just a computational recipe. It is a compact expression of no-arbitrage, martingale pricing, lognormal distributions, and dynamic replication.

19.2 16.1 From PDE to Formula

Chapter 15 derived the Black–Scholes partial differential equation

\[ V_t+\frac12\sigma^2S^2V_{SS}+rSV_S-rV=0. \]

For a European derivative with payoff \(h(S_T)\), the terminal condition is

\[ V(T,S)=h(S). \]

The Black–Scholes formula gives an explicit solution for two especially important payoff functions:

\[ h_{\text{call}}(S_T)=(S_T-K)^+, \]

and

\[ h_{\text{put}}(S_T)=(K-S_T)^+. \]

Here \(x^+=\max(x,0)\).

There are two standard ways to obtain the formula:

  1. solve the Black–Scholes PDE by transforming it into the heat equation;
  2. use risk-neutral valuation and integrate the lognormal distribution.

In this chapter we focus mainly on the second approach because it is cleaner for students who already understand probability and conditional expectation.

NoteCentral pricing principle

Under the Black–Scholes assumptions, the time-zero price of a European payoff \(h(S_T)\) is

\[ V_0=e^{-rT}\mathbb E^{\mathbb Q}[h(S_T)]. \]

For a call option, \(h(S_T)=(S_T-K)^+\). Therefore

\[ C_0=e^{-rT}\mathbb E^{\mathbb Q}[(S_T-K)^+]. \]

19.3 16.2 Risk-Neutral Stock Dynamics

Under the physical measure \(\mathbb P\), the stock may have drift \(\mu\):

\[ dS_t=\mu S_t\,dt+\sigma S_t\,dW_t^{\mathbb P}. \]

Under the risk-neutral measure \(\mathbb Q\), the drift becomes the risk-free rate \(r\):

\[ dS_t=rS_t\,dt+\sigma S_t\,dW_t^{\mathbb Q}. \]

Therefore the explicit solution is

\[ S_T=S_0\exp\left[\left(r-\frac12\sigma^2\right)T+\sigma\sqrt{T}Z\right], \qquad Z\sim N(0,1) \text{ under } \mathbb Q. \]

Taking logarithms gives

\[ \log S_T \sim N\left(\log S_0+\left(r-\frac12\sigma^2\right)T,\,\sigma^2T\right). \]

This lognormal distribution is the source of the closed-form expression.

19.3.1 Definition 16.1: European call and put

A European call option with strike \(K\) and maturity \(T\) pays

\[ (S_T-K)^+. \]

A European put option with strike \(K\) and maturity \(T\) pays

\[ (K-S_T)^+. \]

The word European means that the option can be exercised only at maturity. This matters because early exercise is an optimal stopping problem, not a simple terminal-expectation problem.

19.4 16.3 Deriving the Call Formula

The risk-neutral price of a call is

\[ C_0=e^{-rT}\mathbb E^{\mathbb Q}[(S_T-K)^+]. \]

Since the payoff is positive exactly when \(S_T>K\),

\[ (S_T-K)^+=S_T1_{\{S_T>K\}}-K1_{\{S_T>K\}}. \]

Thus

\[ C_0=e^{-rT}\mathbb E^{\mathbb Q}\left[S_T1_{\{S_T>K\}}\right] -Ke^{-rT}\mathbb Q(S_T>K). \]

The second term is simpler. Since

\[ \log S_T=\log S_0+\left(r-\frac12\sigma^2\right)T+\sigma\sqrt{T}Z, \]

the event \(S_T>K\) is equivalent to

\[ Z>\frac{\log(K/S_0)-(r-\frac12\sigma^2)T}{\sigma\sqrt T}. \]

Define

\[ d_2=\frac{\log(S_0/K)+(r-\frac12\sigma^2)T}{\sigma\sqrt T}. \]

Then

\[ \mathbb Q(S_T>K)=N(d_2). \]

The first term requires a tilted normal integral. Using the identity

\[ \mathbb E[e^{aZ}1_{\{Z>b\}}]=e^{a^2/2}N(a-b), \qquad Z\sim N(0,1), \]

with \(a=\sigma\sqrt T\), one obtains

\[ e^{-rT}\mathbb E^{\mathbb Q}\left[S_T1_{\{S_T>K\}}\right]=S_0N(d_1), \]

where

\[ d_1=d_2+\sigma\sqrt T =\frac{\log(S_0/K)+(r+\frac12\sigma^2)T}{\sigma\sqrt T}. \]

Therefore

\[ C_0=S_0N(d_1)-Ke^{-rT}N(d_2). \]

19.4.1 Theorem 16.1: Black–Scholes European call formula

Assume the Black–Scholes model with constant \(r\) and \(\sigma>0\). The time-zero price of a European call option with strike \(K\) and maturity \(T\) is

\[ C_0=S_0N(d_1)-Ke^{-rT}N(d_2), \]

where

\[ d_1=\frac{\log(S_0/K)+(r+\frac12\sigma^2)T}{\sigma\sqrt T}, \qquad d_2=d_1-\sigma\sqrt T. \]

Remark (Interpretation of \(N(d_2)\) and \(N(d_1)\)). The quantity \(N(d_2)\) is the risk-neutral probability that the call finishes in the money:

\[ N(d_2)=\mathbb Q(S_T>K). \]

The quantity \(N(d_1)\) is not exactly the same probability under \(\mathbb Q\). It is a probability under a tilted measure that weights states by the stock price. Financially, \(N(d_1)\) is also the call delta in the Black–Scholes model.

19.5 16.4 Deriving the Put Formula

A European put has payoff

\[ (K-S_T)^+. \]

The direct risk-neutral expectation calculation gives

\[ P_0=Ke^{-rT}N(-d_2)-S_0N(-d_1). \]

This can also be obtained from put-call parity, which is often the simplest derivation.

19.5.1 Theorem 16.2: Black–Scholes European put formula

Under the same assumptions, the time-zero price of a European put option with strike \(K\) and maturity \(T\) is

\[ P_0=Ke^{-rT}N(-d_2)-S_0N(-d_1). \]

19.6 16.5 Put-Call Parity

For every \(S_T\), the terminal payoffs satisfy

\[ (S_T-K)^+-(K-S_T)^+=S_T-K. \]

Therefore a long call and short put have the same terminal payoff as holding one share of stock and borrowing \(Ke^{-rT}\) today to repay \(K\) at maturity. By no-arbitrage,

\[ C_0-P_0=S_0-Ke^{-rT}. \]

19.6.1 Theorem 16.3: Put-call parity

For European options on a non-dividend-paying stock,

\[ C_0-P_0=S_0-Ke^{-rT}. \]

Equivalently,

\[ P_0=C_0-S_0+Ke^{-rT}. \]

19.6.2 Example 16.1: Numerical Black–Scholes price

Let

\[ S_0=100,\qquad K=105,\qquad r=0.04,\qquad \sigma=0.25,\qquad T=1. \]

Then

\[ d_1=\frac{\log(100/105)+(0.04+0.5(0.25)^2)}{0.25}, \]

and

\[ d_2=d_1-0.25. \]

The call price is

\[ C_0=100N(d_1)-105e^{-0.04}N(d_2). \]

The put price can be computed either from its own formula or from put-call parity.

import math


def norm_cdf(x):
    return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))


def black_scholes_call_put(S, K, r, sigma, T):
    if S <= 0 or K <= 0 or sigma <= 0 or T <= 0:
        raise ValueError("S, K, sigma, and T must be positive.")
    d1 = (math.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
    d2 = d1 - sigma * math.sqrt(T)
    call = S * norm_cdf(d1) - K * math.exp(-r * T) * norm_cdf(d2)
    put = K * math.exp(-r * T) * norm_cdf(-d2) - S * norm_cdf(-d1)
    return call, put, d1, d2

S, K, r, sigma, T = 100.0, 105.0, 0.04, 0.25, 1.0
call, put, d1, d2 = black_scholes_call_put(S, K, r, sigma, T)

print(f"d1 = {d1:.6f}")
print(f"d2 = {d2:.6f}")
print(f"call = {call:.6f}")
print(f"put  = {put:.6f}")
print(f"parity check C - P = {call - put:.6f}")
print(f"S - K exp(-rT)    = {S - K * math.exp(-r * T):.6f}")
d1 = 0.089839
d2 = -0.160161
call = 9.556256
put  = 10.439147
parity check C - P = -0.882891
S - K exp(-rT)    = -0.882891

19.7 16.6 Intrinsic Value and Time Value

The intrinsic value of a call is

\[ (S_0-K)^+. \]

The intrinsic value of a put is

\[ (K-S_0)^+. \]

The option price is usually larger than intrinsic value because the option holder benefits from favorable future movement while having limited downside. The difference is called time value:

\[ \text{time value}=\text{option price}-\text{intrinsic value}. \]

Time value is strongly affected by volatility and maturity. Higher volatility increases both call and put prices, because convex payoffs benefit from dispersion.

Remark (Convexity intuition). The call payoff \(h(S)=(S-K)^+\) is convex. For a convex payoff, uncertainty can increase expected payoff. This is one reason why option values increase with volatility in the Black–Scholes model.

19.8 16.7 Interactive: Call and Put Prices

The following interactive example shows how European call and put prices change with the stock price. The dashed curves show intrinsic values. Move the controls to see how maturity, volatility, strike, and interest rate affect time value.

19.8.1 Interactive 16.1: Black–Scholes price curves

100

0.25

1.00

0.04

19.9 16.8 Greeks: Local Sensitivities

The Greeks are derivatives of the option price with respect to model inputs. They are not new pricing formulas; they are local sensitivity measures.

For a European call,

\[ C=S_0N(d_1)-Ke^{-rT}N(d_2). \]

The main Greeks are:

\[ \Delta_C=\frac{\partial C}{\partial S_0}=N(d_1), \]

\[ \Gamma_C=\frac{\partial^2 C}{\partial S_0^2} =\frac{\varphi(d_1)}{S_0\sigma\sqrt T}, \]

\[ \text{Vega}_C=\frac{\partial C}{\partial \sigma} =S_0\varphi(d_1)\sqrt T, \]

\[ \Theta_C=\frac{\partial C}{\partial T_{\text{calendar}}} =-\frac{S_0\varphi(d_1)\sigma}{2\sqrt T}-rKe^{-rT}N(d_2), \]

and

\[ \rho_C=\frac{\partial C}{\partial r}=KTe^{-rT}N(d_2). \]

Here

\[ \varphi(x)=\frac{1}{\sqrt{2\pi}}e^{-x^2/2} \]

is the standard normal density.

19.9.1 Sign convention for theta

There are two common conventions. In this book, \(T\) denotes time to maturity. Many trading platforms report theta as sensitivity to the passage of calendar time, so it is usually the negative of \(\partial C/\partial T\) when \(T\) is time to maturity.

For a European put,

\[ \Delta_P=N(d_1)-1, \]

\[ \Gamma_P=\Gamma_C, \]

\[ \text{Vega}_P=\text{Vega}_C, \]

\[ \Theta_P=-\frac{S_0\varphi(d_1)\sigma}{2\sqrt T}+rKe^{-rT}N(-d_2), \]

and

\[ \rho_P=-KTe^{-rT}N(-d_2). \]

Remark (Why calls and puts have the same gamma and vega). Put-call parity gives

\[ C-P=S_0-Ke^{-rT}. \]

The right-hand side is linear in \(S_0\) and independent of \(\sigma\). Differentiating twice with respect to \(S_0\) gives

\[ \Gamma_C-\Gamma_P=0. \]

Differentiating with respect to \(\sigma\) gives

\[ \text{Vega}_C-\text{Vega}_P=0. \]

import math
import numpy as np
import pandas as pd


def n_pdf(x):
    return math.exp(-0.5 * x * x) / math.sqrt(2.0 * math.pi)


def N(x):
    return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))


def bs_price_greeks(S, K, r, sigma, T, option="call"):
    d1 = (math.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
    d2 = d1 - sigma * math.sqrt(T)
    discK = K * math.exp(-r * T)
    if option == "call":
        price = S * N(d1) - discK * N(d2)
        delta = N(d1)
        theta = -S * n_pdf(d1) * sigma / (2 * math.sqrt(T)) - r * discK * N(d2)
        rho = K * T * math.exp(-r * T) * N(d2)
    elif option == "put":
        price = discK * N(-d2) - S * N(-d1)
        delta = N(d1) - 1
        theta = -S * n_pdf(d1) * sigma / (2 * math.sqrt(T)) + r * discK * N(-d2)
        rho = -K * T * math.exp(-r * T) * N(-d2)
    else:
        raise ValueError("option must be 'call' or 'put'")
    gamma = n_pdf(d1) / (S * sigma * math.sqrt(T))
    vega = S * n_pdf(d1) * math.sqrt(T)
    return {
        "price": price,
        "delta": delta,
        "gamma": gamma,
        "vega": vega,
        "theta": theta,
        "rho": rho,
        "d1": d1,
        "d2": d2,
    }

rows = []
for option in ["call", "put"]:
    out = bs_price_greeks(100, 100, 0.04, 0.25, 1.0, option=option)
    rows.append({"option": option, **out})

pd.DataFrame(rows).round(6)
option price delta gamma vega theta rho d1 d2
0 call 11.837046 0.612178 0.015323 38.306483 -6.763540 49.380746 0.285 0.035
1 put 7.915990 -0.387822 0.015323 38.306483 -2.920382 -46.698197 0.285 0.035

Black–Scholes price and Greeks implemented from scratch.

19.10 16.9 Interactive: Greeks Across Stock Price

Gamma and vega are largest near the strike when maturity is not too long. Delta changes most rapidly near the strike, and that rapid change is exactly what gamma measures.

19.10.1 Interactive 16.2: Greeks as functions of \(S\)

100

0.25

1.00

0.04

19.11 16.10 Finite-Difference Checks for Greeks

The formulas for Greeks should be checked numerically. For example, delta can be approximated by a central difference:

\[ \Delta \approx \frac{C(S_0+h)-C(S_0-h)}{2h}. \]

Gamma can be approximated by

\[ \Gamma \approx \frac{C(S_0+h)-2C(S_0)+C(S_0-h)}{h^2}. \]

These checks are useful because sign errors in Greeks are common.

S, K, r, sigma, T = 100.0, 100.0, 0.04, 0.25, 1.0
h = 0.01

base = bs_price_greeks(S, K, r, sigma, T, option="call")
C_plus = bs_price_greeks(S + h, K, r, sigma, T, option="call")["price"]
C_0 = base["price"]
C_minus = bs_price_greeks(S - h, K, r, sigma, T, option="call")["price"]

fd_delta = (C_plus - C_minus) / (2 * h)
fd_gamma = (C_plus - 2 * C_0 + C_minus) / (h**2)

print(f"analytic delta = {base['delta']:.8f}")
print(f"FD delta       = {fd_delta:.8f}")
print(f"analytic gamma = {base['gamma']:.8f}")
print(f"FD gamma       = {fd_gamma:.8f}")
analytic delta = 0.61217793
FD delta       = 0.61217792
analytic gamma = 0.01532259
FD gamma       = 0.01532259

19.12 16.11 Monte Carlo Check of the Formula

The call price can be estimated by simulating risk-neutral terminal stock prices:

\[ S_T=S_0\exp\left[\left(r-\frac12\sigma^2\right)T+\sigma\sqrt T Z\right]. \]

Then

\[ C_0\approx e^{-rT}\frac1M\sum_{m=1}^M(S_T^{(m)}-K)^+. \]

This is not the most efficient way to price a plain European call, because the closed-form formula is available. However, Monte Carlo is important because it extends to many path-dependent and high-dimensional problems where no closed form is available.

import numpy as np

rng = np.random.default_rng(7243)
S, K, r, sigma, T = 100.0, 100.0, 0.04, 0.25, 1.0
M = 200_000
Z = rng.standard_normal(M)
ST = S * np.exp((r - 0.5 * sigma**2) * T + sigma * np.sqrt(T) * Z)
payoff = np.maximum(ST - K, 0.0)
mc_price = np.exp(-r * T) * payoff.mean()
mc_se = np.exp(-r * T) * payoff.std(ddof=1) / np.sqrt(M)
formula_price = bs_price_greeks(S, K, r, sigma, T, option="call")["price"]

print(f"Black--Scholes formula = {formula_price:.6f}")
print(f"Monte Carlo estimate   = {mc_price:.6f}")
print(f"Monte Carlo s.e.       = {mc_se:.6f}")
print(f"z-score of error       = {(mc_price - formula_price) / mc_se:.3f}")
Black--Scholes formula = 11.837046
Monte Carlo estimate   = 11.914679
Monte Carlo s.e.       = 0.040881
z-score of error       = 1.899

19.13 16.12 Implied Volatility

The Black–Scholes formula gives the option price when \(\sigma\) is known. In the market, option prices are observed and volatility is often inferred from those prices.

19.13.1 Definition 16.2: Implied volatility

Given a market call price \(C^{\text{mkt}}\), the Black–Scholes implied volatility is a value \(\sigma_{\text{imp}}>0\) such that

\[ C^{\text{mkt}}=C^{\text{BS}}(S_0,K,r,T,\sigma_{\text{imp}}). \]

For a put, it is defined similarly using the put formula.

Implied volatility is a model-based way of quoting option prices. It is not the same as historical volatility and is not directly observed. It is the volatility input that makes the Black–Scholes model reproduce an observed option price.

For a non-dividend-paying European call, the Black–Scholes call price is increasing in \(\sigma\) because vega is positive:

\[ \frac{\partial C}{\partial \sigma}=S_0\varphi(d_1)\sqrt T>0. \]

Therefore, under valid no-arbitrage bounds, implied volatility is unique.

The no-arbitrage bounds for a European call are

\[ (S_0-Ke^{-rT})^+\le C_0\le S_0. \]

For a put they are

\[ (Ke^{-rT}-S_0)^+\le P_0\le Ke^{-rT}. \]

def implied_vol_call(C_market, S, K, r, T, tol=1e-10, max_iter=200):
    lower_bound = max(S - K * math.exp(-r * T), 0.0)
    upper_bound = S
    if not (lower_bound <= C_market <= upper_bound):
        raise ValueError("Market price violates no-arbitrage call bounds.")

    lo, hi = 1e-8, 5.0
    for _ in range(max_iter):
        mid = 0.5 * (lo + hi)
        C_mid = bs_price_greeks(S, K, r, mid, T, option="call")["price"]
        if abs(C_mid - C_market) < tol:
            return mid
        if C_mid < C_market:
            lo = mid
        else:
            hi = mid
    return 0.5 * (lo + hi)

S, K, r, T = 100.0, 100.0, 0.04, 1.0
true_sigma = 0.32
market_price = bs_price_greeks(S, K, r, true_sigma, T, option="call")["price"]
recovered_sigma = implied_vol_call(market_price, S, K, r, T)

print(f"market price generated from sigma = {true_sigma:.4f}")
print(f"market call price                 = {market_price:.6f}")
print(f"recovered implied volatility       = {recovered_sigma:.6f}")
market price generated from sigma = 0.3200
market call price                 = 14.519595
recovered implied volatility       = 0.320000

19.14 16.13 Volatility Smile and Model Critique

If the Black–Scholes model were exactly correct with constant volatility, all options on the same underlying and maturity would have the same implied volatility. In real markets, implied volatility often depends on strike and maturity. The graph of implied volatility against strike is called a volatility smile or skew.

A stylized smile can be created by assigning different prices to different strikes and then inverting the Black–Scholes formula. This does not prove that the market follows a different model, but it shows that the constant-volatility Black–Scholes model cannot fit all option prices with one value of \(\sigma\).

import matplotlib.pyplot as plt

S, r, T = 100.0, 0.03, 0.75
strikes = np.arange(70.0, 131.0, 5.0)
# A stylized market volatility curve used only for demonstration.
market_sigmas = 0.20 + 0.45 * (np.log(strikes / S))**2 + 0.08 * np.maximum(np.log(strikes / S), 0)
market_prices = [bs_price_greeks(S, K, r, sig, T, option="call")["price"]
                 for K, sig in zip(strikes, market_sigmas)]
implied = [implied_vol_call(C, S, K, r, T) for C, K in zip(market_prices, strikes)]

plt.figure(figsize=(7, 4))
plt.plot(strikes, implied, marker="o")
plt.xlabel("strike K")
plt.ylabel("implied volatility")
plt.title("Stylized implied volatility curve")
plt.grid(True, alpha=0.3)
plt.show()

A stylized implied-volatility smile.

19.14.1 AI-assisted model critique

Ask an AI assistant:

In the Black–Scholes model, volatility is constant. In market data, implied volatility often varies by strike and maturity. Explain why this creates a model-risk issue. Do not recommend a trading strategy. Focus on the mathematical inconsistency between a one-parameter volatility model and a surface of quoted implied volatilities.

Then check whether the response correctly distinguishes:

  1. historical volatility;
  2. model volatility;
  3. implied volatility;
  4. local or stochastic volatility as possible model extensions.

19.15 16.14 Dividends and Foreign Exchange Variant

If the stock pays a continuous dividend yield \(q\), the risk-neutral stock dynamics become

\[ dS_t=(r-q)S_t\,dt+\sigma S_t\,dW_t^{\mathbb Q}. \]

The European call and put formulas become

\[ C_0=S_0e^{-qT}N(d_1)-Ke^{-rT}N(d_2), \]

and

\[ P_0=Ke^{-rT}N(-d_2)-S_0e^{-qT}N(-d_1), \]

where

\[ d_1=\frac{\log(S_0/K)+(r-q+\frac12\sigma^2)T}{\sigma\sqrt T}, \qquad d_2=d_1-\sigma\sqrt T. \]

Put-call parity becomes

\[ C_0-P_0=S_0e^{-qT}-Ke^{-rT}. \]

This same structure is used for foreign exchange options by interpreting \(q\) as the foreign risk-free rate.

19.16 16.15 Numerical Stability and Edge Cases

A good implementation should handle limiting cases carefully.

As \(T\to 0\), the option value approaches intrinsic value:

\[ C_0\to (S_0-K)^+, \qquad P_0\to (K-S_0)^+. \]

As \(\sigma\to 0\), the risk-neutral terminal stock becomes deterministic:

\[ S_T=S_0e^{rT}. \]

Hence

\[ C_0\to e^{-rT}(S_0e^{rT}-K)^+=(S_0-Ke^{-rT})^+. \]

A robust implementation should avoid division by zero when \(T\) or \(\sigma\) is extremely small.

def robust_bs_price(S, K, r, sigma, T, option="call"):
    if S <= 0 or K <= 0:
        raise ValueError("S and K must be positive.")
    if T <= 1e-12:
        return max(S - K, 0.0) if option == "call" else max(K - S, 0.0)
    if sigma <= 1e-12:
        forward = S * math.exp(r * T)
        payoff = max(forward - K, 0.0) if option == "call" else max(K - forward, 0.0)
        return math.exp(-r * T) * payoff
    return bs_price_greeks(S, K, r, sigma, T, option=option)["price"]

for T_small, sigma_small in [(1e-14, 0.25), (1.0, 1e-14), (1.0, 0.25)]:
    print(T_small, sigma_small, robust_bs_price(100, 100, 0.04, sigma_small, T_small, "call"))
1e-14 0.25 0
1.0 1e-14 3.92105608476768
1.0 0.25 11.837046440824082

19.17 16.16 Formula Verification by PDE Residual

The call formula should solve the Black–Scholes PDE:

\[ C_t+\frac12\sigma^2S^2C_{SS}+rSC_S-rC=0. \]

When using time to maturity \(\tau=T-t\), write the price as \(C(S,\tau)\). Since \(C_t=-C_\tau\), the PDE becomes

\[ -C_\tau+\frac12\sigma^2S^2C_{SS}+rSC_S-rC=0. \]

This is a good way to check signs. In particular, students often accidentally use \(+C_\tau\) in the PDE when the variable is time to maturity.

def call_price_tau(S, K, r, sigma, tau):
    return robust_bs_price(S, K, r, sigma, tau, option="call")

S, K, r, sigma, tau = 110.0, 100.0, 0.04, 0.25, 0.8
hS = 0.05
ht = 1e-4

C = call_price_tau(S, K, r, sigma, tau)
C_plus_S = call_price_tau(S + hS, K, r, sigma, tau)
C_minus_S = call_price_tau(S - hS, K, r, sigma, tau)
C_plus_tau = call_price_tau(S, K, r, sigma, tau + ht)
C_minus_tau = call_price_tau(S, K, r, sigma, tau - ht)

C_S = (C_plus_S - C_minus_S) / (2 * hS)
C_SS = (C_plus_S - 2 * C + C_minus_S) / (hS**2)
C_tau = (C_plus_tau - C_minus_tau) / (2 * ht)

residual = -C_tau + 0.5 * sigma**2 * S**2 * C_SS + r * S * C_S - r * C
print(f"PDE residual = {residual:.6e}")
PDE residual = -8.379089e-07

19.18 16.17 Common Mistakes

  1. Using \(\mu\) in the Black–Scholes formula. The formula uses \(r\), not the physical drift \(\mu\).
  2. Forgetting the discount factor. The strike term is \(Ke^{-rT}\), not \(K\).
  3. Confusing \(d_1\) and \(d_2\). The stock term uses \(N(d_1)\); the strike term uses \(N(d_2)\).
  4. Treating \(N(d_1)\) as the risk-neutral exercise probability. The risk-neutral exercise probability is \(N(d_2)\).
  5. Confusing vega units. The mathematical derivative is per unit volatility. Market convention often reports vega per one volatility point, which is one percent.
  6. Ignoring no-arbitrage bounds before computing implied volatility. Invalid prices may not have a valid implied volatility.
  7. Using time-to-maturity signs incorrectly in theta or PDE checks.

19.18.1 AI-assisted formula audit

Ask an AI assistant to check the following intentionally suspicious claim:

The Black–Scholes call price is \(C=S_0N(d_2)-Ke^{-rT}N(d_1)\), and \(N(d_1)\) is the risk-neutral probability that the option expires in the money.

Your task is to identify every error. A correct critique should say:

  • the stock term uses \(N(d_1)\);
  • the strike term uses \(N(d_2)\);
  • \(N(d_2)\) is the risk-neutral in-the-money probability;
  • \(N(d_1)\) has a different tilted-measure interpretation and equals call delta.

19.18.2 AI-assisted code review

Paste your Black–Scholes implementation into an AI assistant and ask it to design tests. The tests should include:

  1. put-call parity;
  2. monotonicity in \(S\);
  3. monotonicity in \(\sigma\);
  4. limiting behavior as \(T\to 0\);
  5. finite-difference checks of delta and gamma;
  6. no-arbitrage bounds for calls and puts.

Do not ask the AI to simply rewrite the code. Ask it to find mathematical tests that would reveal implementation errors.

19.19 16.18 Exercises

19.19.1 Conceptual exercises

  1. Explain why the Black–Scholes formula uses \(r\) instead of the physical drift \(\mu\).
  2. Explain why call and put prices increase with volatility under the Black–Scholes model.
  3. Explain why \(N(d_2)\) is a probability but \(N(d_1)\) should not be interpreted as the same probability.
  4. What happens to the call price when \(T\to 0\)? What happens when \(\sigma\to 0\)?
  5. Explain why implied volatility is not the same object as historical realized volatility.

19.19.2 Mathematical exercises

  1. Starting from

\[ C_0=e^{-rT}\mathbb E^{\mathbb Q}[(S_T-K)^+], \]

derive the Black–Scholes call formula by splitting the payoff into

\[ S_T1_{\{S_T>K\}}-K1_{\{S_T>K\}}. \]

  1. Prove put-call parity by comparing terminal payoffs.
  2. Differentiate the call formula with respect to \(S_0\) and show that \(\Delta_C=N(d_1)\).
  3. Use put-call parity to derive the put delta from the call delta.
  4. Show that call and put gamma are equal.
  5. Show that call and put vega are equal.
  6. Derive the no-arbitrage bounds

\[ (S_0-Ke^{-rT})^+\le C_0\le S_0. \]

19.19.3 Computational exercises

  1. Implement the Black–Scholes call and put formulas without using scipy.stats.
  2. Verify put-call parity numerically for at least five different parameter choices.
  3. Plot call price as a function of \(S_0\) for several values of \(\sigma\).
  4. Plot delta, gamma, and vega as functions of \(S_0\).
  5. Write a bisection solver for implied volatility and test it on synthetic option prices.
  6. Simulate the risk-neutral terminal stock price and compare Monte Carlo estimates with the formula.
  7. Use finite differences to check delta, gamma, and vega.
  8. Create a stylized implied-volatility smile and explain why one constant volatility cannot fit it.

19.19.4 AI-assisted exercises

  1. Ask an AI assistant to explain the difference between \(d_1\) and \(d_2\). Then rewrite the explanation in your own words and correct any overstatements.
  2. Ask an AI assistant to generate unit tests for a Black–Scholes function. Keep only the tests that have a clear mathematical reason.
  3. Ask an AI assistant to critique the assumptions of Black–Scholes. Separate mathematical assumptions from market microstructure issues.
  4. Ask an AI assistant to explain why vega is positive. Check whether the explanation uses convexity, differentiation, or both.

19.20 16.19 Chapter Summary

The Black–Scholes formula is the closed-form expression for European call and put prices in the constant-volatility complete-market model. It can be derived from the Black–Scholes PDE or from risk-neutral expectation under lognormal stock dynamics.

The call formula is

\[ C_0=S_0N(d_1)-Ke^{-rT}N(d_2), \]

and the put formula is

\[ P_0=Ke^{-rT}N(-d_2)-S_0N(-d_1). \]

The formula is mathematically elegant because it compresses several ideas into one expression: no-arbitrage, discounting, lognormal distributions, martingale pricing, and dynamic hedging. The Greeks measure local sensitivities of the formula, while implied volatility reverses the formula to express market prices in volatility units.

The next chapter studies change of measure and Girsanov’s theorem, which explains more deeply why the physical Brownian motion under \(\mathbb P\) can be transformed into a risk-neutral Brownian motion under \(\mathbb Q\).