The Black–Scholes–Merton (BSM) model turns five observable inputs — spot, strike, time, rate, volatility — into a fair option price and a full risk report (the Greeks). Fifty years on, it remains the language in which options are quoted, hedged, and risk-managed.

1.Summary

BSM assumes the underlying follows Geometric Brownian Motion (see the previous tutorial) and shows that an option's payoff can be replicatedby continuously trading the stock and a bond — so the option's price is the cost of that replication, independent of anyone's market view. The result is a closed-form price for European calls and puts, and analytic Greeks: the sensitivities of that price to spot (, ), volatility (, “vega”), time () and rates ().

2.Intuition

2.1Replication, not prediction

Hold shares against a short call and the portfolio is (momentarily) immune to small price moves. Rebalance continuously and the option is manufactured from stock and cash. No-arbitrage then pins the price — the expected return of the stock drops out entirely. That is the magic: two people who disagree wildly about where the Nasdaq is going must still agree on the option's fair price.

2.2Volatility is the price of uncertainty

Of the five inputs, four are observable. Volatility is not — and the option premium is, in essence, the market's bid on future uncertainty. Higher → wider terminal distribution → both calls and puts gain value (optionality only benefits from dispersion).

2.3Greeks: the risk dashboard

A trader rarely asks “what is the price?” — they ask “what happens to my book if spot drops 1%, vol jumps 2 points, and a day passes?” Delta, Gamma, Vega and Theta answer exactly that, one partial derivative at a time.

GreekSensitivity toTypical use
Delta SpotHedge ratio
Gamma Spot (2nd order)Hedge stability / convexity
Vega VolatilityVol exposure
Theta TimeDaily carry / decay
Rho Interest rateRate exposure

3.Theory & Mechanics

3.1The pricing formula

With spot , strike , maturity , rate and volatility :

Read as the (risk-neutral) probability the option finishes in the money; is the delta of the call.

3.2The Greeks in closed form

python
def bs_price(S, K, T, r, sigma, kind="call"):
    """Black-Scholes price for a European call or put."""
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    if kind == "call":
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)

def bs_greeks(S, K, T, r, sigma, kind="call"):
    """Delta, Gamma, Vega, Theta (per year), Rho for a European option."""
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    sign = 1 if kind == "call" else -1
    return {
        "delta": sign * norm.cdf(sign * d1),
        "gamma": norm.pdf(d1) / (S * sigma * np.sqrt(T)),
        "vega":  S * norm.pdf(d1) * np.sqrt(T),
        "theta": (-S * norm.pdf(d1) * sigma / (2 * np.sqrt(T))
                  - sign * r * K * np.exp(-r * T) * norm.cdf(sign * d2)),
        "rho":   sign * K * T * np.exp(-r * T) * norm.cdf(sign * d2),
    }

3.3Put-call parity — the free sanity check

Independent of any model: . If your implementation violates it, something is wrong.

4.Applied Example — QQQ

4.1Inputs from real data

Spot from the latest QQQ close; volatility proxied by the trailing 1-year realised vol of log returns (in practice you would use the implied vol quoted in the market — that distinction is exactly what the Heston vs Black-Scholes piece explores).

python
TICKER = "QQQ"
START, END = "2018-01-01", "2024-12-31"

def load_prices(ticker, start, end):
    """Adjusted-close prices: yfinance first, Stooq as fallback."""
    try:
        import yfinance as yf
        df = yf.download(ticker, start=start, end=end, auto_adjust=True, progress=False)
        if not df.empty:
            return df["Close"].squeeze().rename(ticker).dropna()
    except Exception as exc:
        print(f"yfinance failed ({exc}); trying Stooq…")
    url = f"https://stooq.com/q/d/l/?s={ticker.lower()}.us&i=d"
    df = pd.read_csv(url, parse_dates=["Date"], index_col="Date")
    return df.loc[start:end, "Close"].rename(ticker).dropna()

px = load_prices(TICKER, START, END)
log_ret = np.log(px / px.shift(1)).dropna()

S0    = float(px.iloc[-1])
SIGMA = float(log_ret.tail(252).std() * np.sqrt(252))   # trailing 1y realised vol
R     = 0.045                                           # short-term risk-free rate
T3M   = 0.25                                            # 3 months
K_ATM = round(S0)                                       # at-the-money strike

call = bs_price(S0, K_ATM, T3M, R, SIGMA, "call")
put  = bs_price(S0, K_ATM, T3M, R, SIGMA, "put")

print(f"{TICKER} spot = {S0:,.2f}   sigma = {SIGMA:.2%}   r = {R:.2%}")
print(f"3M ATM call (K={K_ATM}): {call:6.2f}")
print(f"3M ATM put  (K={K_ATM}): {put:6.2f}")

g = bs_greeks(S0, K_ATM, T3M, R, SIGMA, "call")
print("\n3M ATM call Greeks:")
print(f"  delta {g['delta']:7.3f}   gamma {g['gamma']:8.5f}   vega {g['vega']:7.2f}"
      f"   theta {g['theta']/365:7.3f}/day   rho {g['rho']:6.2f}")

4.2The Greeks across strikes

The same option book looks completely different in-, at- and out-of-the-money. Gamma and Vega peak at the money — that is where hedges churn fastest and vol exposure is largest; Delta rolls from 0 to 1 like a smoothed step function.

python
strikes = np.linspace(0.80 * S0, 1.20 * S0, 200)
G = {k: [] for k in ["delta", "gamma", "vega", "theta"]}
for K in strikes:
    gk = bs_greeks(S0, K, T3M, R, SIGMA, "call")
    for k in G: G[k].append(gk[k])

fig, axes = plt.subplots(2, 2, figsize=(11, 6), sharex=True)
for ax, k in zip(axes.flat, G):
    ax.plot(strikes / S0, G[k], color="steelblue")
    ax.axvline(1.0, color="gray", ls=":", lw=1)
    ax.set_title(k.capitalize()); ax.set_xlabel("Moneyness  K / S")
fig.suptitle(f"{TICKER} 3M call Greeks across strikes  (S={S0:,.0f}, sigma={SIGMA:.0%})")
plt.tight_layout(); plt.show()
Four panels showing Delta, Gamma, Vega and Theta of a 3-month QQQ call as functions of moneyness K/S; Delta falls smoothly from 1 to 0, Gamma and Vega peak at the money, and Theta is most negative at the money
Figure 4.2 · QQQ 3-month call — the Greeks across strikes

4.3Time decay — the option's ticking clock

Repricing the ATM call as maturity shrinks shows Theta at work: decay is slow far from expiry and accelerates sharply in the final weeks — the reason short-dated option selling and 0DTE trading are games of Theta and Gamma.

python
maturities = np.linspace(1e-3, 1.0, 200)
atm_prices  = [bs_price(S0, K_ATM, t, R, SIGMA, "call") for t in maturities]
otm_prices  = [bs_price(S0, 1.05 * S0, t, R, SIGMA, "call") for t in maturities]

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(maturities * 12, atm_prices, label=f"ATM (K={K_ATM})", color="steelblue")
ax.plot(maturities * 12, otm_prices, label="5% OTM", color="darkorange")
ax.set_xlabel("Months to expiry"); ax.set_ylabel("Call value")
ax.set_title(f"{TICKER}: value vs time to expiry — Theta accelerates near zero")
ax.invert_xaxis(); ax.legend()
plt.tight_layout(); plt.show()
Call value against months to expiry for an at-the-money and a 5% out-of-the-money QQQ call; both decay toward zero, with the curve steepening in the final weeks before expiry
Figure 4.3 · QQQ call value vs time to expiry — Theta accelerates near zero

4.4Sanity checks: parity and Monte Carlo

Two independent tests of the implementation:

  • Put-call parity must hold to machine precision.
  • Monte Carlo under GBM (the simulator from the previous tutorial, with drift ) must converge to the closed-form price — Black-Scholes is the GBM expectation in disguise.
python
# 1) put-call parity
lhs, rhs = call - put, S0 - K_ATM * np.exp(-R * T3M)
print(f"parity: C - P = {lhs:.6f}   S - K e^-rT = {rhs:.6f}   diff = {abs(lhs-rhs):.2e}")

# 2) Monte Carlo pricing under risk-neutral GBM
N = 200_000
z = np.random.normal(size=N)
S_T = S0 * np.exp((R - 0.5 * SIGMA**2) * T3M + SIGMA * np.sqrt(T3M) * z)
mc_call = np.exp(-R * T3M) * np.maximum(S_T - K_ATM, 0).mean()
se      = np.exp(-R * T3M) * np.maximum(S_T - K_ATM, 0).std() / np.sqrt(N)
print(f"Monte Carlo call: {mc_call:.3f} ± {2*se:.3f}   closed form: {call:.3f}")

5.Conclusion

5aStrengths

  • Closed form — instant prices and Greeks, no simulation needed
  • Preference-free — the stock's expected return drops out; only volatility matters
  • The market's lingua franca — options are quoted in BSM implied vol even where the model's assumptions fail
  • Analytic Greeks — the entire hedging industry runs on them

5bWeaknesses & Limitations

  • Constant volatility — markets trade a smile/skew, not a flat vol (one number cannot price all strikes)
  • GBM underlying — no jumps, no fat tails; deep OTM puts are systematically underpriced
  • European exercise only — American-style early exercise needs trees or numerical methods
  • Continuous frictionless hedging — real rebalancing is discrete and costs money

5cApplications in Practice

  • Pricing and hedging European options and warrants
  • Implied volatility extraction — the quoting convention for the entire options market
  • Real-time Greek dashboards for market-making and portfolio risk

5dAlternatives & Extensions

  • Stochastic volatility — Heston (see our Quant Insights piece “Heston vs Black-Scholes: fitting the volatility smile”)
  • Jump-diffusion — Merton, for crash risk and short-dated skew
  • Binomial trees — American exercise, dividends, path-dependence
  • Local volatility — Dupire, fitting the entire observed surface