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.
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.15The 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 holdsWhat 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 valueN(x) = 0.5 * (1 + erf(x / sqrt(2))), andd1 = (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 valueS - 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