σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 4 · Fixed income & options pricing›Topic 4.8·~55 minIntermediate

The Black–Scholes formula

Skill positionModule 4 of 11 · Topic 8 of 16
4.8 The Black–Scholes formula
—Lab score— / 100Total XP—Streak—

A formula for an option

In 1973 Black and Scholes showed that, if a stock follows the lognormal model with constant volatility, an option can be replicated by trading the stock and a bond, so its price does not depend on how likely investors think a rise is. The price of a European call is S * N(d1) - K * exp(-rT) * N(d2): the stock you receive if the option ends up in the money, weighted by a probability-like number, minus the discounted strike you pay, weighted by the risk-neutral probability N(d2) of exercising. The put follows from parity.

d1 = (ln(S/K) + (r + sigma^2/2) T) / (sigma sqrt(T)) d2 = d1 - sigma sqrt(T) C = S N(d1) - K e^(-rT) N(d2)
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))      # N(x): the probability that a standard normal is below x

print(round(norm_cdf(0.0), 4), round(norm_cdf(1.96), 4))      # 0.5 0.975

def d1_d2(spot, strike, rate, sigma, t):
    d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    return d1, d1 - sigma * math.sqrt(t)

d1, d2 = d1_d2(100, 100, 0.05, 0.2, 1)
print(round(d1, 4), round(d2, 4))                               # 0.35 0.15

The call and the put

N is the standard normal distribution function, which Python's math.erf gives as 0.5 * (1 + erf(x / sqrt(2))). d1 and d2 combine the moneyness ln(S/K), the drift and the volatility over the option's life. With them: call = S * N(d1) - K * exp(-rT) * N(d2) and put = K * exp(-rT) * N(-d2) - S * N(-d1). For spot 100, strike 100, 5% rate, 20% volatility and one year, the call costs 10.45 and the put 5.57, and the difference 4.88 is exactly the parity value S - K * exp(-rT).

def bs_call(spot, strike, rate, sigma, t):
    d1, d2 = d1_d2(spot, strike, rate, sigma, t)
    return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d2)

def bs_put(spot, strike, rate, sigma, t):
    d1, d2 = d1_d2(spot, strike, rate, sigma, t)
    return strike * math.exp(-rate * t) * norm_cdf(-d2) - spot * norm_cdf(-d1)

call, put = bs_call(100, 100, 0.05, 0.2, 1), bs_put(100, 100, 0.05, 0.2, 1)
print(round(call, 4), round(put, 4))                            # 10.4506 5.5735
print(round(call - put, 4), round(100 - 100 * math.exp(-0.05), 4))   # 4.8771 4.8771   put-call parity holds

What moves the price

Five inputs drive the price. A higher strike makes the call cheaper (80: 24.59, 100: 10.45, 120: 3.25). A higher volatility makes both options dearer, since more uncertainty only adds upside for the holder (10%: 6.81, 20%: 10.45, 40%: 18.02). A longer time to expiry also raises the price (3 months: 4.62, 1 year: 10.45, 4 years: 25.21). Notice that the expected return of the stock does not appear in the formula at all: only volatility has to be estimated, which is why quoted option prices are often expressed as a volatility.

for strike in (80, 100, 120):
    print("strike", strike, round(bs_call(100, strike, 0.05, 0.2, 1), 4))      # 24.5888, 10.4506, 3.2475
for sigma in (0.1, 0.2, 0.4):
    print("sigma", sigma, round(bs_call(100, 100, 0.05, sigma, 1), 4))         # 6.805, 10.4506, 18.023
for t in (0.25, 1, 4):
    print("t", t, round(bs_call(100, 100, 0.05, 0.2, t), 4))                   # 4.615, 10.4506, 25.2133
# Time value: the call is worth more than its intrinsic value (what it would pay if it expired today)
intrinsic = max(100 - 100 * math.exp(-0.05), 0)
print(round(intrinsic, 4), round(bs_call(100, 100, 0.05, 0.2, 1) - intrinsic, 4))     # 4.8771 5.5735   intrinsic value and time value
A model, with assumptionsBlack–Scholes assumes constant volatility, lognormal prices without jumps, no dividends, European exercise and frictionless trading. Real markets show volatility that changes with the strike and the date (the volatility smile), jumps and fat tails. The formula is still the common language of the market, which is why prices are quoted as implied volatility, the subject of a later topic.
  • N(x) = 0.5 * (1 + erf(x / sqrt(2))), and d1 = (ln(S/K) + (r + sigma²/2) T) / (sigma sqrt(T)), d2 = d1 - sigma sqrt(T).
  • Call = S N(d1) - K e^(-rT) N(d2); put = K e^(-rT) N(-d2) - S N(-d1); their difference is the parity value S - K e^(-rT).
  • Prices rise with volatility and time and fall with the call's strike; the stock's expected return is absent, only volatility is needed.

Next up: delta, how much the option price moves when the stock moves by one dollar.

Lab · what's graded

  • Build the bell curve's cumulative probability with norm_cdf25 pts
  • Compute the two Black–Scholes quantities with d1_d225 pts
  • Price a call with bs_call25 pts
  • Price a put with bs_put25 pts
Explorer · lab-73-1
▾ lab-73-1
…
bash · in-browser
learner@quantcraft:~/lab-73-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-73-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub