SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 6.4.1 Diffusion and the heat equationConcept · 30 min
      2. 6.4.2 The fundamental solutionConcept · 30 min
      3. 6.4.3 Practice set: the heat equationPractice set · 20 min
      4. 6.4.4 Case study: from Black–Scholes to the heat equationCase study · 35 min

Level 6, Differential equations 6.4 The heat equation Lesson 4 of 4

Case studyLesson 6.4.4 in Level 6, Differential equations. About 35 minutes.

Case study: from Black–Scholes to the heat equation

The equation, and four obstacles

Case study 6.3.4 stated the equation for the value V(S,t)V(S, t) of an option on a stock at price SS, at time tt in years:

Vt+12σ2S2 VSS+rS VS−rV=0V_t + \tfrac{1}{2}\sigma^2S^2\,V_{SS} + rS\,V_S - rV = 0

Here rr is the continuous interest rate and σ\sigma the volatility, both annual decimals. A call with strike KK and expiry TT has the terminal condition V(S,T)=max⁡(S−K,0)V(S, T) = \max(S - K, 0). The inputs are those of case study 2.4.6: K=100K = 100, T=0.5T = 0.5, r=0.03r = 0.03, σ=0.2\sigma = 0.2, and today t=0t = 0 with S=100S = 100.

Next to us=uxxu_s = u_{xx}, the equation has four differences, and each change below removes one.

  1. Its condition is at the end, t=Tt = T, not at the start.
  2. Its coefficients S2S^2 and SS change with the stock price.
  3. It has terms in VSV_S and VV that the heat equation lacks.
  4. Its diffusion term has a factor 12σ2\frac{1}{2}\sigma^2, not 1.

Step 1: time to expiry

Case study 6.3.4 found that the equation runs backwards in time, so it needs a condition at expiry. Measure time backwards from expiry instead: τ=T−t\tau = T - t, the time to expiry in years. Then Vt=−VτV_t = -V_\tau (lesson 6.3.2), and

Vτ=12σ2S2 VSS+rS VS−rVV_\tau = \tfrac{1}{2}\sigma^2S^2\,V_{SS} + rS\,V_S - rV

This runs forwards in τ\tau, as heat does (lesson 6.4.1). The payoff becomes a starting profile at τ=0\tau = 0, and today is τ=0.5\tau = 0.5.

Step 2: the log of the price

Measure the price on a log scale relative to the strike: x=ln⁡(S/K)x = \ln(S/K), so S=KexS = Ke^x. A price of 100 is x=0x = 0, the strike itself; 110 is x=0.09531x = 0.09531 and 90 is x=−0.105361x = -0.105361. Lesson 6.2.2 used the same logarithm on the equidimensional equation, whose coefficients also grow with powers of the variable.

Write V(S,t)=W(x,τ)V(S, t) = W(x, \tau). The chain rule (lessons 2.3.3 and 4.2.2) gives the partials in SS, with dxdS=1S\frac{dx}{dS} = \frac{1}{S}:

  1. 1
    VS=WxSV_S = \frac{W_x}{S}
  2. 2
    VSS=Wxx−WxS2V_{SS} = \frac{W_{xx} - W_x}{S^2}
  3. 3
    S VS=WxS2 VSS=Wxx−Wx\begin{gathered} S\,V_S = W_x \\[4pt] S^2\,V_{SS} = W_{xx} - W_x \end{gathered}

Substituting these into the equation of step 1 gives an equation with constant coefficients:

Wτ=12σ2 Wxx+(r−12σ2)Wx−rW\begin{aligned} W_\tau &= \tfrac{1}{2}\sigma^2\,W_{xx} \\ &\quad + \left(r - \tfrac{1}{2}\sigma^2\right)W_x - rW \end{aligned}

Prices from 0 to infinity become all of xx, from −∞-\infty to ∞\infty: the endless rod of lesson 6.4.2. The payoff becomes W(x,0)=Kmax⁡(ex−1,0)W(x, 0) = K\max(e^x - 1, 0).

Try it

W is the Black–Scholes formula written in xx and τ\tau. The loop takes central differences (lesson 2.8.1) at three points and prints WτW_\tau next to the right side of the new equation.

They agree to 4 decimal places: 7.0738, 3.9329 and 4.4488.

⌘/Ctrl + Enter runs the cell

Step 3: a factor that removes two terms

The terms in WxW_x and WW remain. Write W=Keax+bτ u(x,τ)W = K e^{ax + b\tau}\,u(x, \tau) for two numbers aa and bb still to choose. The product rule gives each partial as Keax+bτKe^{ax + b\tau} times a bracket:

Wτ→uτ+b uWx→ux+a uWxx→uxx+2a ux+a2u\begin{aligned} W_\tau &\to u_\tau + b\,u \\ W_x &\to u_x + a\,u \\ W_{xx} &\to u_{xx} + 2a\,u_x + a^2u \end{aligned}

Put these into the equation of step 2 and divide by Keax+bτKe^{ax + b\tau}. The uxu_x terms add up to (σ2a+r−12σ2)ux\left(\sigma^2 a + r - \frac{1}{2}\sigma^2\right)u_x, and the right aa makes that 0. The right bb then cancels every term in uu, leaving only uτ=12σ2uxxu_\tau = \frac{1}{2}\sigma^2 u_{xx}.

Rule: From Black–Scholes to heat

x=ln⁡(S/K),τ=T−tV=Keax+bτ u(x,τ)a=12−rσ2,b=−r−12σ2a2uτ=12σ2 uxx\begin{gathered} x = \ln(S/K), \quad \tau = T - t \\[4pt] V = K e^{ax + b\tau}\,u(x, \tau) \\[4pt] a = \tfrac{1}{2} - \tfrac{r}{\sigma^2}, \quad b = -r - \tfrac{1}{2}\sigma^2a^2 \\[4pt] u_\tau = \tfrac{1}{2}\sigma^2\,u_{xx} \end{gathered}

aa has no units; bb is a rate per year, like rr. For the shared inputs, a=−0.25a = -0.25 and b=−0.03125b = -0.03125.

Step 4: a clock for heat

uτ=12σ2uxxu_\tau = \frac{1}{2}\sigma^2 u_{xx} is a heat equation with diffusivity D=12σ2=0.02D = \frac{1}{2}\sigma^2 = 0.02 per year. As in lesson 6.4.1, change the clock to s=12σ2τs = \frac{1}{2}\sigma^2\tau; then us=uxxu_s = u_{xx}. Today, with half a year to go, s=0.02×0.5=0.01s = 0.02 \times 0.5 = 0.01.

ss has no units, because σ2\sigma^2 is per year and τ\tau is in years. The whole half-year is only 0.01 of heat-equation time: a short spell of smoothing.

The payoff becomes a kink

At τ=0\tau = 0, u=We−ax/Ku = W e^{-ax}/K, so the starting profile is

u0(x)=e−axmax⁡(ex−1,0)=max⁡(e1.25x−e0.25x, 0)\begin{aligned} u_0(x) &= e^{-ax}\max(e^x - 1, 0) \\ &= \max\left(e^{1.25x} - e^{0.25x},\ 0\right) \end{aligned}

using 1−a=1.251 - a = 1.25 and −a=0.25-a = 0.25. It is 0 for x≤0x \le 0, and its slope just right of 0 is 1.25−0.25=11.25 - 0.25 = 1. So near the strike it is lesson 6.4.2's kink max⁡(x,0)\max(x, 0), bending up a little more further out.

xx0.050.10.20.5
u0(x)u_0(x)0.0519160.1078330.2327540.735098
kink max⁡(x,0)\max(x, 0)0.050.10.20.5

The slope is 1 for every input, since (1−a)−(−a)=1(1 - a) - (-a) = 1. So every call becomes a kink-like profile; only how much it bends up depends on rr and σ\sigma.

Try it

SymPy puts W=eax+bτuW = e^{ax + b\tau}u into step 2's equation, divides by eax+bτe^{ax + b\tau}, and solves for the aa that removes uxu_x and the bb that removes uu. With both, what is left is uτ−12σ2uxxu_\tau - \frac{1}{2}\sigma^2 u_{xx}. The constants KK cancel, so they are left out.

Then the numbers for the shared inputs, a central-difference check of uτ=0.02 uxxu_\tau = 0.02\,u_{xx} at two points, and the table of u0u_0.

⌘/Ctrl + Enter runs the cell
Check yourself
Why does step 1 measure time as τ=T−t\tau = T - t?

Solving with the heat kernel

Now lesson 6.4.2 applies. With s=0.01s = 0.01 today,

u(x,s)=∫−∞∞u0(y) Φ(x−y,s) dyu(x, s) = \int_{-\infty}^{\infty} u_0(y)\,\Phi(x - y, s)\,dy

and the price is V=Keax+bτu(x,s)V = Ke^{ax + b\tau}u(x, s). At S=100S = 100, x=0x = 0, so V=100 e−0.015625 u(0,0.01)V = 100\,e^{-0.015625}\,u(0, 0.01).

The bell Φ(⋅,0.01)\Phi(\cdot, 0.01) has width 2s=σ2τ=0.141421\sqrt{2s} = \sqrt{\sigma^2\tau} = 0.141421. That is σT\sigma\sqrt{T}, the standard deviation of the six-month log return in case study 5.6.5. In lesson 6.4.2's reading, u(x,s)u(x, s) is u0u_0 averaged over a random shift of xx: here, the stock's random log return to expiry.

quad from the corner at 0 to ten widths above xx gives u(0,0.01)≈0.0647136u(0, 0.01) \approx 0.0647136, and the price is 6.371028. The kink alone would give s/π≈0.0564\sqrt{s/\pi} \approx 0.0564: the extra upward bend of u0u_0 makes uu 1.147 times as large.

SS8090100110120
x=ln⁡(S/100)x = \ln(S/100)−0.223144-0.223144−0.105361-0.10536100.095310.182322
u(x,0.01)u(x, 0.01)0.0037610.0208190.0647140.1385490.234497
price by heat0.39152.10436.37113.31922.0575
Black–Scholes0.39152.10436.37113.31922.0575

The two rows agree to within 10−1210^{-12} at every price. Three months later, at t=0.25t = 0.25, ss is 0.005 and the call is worth 4.3576 at S=100S = 100 and 11.5715 at S=110S = 110, again matching the formula.

The same problem in two coordinates. In xx and ss the payoff is a kink-like profile that the heat equation smooths for a time of only 0.01. Mapped back with V=Keax+bτuV = Ke^{ax + b\tau}u, the smoothed profile is the call's value today.

Try it

u0 is the starting profile and heat the kernel. u_heat integrates from the corner at 0, or from ten widths below xx if that is higher, to ten widths above.

The first line prints u(0,0.01)u(0, 0.01), the price, the kink's s/π\sqrt{s/\pi} and their ratio. The loop prints the table, and True where the heat price is within 10−1210^{-12} of the formula. The last loop prices at t=0.25t = 0.25, with τ=0.25\tau = 0.25 and s=0.005s = 0.005.

⌘/Ctrl + Enter runs the cell

Back to the Black–Scholes formula

The integral can also be done by hand. Above 0, u0u_0 is two exponentials, ec1y−ec2ye^{c_1y} - e^{c_2y} with c1=1−a=1.25c_1 = 1 - a = 1.25 and c2=−a=0.25c_2 = -a = 0.25. Lesson 6.4.2 gave each one cut at 0:

∫0∞ecy Φ(x−y,s) dy=ecx+c2s N ⁣(x+2cs2s)\begin{aligned} &\int_0^{\infty} e^{cy}\,\Phi(x - y, s)\,dy \\ &= e^{cx + c^2 s}\,N\!\left(\frac{x + 2cs}{\sqrt{2s}}\right) \end{aligned}

Multiply by Keax+bτKe^{ax + b\tau} and add up the exponents. Since 2s=σ2τ2s = \sigma^2\tau, they simplify for every rr and σ\sigma:

  • First piece: a+c1=1a + c_1 = 1 and bτ+c12s=0b\tau + c_1^2 s = 0, so it is KexN(⋅)=S N(d1)Ke^x N(\cdot) = S\,N(d_1).
  • Second piece: a+c2=0a + c_2 = 0 and bτ+c22s=−rτb\tau + c_2^2 s = -r\tau, so it is Ke−rτN(d2)Ke^{-r\tau}N(d_2).
  • Inside NN: 2c1s=(r+12σ2)τ2c_1 s = \left(r + \frac{1}{2}\sigma^2\right)\tau and 2c2s=(r−12σ2)τ2c_2 s = \left(r - \frac{1}{2}\sigma^2\right)\tau.

The last line makes the arguments d1=ln⁡(S/K)+(r+σ2/2)τστd_1 = \frac{\ln(S/K) + (r + \sigma^2/2)\tau}{\sigma\sqrt{\tau}} and d2=d1−στd_2 = d_1 - \sigma\sqrt{\tau}. So the heat solution is

V=S N(d1)−Ke−rτN(d2)V = S\,N(d_1) - Ke^{-r\tau}N(d_2)

the formula of case study 2.4.6. Today d1=0.176777d_1 = 0.176777 and d2=0.035355d_2 = 0.035355, and the two pieces are 57.01581 and 50.644782: the stock part and the strike part of case study 2.6.5.

Try it

SymPy checks the three facts in the list for every rr and σ\sigma. The first line should print 0 and −rτ-r\tau, the second two 0s.

Then the two pieces for the shared inputs, with quad beside the cut-exponential rule, and the arguments of NN.

⌘/Ctrl + Enter runs the cell

The heat route and case study 2.6.5's route are the same calculation. That case study averaged the payoff over the normal log return. Here the heat kernel does the averaging, and the factor eax+bτe^{ax + b\tau} supplies the discounting and the drift.

Check yourself
The heat kernel in the price at t=0t = 0 has width 2s\sqrt{2s}. What is that in terms of the option?

On the desk

Black and Scholes found their formula this way, by reducing their equation to the heat equation. Using the change of variables in code brings its own traps.

The put, from the same equation

The put pays max⁡(K−S,0)\max(K - S, 0). Steps 1 to 4 are unchanged; only the starting profile changes, to u0=max⁡(e0.25x−e1.25x,0)u_0 = \max\left(e^{0.25x} - e^{1.25x}, 0\right), which is positive for x<0x < 0. The heat integral gives 4.882222, case study 5.11.4's European put. Put–call parity holds: 6.371028−4.882222=1.488806=S−Ke−rT6.371028 - 4.882222 = 1.488806 = S - Ke^{-rT}.

A factor that can overflow

The factor is mild here, but a=12−rσ2a = \frac{1}{2} - \frac{r}{\sigma^2} explodes as σ\sigma shrinks. At σ=0.5%\sigma = 0.5\%, a=−1199.5a = -1199.5, so u0(y)u_0(y) contains e1200.5 ye^{1200.5\,y}. That overflows a float for yy above about 0.59, a stock price 1.81 times the strike.

The cure is to multiply before exponentiating. Inside the integral, Keax+bτu0(y)Ke^{ax + b\tau}u_0(y) is Kebτea(x−y)max⁡(ey−1,0)Ke^{b\tau}e^{a(x - y)}\max(e^y - 1, 0), and on the bell's window x−yx - y is tiny. Written that way, a call with S=200S = 200 at 0.5% volatility prices at 101.488806 instead of crashing.

Try it

The put by heat, its formula price and S−Ke−rTS - Ke^{-rT}. Then the low-volatility call: aa, the yy where e(1−a)ye^{(1 - a)y} passes the largest float, and that edge as a multiple of the strike. A separate e(1−a)xe^{(1 - a)x} overflows at x=ln⁡2x = \ln 2, while the combined integrand gives the price, next to its floor S−Ke−rTS - Ke^{-rT}.

The last loop prints ss and the bell's width στ\sigma\sqrt{\tau} for the two unit mistakes of the note below.

⌘/Ctrl + Enter runs the cell
Exercise

Price an option through the heat equation

Write price_by_heat(S, K, T, r, sigma, kind="call"). It returns the price of a European call or put by steps 1 to 4 and the heat kernel.

  1. Compute aa, bb, s=12σ2Ts = \frac{1}{2}\sigma^2 T and x=ln⁡(S/K)x = \ln(S/K).
  2. Integrate KebTea(x−y) payoff(y) Φ(x−y,s)Ke^{bT}e^{a(x - y)}\,\text{payoff}(y)\,\Phi(x - y, s) with quad, where the payoff is ey−1e^y - 1 for the call (for y>0y > 0) and 1−ey1 - e^y for the put (for y<0y < 0). Use a window of 12 widths 2s\sqrt{2s} around xx, cut at 0.
  3. At T=0T = 0 return the payoff. Raise ValueError for S≤0S \le 0, K≤0K \le 0, σ≤0\sigma \le 0, or a kind other than "call" and "put".

The tests compare with the Black–Scholes formula for the shared call and put and three other options, one of them at 0.5% volatility, where a separate e−axe^{-ax} overflows.

See the tests
import math

from scipy.stats import norm


def _bs(S, K, T, r, sigma, kind="call"):
    d1 = (math.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * math.sqrt(T))
    d2 = d1 - sigma * math.sqrt(T)
    if kind == "call":
        return S * norm.cdf(d1) - K * math.exp(-r * T) * norm.cdf(d2)
    return K * math.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)


def test_shared_call():
    """The 6-month at-the-money call is worth 6.3710"""
    expect_close(price_by_heat(100, 100, 0.5, 0.03, 0.2), _bs(100, 100, 0.5, 0.03, 0.2), what="price_by_heat(100, 100, 0.5, 0.03, 0.2)", rel=1e-8)


def test_shared_put():
    """The matching put is worth 4.8822"""
    expect_close(price_by_heat(100, 100, 0.5, 0.03, 0.2, "put"), _bs(100, 100, 0.5, 0.03, 0.2, "put"), what="price_by_heat(..., 'put')", rel=1e-8)


def test_out_of_the_money_call():
    """A strike-120 call is worth 0.8914"""
    expect_close(price_by_heat(100, 120, 0.5, 0.03, 0.2), _bs(100, 120, 0.5, 0.03, 0.2), what="price_by_heat(100, 120, 0.5, 0.03, 0.2)", rel=1e-8)


def test_other_inputs():
    """A 1-year put with S = 80, K = 100, r = 5% and 35% volatility is worth 21.1650"""
    expect_close(price_by_heat(80, 100, 1, 0.05, 0.35, "put"), _bs(80, 100, 1, 0.05, 0.35, "put"), what="price_by_heat(80, 100, 1, 0.05, 0.35, 'put')", rel=1e-8)


def test_low_volatility():
    """At 0.5% volatility a call with S = 200, K = 100 is worth 101.4888, with no overflow"""
    try:
        value = price_by_heat(200, 100, 0.5, 0.03, 0.005)
    except OverflowError:
        raise AssertionError("OverflowError: with a = 1/2 - r/sigma^2 = -1199.5, combine e^(a(x - y)) inside the integral")
    expect_close(value, _bs(200, 100, 0.5, 0.03, 0.005), what="price_by_heat(200, 100, 0.5, 0.03, 0.005)", rel=1e-8)


def test_at_expiry():
    """At T = 0 the price is the payoff: 5 for a call at 105 and for a put at 95"""
    expect_close(price_by_heat(105, 100, 0, 0.03, 0.2), 5.0, what="call at expiry")
    expect_close(price_by_heat(95, 100, 0, 0.03, 0.2, "put"), 5.0, what="put at expiry")


def test_bad_inputs():
    """A zero volatility or an unknown kind raises ValueError"""
    for args, kind in [((100, 100, 0.5, 0.03, 0.0), "call"), ((100, 100, 0.5, 0.03, 0.2), "straddle")]:
        try:
            price_by_heat(*args, kind=kind)
        except ValueError:
            continue
        raise AssertionError(f"expected ValueError for sigma={args[4]}, kind={kind!r}")

Summary card

  • τ=T−t\tau = T - t turns the terminal condition into a starting profile; x=ln⁡(S/K)x = \ln(S/K) gives constant coefficients: Wτ=12σ2Wxx+(r−12σ2)Wx−rWW_\tau = \frac{1}{2}\sigma^2W_{xx} + (r - \frac{1}{2}\sigma^2)W_x - rW.
  • V=Keax+bτuV = Ke^{ax + b\tau}u with a=12−rσ2a = \frac{1}{2} - \frac{r}{\sigma^2} and b=−r−12σ2a2b = -r - \frac{1}{2}\sigma^2a^2 leaves uτ=12σ2uxxu_\tau = \frac{1}{2}\sigma^2u_{xx}; with s=12σ2τs = \frac{1}{2}\sigma^2\tau, us=uxxu_s = u_{xx}. For the shared call, a=−0.25a = -0.25, b=−0.03125b = -0.03125 and s=0.01s = 0.01.
  • The call's payoff becomes u0=max⁡(e(1−a)x−e−ax,0)u_0 = \max(e^{(1 - a)x} - e^{-ax}, 0), a kink with slope 1 at the strike.
  • The heat kernel's width 2s=στ\sqrt{2s} = \sigma\sqrt{\tau} is the spread of the log return. The heat integral gives 6.3710, and by hand it gives SN(d1)−Ke−rτN(d2)SN(d_1) - Ke^{-r\tau}N(d_2).
  • In code, combine ea(x−y)e^{a(x - y)} inside the integral: aa is huge at low volatility. Keep σ\sigma and τ\tau in annual units, and return the payoff at expiry.

Free further reading

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