σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 4 · Fixed income & options pricing›Topic 4.12·~45 minIntermediate

Theta and rho

Skill positionModule 4 of 11 · Topic 12 of 16
4.12 Theta and rho
—Lab score— / 100Total XP—Streak—

Two more ways the price moves

Delta, gamma and vega cover the stock and its volatility. Two inputs of the Black–Scholes formula are left: the time to expiry and the interest rate. Theta is the change of an option's value as time passes (the formula's t shrinks every day), and rho is the change when interest rates move. Theta is usually the one a trader feels daily: a long option is a wasting asset, and the premium it loses each day is the price of owning the convexity.

import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def norm_pdf(x):
    return math.exp(-0.5 * x * x) / math.sqrt(2 * math.pi)

def d1(spot, strike, rate, sigma, t):
    return (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))

def d2(spot, strike, rate, sigma, t):
    return d1(spot, strike, rate, sigma, t) - sigma * math.sqrt(t)

def call_theta(spot, strike, rate, sigma, t):
    decay = -spot * norm_pdf(d1(spot, strike, rate, sigma, t)) * sigma / (2 * math.sqrt(t))
    carry = -rate * strike * math.exp(-rate * t) * norm_cdf(d2(spot, strike, rate, sigma, t))
    return decay + carry

theta = call_theta(100, 100, 0.05, 0.2, 1)
print(round(theta, 4))             # -6.414   per year
print(round(theta / 365, 4))       # -0.0176  per calendar day

Theta: why time hurts

The first term of theta, -S * pdf(d1) * sigma / (2 * sqrt(T)), is the decay of the *option value*: with less time there is less chance of a big move. The second, -r * K * exp(-rT) * N(d2), is the interest on the strike you do not have to pay yet, which the call buyer earns. Both are negative for a call, so theta is, and the loss is not steady: it speeds up as expiry nears, fastest for options at the money. Traders quote it per calendar day, theta / 365.

# Theta per day for the same option as expiry approaches: the decay speeds up
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def norm_pdf(x):
    return math.exp(-0.5 * x * x) / math.sqrt(2 * math.pi)

def call_theta(spot, strike, rate, sigma, t):
    d_1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    d_2 = d_1 - sigma * math.sqrt(t)
    return -spot * norm_pdf(d_1) * sigma / (2 * math.sqrt(t)) - rate * strike * math.exp(-rate * t) * norm_cdf(d_2)

for days in (360, 90, 30, 7):
    per_day = call_theta(100, 100, 0.05, 0.2, days / 365) / 365
    print(days, "days left:", round(per_day, 4))     # 360: -0.0177, 90: -0.0288, 30: -0.045, 7: -0.0858

Rho: the rate

Rho is K * T * exp(-rT) * N(d2): positive for a call, because a higher rate lowers the present value of the strike you will pay. A put has the opposite sign. It is the smallest Greek for short-dated options and matters for long ones (LEAPS, long-dated warrants). Quote it for one percentage point of rate, rho / 100, just as vega is quoted per volatility point.

# Rho of the one-year call at several rates: it grows with the rate and with the time left
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def call_rho(spot, strike, rate, sigma, t):
    d_1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    return strike * t * math.exp(-rate * t) * norm_cdf(d_1 - sigma * math.sqrt(t))

for rate in (0.0, 0.05, 0.10):
    print("rate", rate, "rho per point:", round(call_rho(100, 100, rate, 0.2, 1) / 100, 4))   # 0.0: 0.4602, 0.05: 0.5323, 0.1: 0.5931
print("two years, 5%:", round(call_rho(100, 100, 0.05, 0.2, 2) / 100, 4))                  # 1.0568
# Check theta and rho by bumping the inputs: they are slopes of the price
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def bs_call(spot, strike, rate, sigma, t):
    d_1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    return spot * norm_cdf(d_1) - strike * math.exp(-rate * t) * norm_cdf(d_1 - sigma * math.sqrt(t))

h = 1e-5
theta = -(bs_call(100, 100, 0.05, 0.2, 1 + h) - bs_call(100, 100, 0.05, 0.2, 1 - h)) / (2 * h)
rho = (bs_call(100, 100, 0.05 + h, 0.2, 1) - bs_call(100, 100, 0.05 - h, 0.2, 1)) / (2 * h)
print(round(theta, 4), round(rho, 4))                    # -6.414 53.2325
Theta pays for gammaA delta-hedged long option earns gamma profit when the stock moves and pays theta every day it does not. For a hedged position the two nearly balance: theta ~ -0.5 * sigma^2 * S^2 * gamma (at zero rate). That is why a trader who is long gamma is, in effect, betting that the stock will move more than the volatility already priced in.
  • Theta is the value lost as time passes: negative for a long call, faster near expiry and at the money; quote it as theta / 365 per day.
  • Rho is the sensitivity to the interest rate: positive for calls, negative for puts, largest for long-dated options; quote it as rho / 100 per point.
  • Both are slopes of the price, so a small bump of t or rate checks any formula.

Next up: implied volatility, running the formula backwards to read the market's volatility from an option price.

Lab · what's graded

  • Price the passing of time with call_theta25 pts
  • Quote the daily decay with theta_per_day25 pts
  • Measure the sensitivity to interest rates with call_rho25 pts
  • Quote rho per percentage point with rho_per_point25 pts
Explorer · lab-77-1
▾ lab-77-1
…
bash · in-browser
learner@quantcraft:~/lab-77-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-77-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub