SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 2.6.1 Integration by substitutionConcept · 30 min
      2. 2.6.2 Integration by partsConcept · 30 min
      3. 2.6.3 Improper integrals and the Gaussian integralConcept · 35 min
      4. 2.6.4 Practice set: integration techniquesPractice set · 25 min
      5. 2.6.5 Case study: the normal integrals behind Black–ScholesCase study · 35 min

Level 2, Single-variable calculus 2.6 Integration techniques Lesson 5 of 5

Case studyLesson 2.6.5 in Level 2, Single-variable calculus. About 35 minutes.

Case study: the normal integrals behind Black–Scholes

The future stock price

The Black–Scholes model is derived in the Stochastic Calculus course and used throughout the Derivatives Modelling course. Here we take one claim of it as given: the stock price at expiry TT is

ST=S0 e(r−σ2/2)T+σT zS_T = S_0\,e^{(r - \sigma^2/2)T + \sigma\sqrt{T}\,z}

Here zz is a standard normal number: any real value, weighted by the density φ(z)\varphi(z) of lesson 2.6.3. Each value of zz is one possible future, and φ(z)\varphi(z) says how much weight it gets.

The inputs follow the desk's conventions. S0=100S_0 = 100 is today's price and K=100K = 100 the strike, both in dollars. T=0.5T = 0.5 is in years, and r=0.03r = 0.03 and σ=0.2\sigma = 0.2 are annual decimals, with rr continuously compounded (case study 1.4.6).

Where the option starts to pay

A call pays max⁡(ST−K,0)\max(S_T - K, 0) at expiry: the ramp of lesson 1.1.2. It pays nothing unless ST>KS_T > K. Setting ST=KS_T = K and solving for zz with logs (lesson 1.4.4) gives the corner z∗=−d2z^* = -d_2, where

d2=ln⁡(S0/K)+(r−σ2/2) TσTd1=d2+σT\begin{gathered} d_2 = \frac{\ln(S_0/K) + (r - \sigma^2/2)\,T}{\sigma\sqrt{T}} \\[4pt] d_1 = d_2 + \sigma\sqrt{T} \end{gathered}

These d1d_1 and d2d_2 are the ones in the formula of case study 2.4.6. For our inputs z∗≈−0.035355z^* \approx -0.035355, so the call pays off in every future with zz above that: a little more than half of them.

The price as an integral

The model's price is the payoff averaged over all futures, each weighted by φ(z)\varphi(z), then discounted by e−rTe^{-rT} to today.

Rule: Option price as an integral

c=e−rT∫−∞∞payoff⋅φ(z) dzpayoff=max⁡(ST(z)−K, 0)\begin{gathered} c = e^{-rT}\int_{-\infty}^{\infty} \text{payoff}\cdot\varphi(z)\,dz \\[4pt] \text{payoff} = \max\big(S_T(z) - K,\ 0\big) \end{gathered}

An improper integral over the whole real line (lesson 2.6.3). The payoff is zero for every zz below z∗z^*, so the integral really runs from z∗z^* to ∞\infty.

The payoff for each future zz, and the payoff weighted by φ(z)\varphi(z). The weighted curve's area, 6.4673, discounted by e−rTe^{-rT}, is the price $6.3710. Big payoffs far out barely count, because φ\varphi is tiny there.

Try it

The first line checks the corner: z∗≈−0.035355z^* \approx -0.035355, where STS_T is exactly 100.

Then three prices: the formula, quad over the whole line, and quad split at the corner. The whole-line integral gives 6.37102797 against 6.37102794: quad handles the corner of max⁡\max less precisely. Split at z∗z^*, the integrand is smooth and agrees to 14 digits.

⌘/Ctrl + Enter runs the cell

Where N(d2)N(d_2) and N(d1)N(d_1) come from

Above z∗z^* the payoff is ST−KS_T - K, so the price splits into two integrals over z>−d2z > -d_2: a stock part and a strike part.

c=  e−rT∫−d2∞ST(z) φ(z) dz−Ke−rT∫−d2∞φ(z) dz\begin{aligned} c = \; &e^{-rT}\int_{-d_2}^{\infty} S_T(z)\,\varphi(z)\,dz \\ &- K e^{-rT}\int_{-d_2}^{\infty} \varphi(z)\,dz \end{aligned}

The strike part

∫−d2∞φ(z) dz\int_{-d_2}^{\infty}\varphi(z)\,dz is the area under the bell above −d2-d_2. The bell is symmetric (lesson 2.6.3), so that equals the area below d2d_2, which is N(d2)N(d_2).

So the strike part is Ke−rTN(d2)≈50.644782K e^{-rT}N(d_2) \approx 50.644782. Here N(d2)≈0.514102N(d_2) \approx 0.514102 is the model's chance that the call ends in the money, above the strike.

The stock part, by substitution

  1. 1
    ST(z) φ(z)=S0 e(r−σ2/2)T2π×eσTz−z2/2\begin{aligned} S_T(z)\,\varphi(z) &= \frac{S_0\,e^{(r - \sigma^2/2)T}}{\sqrt{2\pi}} \\ &\quad \times e^{\sigma\sqrt{T}z - z^2/2} \end{aligned}
  2. 2
    σT z−z22=−(z−σT)22+σ2T2\begin{aligned} &\sigma\sqrt{T}\,z - \frac{z^2}{2} \\ &= -\frac{(z - \sigma\sqrt{T})^2}{2} + \frac{\sigma^2 T}{2} \end{aligned}
  3. 3
    ST(z) φ(z)=S0 erT φ(z−σT)\begin{aligned} &S_T(z)\,\varphi(z) \\ &= S_0\,e^{rT}\,\varphi\big(z - \sigma\sqrt{T}\big) \end{aligned}
  4. 4
    e−rT∫−d2∞ST φ dz=S0∫−d1∞φ(w) dw\begin{aligned} &e^{-rT}\int_{-d_2}^{\infty} S_T\,\varphi\,dz \\ &= S_0\int_{-d_1}^{\infty}\varphi(w)\,dw \end{aligned}
  5. 5
    e−rT∫−d2∞ST φ dz=S0 N(d1)\begin{aligned} &e^{-rT}\int_{-d_2}^{\infty} S_T\,\varphi\,dz \\ &= S_0\,N(d_1) \end{aligned}

The stock part is S0N(d1)≈57.01581S_0 N(d_1) \approx 57.01581. Together, c=S0N(d1)−Ke−rTN(d2)≈57.01581−50.644782=6.371028c = S_0 N(d_1) - K e^{-rT} N(d_2) \approx 57.01581 - 50.644782 = 6.371028: the formula of case study 2.4.6.

Step 3 over the whole line gives one more check. The shifted bell has area 1, so the model's average future price is S0erT≈101.5113S_0 e^{rT} \approx 101.5113: the stock grows on average at the rate rr.

Try it

The loop checks step 3 at three values of zz: both sides agree.

Then the two parts by quad against N(d2)N(d_2) and S0N(d1)S_0N(d_1), their difference (the price), and the average future price.

⌘/Ctrl + Enter runs the cell
Check yourself
Why is the integral split at z∗=−d2z^* = -d_2?

On the desk

NN has no formula, so it is computed

N(x)N(x) is an area under φ\varphi, and lesson 2.6.3 showed it has no formula in familiar functions. Every pricing library computes it numerically. SciPy's norm.cdf uses tuned approximations accurate to about 16 significant digits, close to the limit of a float.

So "the Black–Scholes formula" is exact algebra wrapped around two numerical integrals. Pricing by quad directly, as above, is the same calculation done more slowly; it is useful for payoffs with no formula.

Cutting the infinite integral off

Any numerical method must stop somewhere. Cutting the call integral at z=Lz = L instead of ∞\infty leaves out the tail beyond LL.

cut at LL23468
price left out ($)0.910.0800.00261.4×10−71.4 \times 10^{-7}1.3×10−131.3 \times 10^{-13}

Step 3 shows where the weight sits. The stock part's integrand is a bell centred at σT\sigma\sqrt{T}, not at 0, so the tail starts later when σT\sigma\sqrt{T} is large.

For a 5-year option with σ=0.8\sigma = 0.8, σT≈1.79\sigma\sqrt{T} \approx 1.79 and the price is $65.6312. Cutting at 4 then leaves out $1.3, about 2%. Measure the cut from σT\sigma\sqrt{T} instead: 6 beyond it leaves out less than $0.0000001 for both options.

Try it

call prices by quad from z∗z^* up to top, which defaults to infinity. It writes ST φS_T\,\varphi in the form of step 3: multiplied out directly, eσTze^{\sigma\sqrt{T}z} overflows a float at large zz before φ\varphi can shrink it.

The loops print what each cut leaves out, in scientific notation. The second is the 5-year, 80% volatility option; its cut at 4 leaves out 1.3, written 1.3e+00.

⌘/Ctrl + Enter runs the cell

Tails and precision

A float keeps about 16 significant digits. So 1−N(x)1 - N(x), a tiny number found by subtracting from 1, loses digits as xx grows.

At x=9x = 9, N(9)N(9) is 1−1.1×10−191 - 1.1 \times 10^{-19}, which rounds to exactly 1, and 1 - norm.cdf(9) gives 0. By symmetry the same tail is N(−9)≈1.1×10−19N(-9) \approx 1.1 \times 10^{-19}, computed with no subtraction; SciPy also offers norm.sf(x) for 1−N(x)1 - N(x). Worse, 1 - quad(norm.pdf, -math.inf, 9) returns a small negative number: a negative probability.

Desks meet this in far out-of-the-money options. The put integral, done the same way, gives p=Ke−rTN(−d2)−S0N(−d1)p = K e^{-rT}N(-d_2) - S_0 N(-d_1).

For a strike of 40, writing N(−d)N(-d) as 1−N(d)1 - N(d) gets the fifth digit of the price wrong. Such tiny prices feed ratios, such as implied volatilities, where every digit counts.

Try it

The loop compares 1−N(x)1 - N(x) with N(−x)N(-x) and norm.sf(x) at xx = 5, 8 and 9. At 8 the subtraction is already 7% off; at 9 it gives 0.

Then the far tail by quad both ways, and the strike-40 put both ways.

⌘/Ctrl + Enter runs the cell

Bad inputs and units

  • At expiry, T=0T = 0, the formulas for d1d_1 and d2d_2 divide by zero. There is nothing left to average: the price is the payoff, max⁡(S0−K,0)\max(S_0 - K, 0).
  • With σ→0\sigma \to 0 there is no randomness, and the price tends to max⁡(S0−Ke−rT,0)\max(S_0 - Ke^{-rT}, 0), about $1.488806 here. σ=0\sigma = 0 itself divides by zero, so code must treat it separately.
  • Volatility as a decimal. Typing σ=20\sigma = 20 for 20% prices the call at $100.0, the whole stock.
  • Time in years. Typing T=6T = 6 for six months gives $27.1509 instead of $6.3710.
  • Positive prices. ln⁡(S0/K)\ln(S_0/K) needs S0>0S_0 > 0 and K>0K > 0; a zero or negative input raises an error.

Try it

bs_call is the formula of case study 2.4.6. With T=0T = 0 Python raises ZeroDivisionError.

Then a tiny σ\sigma against its limit S0−Ke−rTS_0 - Ke^{-rT}, and the two unit mistakes.

⌘/Ctrl + Enter runs the cell
Check yourself
A risk report needs the tail area 1−N(9)1 - N(9). Which line gives it accurately?
Check yourself
You price a 5-year option with 80% volatility by integrating up to z=4z = 4. What goes wrong?
Exercise

Price a call and a put by integration

Write call_by_integral(S0, K, T, r, sigma) and put_by_integral(S0, K, T, r, sigma). Each computes the price as an integral with quad, split at z∗=−d2z^* = -d_2.

  1. For the call, the payoff ST−KS_T - K is positive above z∗z^*, so integrate from z∗z^* to ∞\infty.
  2. For the put, the payoff K−STK - S_T is positive below z∗z^*, so integrate from −∞-\infty to z∗z^*.
  3. At expiry, T=0T = 0, return the payoff itself: max⁡(S0−K,0)\max(S_0 - K, 0) for the call and max⁡(K−S0,0)\max(K - S_0, 0) for the put.

The tests compare with the Black–Scholes formula, check put–call parity, c−p=S0−Ke−rTc - p = S_0 - Ke^{-rT}, and try T=0T = 0.

See the tests
import math

from scipy.stats import norm


def _bs_call(S0, K, T, r, sigma):
    d1 = (math.log(S0 / K) + (r + sigma**2 / 2) * T) / (sigma * math.sqrt(T))
    return S0 * norm.cdf(d1) - K * math.exp(-r * T) * norm.cdf(d1 - sigma * math.sqrt(T))


def test_call_matches_formula():
    """The 6-month at-the-money call is worth about 6.3710, as the formula says"""
    expect_close(call_by_integral(100, 100, 0.5, 0.03, 0.2), _bs_call(100, 100, 0.5, 0.03, 0.2), what="call_by_integral", rel=1e-9)


def test_out_of_the_money_call():
    """A strike-120 call matches the formula, about 0.8914"""
    expect_close(call_by_integral(100, 120, 0.5, 0.03, 0.2), _bs_call(100, 120, 0.5, 0.03, 0.2), what="call, K = 120", rel=1e-8)


def test_put_call_parity():
    """Call minus put equals S0 − K e^(−rT)"""
    c = call_by_integral(80, 75, 1.0, 0.04, 0.3)
    p = put_by_integral(80, 75, 1.0, 0.04, 0.3)
    expect_close(c - p, 80 - 75 * math.exp(-0.04), what="c − p", rel=1e-8)


def test_put_positive():
    """A put is worth something: the at-the-money put is about 4.8822"""
    expect_close(put_by_integral(100, 100, 0.5, 0.03, 0.2), 4.882221902473731, what="put_by_integral", rel=1e-8)


def test_at_expiry():
    """At expiry (T = 0) each option is worth its payoff: a call 10 when S0 = 110, a put 10 when S0 = 90"""
    expect_close(call_by_integral(110, 100, 0.0, 0.03, 0.2), 10.0, what="call_by_integral at T = 0, S0 = 110")
    expect_close(call_by_integral(90, 100, 0.0, 0.03, 0.2), 0.0, what="call_by_integral at T = 0, S0 = 90")
    expect_close(put_by_integral(90, 100, 0.0, 0.03, 0.2), 10.0, what="put_by_integral at T = 0, S0 = 90")

Summary card

  • An option's price is its discounted average payoff: c=e−rT∫payoff(z) φ(z) dzc = e^{-rT}\int \text{payoff}(z)\,\varphi(z)\,dz, with ST=S0e(r−σ2/2)T+σTzS_T = S_0e^{(r - \sigma^2/2)T + \sigma\sqrt{T}z}.
  • The payoff has a corner at z∗=−d2z^* = -d_2; split the integral there.
  • The strike part is Ke−rTN(d2)Ke^{-rT}N(d_2), where N(d2)N(d_2) is the model's chance of ending in the money.
  • Completing the square and the substitution w=z−σTw = z - \sigma\sqrt{T} turn the stock part into S0N(d1)S_0N(d_1).
  • NN has no formula and is computed numerically. Use N(−x)N(-x), not 1−N(x)1 - N(x), for far tails.
  • Cut an infinite integral about 6 beyond the centre of its integrand, σT\sigma\sqrt{T} here. Treat T=0T = 0 and σ=0\sigma = 0 separately, and keep σ\sigma and TT in annual decimals and years.

Free further reading

Optional. This lesson covers what you need; these add more examples and practice. All are free to read.