The Black–Scholes equation
Take the call of case studies 2.4.6 and 4.1.4: strike , expiry years, rate and volatility , both yearly decimals. Its value depends on the stock price and the date , in years from today, so is the time left.
Rule: The Black–Scholes equation
For every stock price and every date . , and are partial derivatives (lesson 4.1.2). The Stochastic Calculus course derives the equation from a hedging argument; here, take it as given, as case study 2.4.6 took the formula.
Read it with lesson 6.3.1. It is linear: and its partials appear only on their own, multiplied by , or . Its order is 2, from .
Unlike the heat equation, its coefficients depend on . Case study 6.4.4 changes variables to turn it into the heat equation itself.
Which way it runs
Move the other terms across: . The coefficient of is negative, as in lesson 6.3.2's . So the equation runs backwards in time, and needs a terminal condition, at expiry.
In the time left , , and the equation runs forwards in from the payoff. That is why every formula of case study 4.1.4 contains .
If an option never expires, nothing depends on , so . The equation becomes the ODE , which case study 6.2.4 solves for the perpetual American put.
Conditions: the payoff and the two ends
The region is and : a strip in the plane, open on the side of large . Its conditions, edge by edge:
- At expiry, . The call is worth its payoff: .
- At . In this model a stock that reaches 0 stays there. Put in the equation: the two terms with vanish, leaving the ODE of module 6.1. So , and for the call : the call is worthless.
- For large . Exercise is all but certain, so the call is worth the stock minus the strike paid at expiry: . That is case study 2.4.6's floor.
Rule: The call's conditions
A terminal condition on the edge and a condition at each end of the range. With them the problem is well-posed (lesson 6.3.2), and the Black–Scholes formula is its solution.
The formula meets each condition. Today, at it gives 51.4960 against the floor's 51.4888, and at the gap is only . At the call is worth less than .
The call's value as a surface over and as slices. The equation carries the payoff, on the back edge, back to today's smooth curve, which nears the payoff's corner as expiry approaches.
Try it
call is case study 4.1.4's formula with the date t. The first line compares the formula at and with the floor .
The second line shows the call all but worthless far below the strike: at and it is below and .
Checking the formula by finite differences
Lesson 6.3.1 tested a candidate by computing its partials and substituting. Here, reprice the call at bumped points and estimate , and by central differences (lesson 4.1.2). The residual is then the left side of the equation.
With desk bumps, 1% of and one day each way in :
| residual | with bumps 10 times smaller | ||||
|---|---|---|---|---|---|
| 0.080751 | 0.013216 | 0.0007 | |||
| 0.570054 | 0.027762 | ||||
| 0.928491 | 0.008033 | 0.0011 | |||
| 0.549590 | 0.039553 | ||||
| 0.521960 | 0.088703 |
Every residual is small next to the terms, which run to 19. Bumps 10 times smaller make it 100 times smaller: truncation error of size (lesson 2.8.1), the mark of a true solution. The formula solves the equation.
Try it
residual estimates the three partials by central differences and returns them with the left side of the equation. hS is the bump in , ht the bump in , in years.
Each line prints a point, its estimates and residual, then the residual with bumps of 0.1% and a tenth of a day.
A candidate that meets the payoff but fails
Matching the terminal condition is not enough. Take the discounted payoff, : at it is exactly the payoff. Below the strike it is 0, which solves the equation.
Above the strike it is , with , and . The residual is : 3.5464 at today, and the finite differences agree. The equation is what fixes the value before expiry.
Close to expiry
With one day left, at , the call is worth 0.4217 and a year. A $1 bump now gives a gamma of 0.3545 instead of 0.3811, as case study 2.8.4 found, and a residual of .
That residual measures the bump, not the formula. The bend crowds into about dollars (case study 2.8.4), so a $1 bump is far too wide. With a 10-cent bump the residual is , and with 1 cent , with the time bump a hundredth of a day.
Theta changes fast too, so the time bump must shrink as well: with a tenth of a day, even the 1-cent bump leaves . And a central step of one day, from one day before expiry, reaches , where the formula divides by zero.
Try it
The first lines test the discounted payoff below and above the strike: the residual is 0, then .
The loop is the one-day call with three bumps in . The time bump is a hundredth of a day, and a tenth in the last row. Each line prints the bumps, the estimated gamma and the residual.
The last lines take a one-day step in , which reaches expiry.
The equation in Greeks
The three partials have names on a desk (case study 4.1.4): , and . So the equation is a relation among the Greeks.
Rule: Black–Scholes in Greeks
With per year, per dollar of stock, and and as yearly decimals. It holds at every before expiry, for any option whose value the Black–Scholes equation describes.
With the closed forms of case studies 2.7.4 and 4.1.4, each row adds up to 0:
| 0.3915 | 1.6914 | 0.1934 | |||
| 6.3710 | 5.5544 | 1.7105 | |||
| 22.0575 | 2.3117 | 3.3432 | |||
| 4.3576 | 7.9168 | 1.6492 | |||
| 1.8586 | 17.8134 | 1.5669 |
The algebra shows why. Case study 4.1.4 split theta into two parts, and each cancels one side of the rest:
- 1
, here .
- 2
, here before rounding.
- 3
.
Try it
greeks returns the value, delta, gamma and theta (per year) from the closed forms of case studies 2.7.4 and 4.1.4.
Each line prints a point, the value, the four terms of the equation, and whether they add up to 0 within .
Theta pays for gamma
The equation has a desk reading. Hold the call and sell shares against it, a delta hedge, with the cash from the sale earning the rate . Over a short time , with a stock move , the position gains about
The first line is case study 2.7.4's delta–gamma estimate with the delta term hedged away, plus theta and the interest on the cash. The equation turns it into the second line: the time decay and the interest together cost exactly .
So the hedged position breaks even when the stock moves by , about one standard deviation: $1.0468 for a day. Smaller moves lose, larger moves gain. Repricing the call agrees:
| stock move in a day | 0 | 0.50 | ||||
|---|---|---|---|---|---|---|
| repriced gain | 0.0392 | 0.0416 | 0.1063 | |||
| 0 | 0.0403 | 0.0403 | 0.1098 |
What is left grows like the cube of the move, as in case study 2.7.4.
Try it
The first line prints the break-even move and the day's cost, .
The loop reprices the hedged position a day later, with the interest on the cash, and prints it beside the estimate.
The put: same equation, another payoff
A put, the right to sell at the strike, satisfies the same equation; only the conditions change. At expiry it pays , and for large it is worthless.
At the ODE now starts from . So : $98.5112 today.
The solution is the Black–Scholes put, , worth 4.8822 today: the price case study 5.11.4's tree closed in on. Its delta is , its gamma is the call's, and its theta is
which is a year, a day. In the equation: .
Put–call parity from the equation
The equation is linear, so the call minus the put, , is a solution too (lesson 6.3.1). Its terminal condition is .
A simpler solution has the same terminal value: . Both and solve the equation on their own, and at their difference is . A well-posed problem has only one solution (lesson 6.3.2), so
which is put–call parity (case study 3.3.4): .
When theta is positive
Case study 4.1.4 found that a deep in-the-money put, at strike 130, gains 0.0064 a day as time passes. The equation says why: .
For that put , and gamma is small, 0.006897. So a year, or 0.0064 a day: the interest terms outweigh the gamma term.
Try it
put_greeks returns the put's value, delta, gamma and theta. Each of the first two lines prints them and checks the equation, at strike 100 and at strike 130 (theta also per day).
SymPy then checks that and each solve the equation, and the last line is parity.
Try it
The traps of the note, with the six-month call's numbers: the per-day theta, the time-to-expiry sign, and the theta the equation implies.
Theta from the equation, and a report check
Write two functions.
implied_theta(V, delta, gamma, S, r, sigma, days=None)returns the theta the Black–Scholes equation implies, , per year. Ifdaysis given (365 for calendar days, 252 for trading days), return it per day instead. RaiseValueErrorifSorsigmais not positive, or ifdaysis given and not positive.flag_reports(rows, tol=0.001)takes a list of risk-report rows. Each row is a dictionary with keys"V","delta","gamma","theta","S","r"and"sigma", where"theta"is per calendar day. Return the positions (0, 1, 2, …) of the rows whose theta differs from the implied one by more thantoldollars a day.
The tests use the call and puts above, and a report with a per-year theta, a flipped sign and a per-trading-day theta.
See the tests
import math
from scipy.stats import norm
def _greeks(S, K, tau, r, sigma, kind="call"):
"""Value, delta, gamma and theta per year from the closed forms."""
d1 = (math.log(S / K) + (r + sigma**2 / 2) * tau) / (sigma * math.sqrt(tau))
d2 = d1 - sigma * math.sqrt(tau)
gamma = norm.pdf(d1) / (S * sigma * math.sqrt(tau))
vol_part = -S * norm.pdf(d1) * sigma / (2 * math.sqrt(tau))
if kind == "call":
value = S * norm.cdf(d1) - K * math.exp(-r * tau) * norm.cdf(d2)
return value, norm.cdf(d1), gamma, vol_part - r * K * math.exp(-r * tau) * norm.cdf(d2)
value = K * math.exp(-r * tau) * norm.cdf(-d2) - S * norm.cdf(-d1)
return value, norm.cdf(d1) - 1, gamma, vol_part + r * K * math.exp(-r * tau) * norm.cdf(-d2)
def _row(S, K, tau, r, sigma, kind="call"):
V, delta, gamma, theta = _greeks(S, K, tau, r, sigma, kind)
return {"V": V, "delta": delta, "gamma": gamma, "theta": theta / 365, "S": S, "r": r, "sigma": sigma}
def test_call_per_year():
"""The six-month call implies theta −7.0738 a year"""
V, delta, gamma, theta = _greeks(100, 100, 0.5, 0.03, 0.2)
expect_close(implied_theta(V, delta, gamma, 100, 0.03, 0.2), theta, what="implied_theta for the six-month call", rel=1e-9)
def test_call_per_day():
"""Per calendar day it is −0.0194, and per trading day −0.0281"""
V, delta, gamma, theta = _greeks(100, 100, 0.5, 0.03, 0.2)
expect_close(implied_theta(V, delta, gamma, 100, 0.03, 0.2, days=365), theta / 365, what="implied_theta(..., days=365)", rel=1e-9)
expect_close(implied_theta(V, delta, gamma, 100, 0.03, 0.2, days=252), theta / 252, what="implied_theta(..., days=252)", rel=1e-9)
def test_put():
"""The six-month put implies theta −4.1184 a year"""
V, delta, gamma, theta = _greeks(100, 100, 0.5, 0.03, 0.2, kind="put")
expect_close(implied_theta(V, delta, gamma, 100, 0.03, 0.2), theta, what="implied_theta for the put", rel=1e-9)
def test_deep_put_positive():
"""The deep in-the-money put (strike 130) has positive theta, 2.3304 a year"""
V, delta, gamma, theta = _greeks(100, 130, 0.5, 0.03, 0.2, kind="put")
expect_close(implied_theta(V, delta, gamma, 100, 0.03, 0.2), theta, what="implied_theta for the strike-130 put", rel=1e-9)
def test_other_option():
"""A one-year call on a $50 stock, strike 55, 35% volatility, 1% rate"""
V, delta, gamma, theta = _greeks(50, 55, 1.0, 0.01, 0.35)
expect_close(implied_theta(V, delta, gamma, 50, 0.01, 0.35), theta, what="implied_theta for the $50 stock", rel=1e-9)
def test_flags():
"""A report flags the per-year theta, the flipped sign and the per-trading-day theta, not the good rows"""
good_call, good_put = _row(100, 100, 0.5, 0.03, 0.2), _row(100, 100, 0.5, 0.03, 0.2, kind="put")
per_year = dict(good_call, theta=good_call["theta"] * 365)
flipped = dict(good_call, theta=-good_call["theta"])
trading = dict(good_call, theta=good_call["theta"] * 365 / 252)
expect_equal(flag_reports([good_call, good_put, per_year, flipped, trading]), [2, 3, 4], what="flag_reports(rows)")
def test_flags_tolerance():
"""A theta off by half a cent a day is flagged at tol = 0.001 but not at tol = 0.01"""
off = _row(100, 100, 0.5, 0.03, 0.2)
off["theta"] += 0.005
expect_equal(flag_reports([off]), [0], what="flag_reports([row off by 0.005])")
expect_equal(flag_reports([off], tol=0.01), [], what="flag_reports([row off by 0.005], tol=0.01)")
def test_empty():
"""An empty report has nothing to flag"""
expect_equal(flag_reports([]), [], what="flag_reports([])")
def test_bad_inputs():
"""A stock price or volatility that is not positive, or days = 0, raises ValueError"""
for args, kwargs in [((6.37, 0.57, 0.028, 0, 0.03, 0.2), {}), ((6.37, 0.57, 0.028, 100, 0.03, 0), {}),
((6.37, 0.57, 0.028, 100, 0.03, 0.2), {"days": 0})]:
try:
implied_theta(*args, **kwargs)
except ValueError:
continue
raise AssertionError(f"implied_theta{args} with {kwargs} should raise ValueError")
Summary card
- The Black–Scholes equation is a linear PDE of order 2 for . It runs backwards in time, from a terminal condition at expiry.
- A call has , and for large ; at the equation becomes the ODE .
- The Black–Scholes formula solves it: central differences leave residuals that shrink like . Near expiry the bumps must be small next to .
- In Greeks: , which is for the six-month call. A delta-hedged position breaks even on a move of : theta pays for gamma.
- The put solves the same equation with the payoff ; by linearity and uniqueness, .
- Theta must be per year in the equation; a per-day or flipped theta breaks the check.