Delta, Gamma, Theta, Vega. Step by step, with code, plots, and an interactive view of how gamma changes near expiry.
The Greeks are sensitivities. They answer simple questions about an option price.
All four come from the same Black and Scholes formula. Once you have the helper variables $d_1$ and $d_2$, every Greek is one short line of code.
You only need NumPy and SciPy.
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
And one set of inputs we will reuse for every example.
S = 100.0 # Spot price
K = 100.0 # Strike
T = 1.0 # Time to expiry, in years
r = 0.05 # Risk-free rate
sigma = 0.25 # Volatility
$d_1$ and $d_2$ show up in every Greek. Compute them once and reuse.
$$d_1 = \frac{\ln(S/K) + (r + \tfrac{\sigma^2}{2})\,T}{\sigma\sqrt{T}}, \quad d_2 = d_1 - \sigma\sqrt{T}$$
def d1(S, K, T, r, sigma):
return (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
def d2(S, K, T, r, sigma):
return d1(S, K, T, r, sigma) - sigma * np.sqrt(T)
Delta measures the change in option price for a $1 change in the stock. For a call, it sits between 0 and 1. For a put, between $-1$ and 0.
$$\Delta_{\text{call}} = N(d_1), \qquad \Delta_{\text{put}} = N(d_1) - 1$$
Reading: a call delta of 0.60 means the option will move about $0.60 if the stock moves $1. A put delta of $-0.40$ means the put will move about $0.40 against you if the stock moves up by $1.
def delta(S, K, T, r, sigma, option_type="call"):
d_1 = d1(S, K, T, r, sigma)
if option_type == "call":
return norm.cdf(d_1)
return norm.cdf(d_1) - 1.0
print(f"Call delta: {delta(S, K, T, r, sigma, 'call'):.4f}")
print(f"Put delta : {delta(S, K, T, r, sigma, 'put'):.4f}")
Gamma measures how fast delta moves. It is identical for calls and puts.
$$\Gamma = \frac{\varphi(d_1)}{S\,\sigma\,\sqrt{T}}$$
where $\varphi$ is the standard normal density. Gamma is largest when the option is at the money and small once the option is deep in or deep out of the money. We will see in section 9 that gamma also explodes as $T$ approaches zero.
def gamma(S, K, T, r, sigma):
d_1 = d1(S, K, T, r, sigma)
return norm.pdf(d_1) / (S * sigma * np.sqrt(T))
print(f"Gamma: {gamma(S, K, T, r, sigma):.4f}")
Theta measures the value the option loses with the passage of time, holding everything else constant. It is usually negative for both calls and puts because options are decaying assets.
$$\Theta_{\text{call}} = -\frac{S\,\varphi(d_1)\,\sigma}{2\sqrt{T}} - r\,K\,e^{-rT}\,N(d_2)$$
$$\Theta_{\text{put}} = -\frac{S\,\varphi(d_1)\,\sigma}{2\sqrt{T}} + r\,K\,e^{-rT}\,N(-d_2)$$
The values above are theta per year. To get the daily theta, divide by 365.
def theta(S, K, T, r, sigma, option_type="call"):
d_1 = d1(S, K, T, r, sigma)
d_2 = d2(S, K, T, r, sigma)
first = -S * norm.pdf(d_1) * sigma / (2.0 * np.sqrt(T))
if option_type == "call":
return first - r * K * np.exp(-r * T) * norm.cdf(d_2)
return first + r * K * np.exp(-r * T) * norm.cdf(-d_2)
print(f"Call theta (per year): {theta(S, K, T, r, sigma, 'call'):.4f}")
print(f"Call theta (per day) : {theta(S, K, T, r, sigma, 'call') / 365:.4f}")
Vega measures the change in option price for a 1 percentage point change in implied volatility. It is the same for calls and puts and is always positive.
$$\mathcal{V} = S\,\sqrt{T}\,\varphi(d_1)$$
The expression above gives vega per unit of $\sigma$. Most desks quote vega per 1 percentage point, so divide by 100.
def vega(S, K, T, r, sigma):
d_1 = d1(S, K, T, r, sigma)
return S * np.sqrt(T) * norm.pdf(d_1) / 100.0
print(f"Vega (per 1pp of vol): {vega(S, K, T, r, sigma):.4f}")
The clearest way to build intuition is to plot each Greek against the spot price for a fixed time to expiry. The chart below shows how each one behaves around the strike.
spots = np.linspace(60, 140, 200)
deltas = [delta(s, K, T, r, sigma, "call") for s in spots]
gammas = [gamma(s, K, T, r, sigma) for s in spots]
thetas = [theta(s, K, T, r, sigma, "call") / 365 for s in spots] # per day
vegas = [vega(s, K, T, r, sigma) for s in spots]
fig, axes = plt.subplots(2, 2, figsize=(11, 7))
axes[0, 0].plot(spots, deltas); axes[0, 0].set_title("Delta (call)")
axes[0, 1].plot(spots, gammas); axes[0, 1].set_title("Gamma")
axes[1, 0].plot(spots, thetas); axes[1, 0].set_title("Theta per day (call)")
axes[1, 1].plot(spots, vegas); axes[1, 1].set_title("Vega per 1pp")
for ax in axes.flat:
ax.axvline(K, color="red", linestyle="--", alpha=0.5)
ax.set_xlabel("Spot price")
plt.tight_layout()
plt.show()
This is where most of the intuition about expiry pinning comes from. Drag the slider below and watch the gamma curve. When time to expiry is large, gamma is a smooth, low hill across a wide range of spot prices. As expiry approaches, gamma narrows into a tall spike right at the strike.
The reason is simple. Far from expiry, every cent of stock movement makes the option a little more or a little less likely to finish in the money. Near expiry, only the last few cents around the strike still flip the outcome, so the option price reacts violently in that tiny zone and barely at all everywhere else.
Strike fixed at $K = 100$. Volatility 25%. Risk-free rate 5%. Move the slider toward 0 to see the gamma spike.
The same effect, written in Python:
spots = np.linspace(60, 140, 400)
maturities = [1.0, 0.5, 0.25, 0.10, 0.02]
plt.figure(figsize=(9, 5))
for T_i in maturities:
g = [gamma(s, K, T_i, r, sigma) for s in spots]
plt.plot(spots, g, label=f"T = {T_i:.2f}")
plt.axvline(K, color="red", linestyle="--", alpha=0.5)
plt.title("Gamma vs Spot Price for several expiries")
plt.xlabel("Spot price")
plt.ylabel("Gamma")
plt.legend()
plt.show()
Pair this notebook with the interactive Greeks calculator to play with all five inputs at once.