σμ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.5·~50 minIntermediate

Yield curves and interpolation

Skill positionModule 4 of 11 · Topic 5 of 16
4.5 Yield curves and interpolation
—Lab score— / 100Total XP—Streak—

One rate per maturity

So far one yield discounted every payment of a bond. In reality money lent for 6 months, 2 years and 10 years earns different rates, and the pattern across maturities is the yield curve. A zero rate is the annual rate for a single payment at a given date, with no coupons in between. Usually the curve rises with maturity (investors want more for waiting longer), but it can be flat or inverted, when short rates exceed long ones, which markets read as a warning about growth.

import numpy as np

maturities = np.array([0.5, 1, 2, 5, 10, 30])                      # years where the market quotes a rate
rates = np.array([0.040, 0.042, 0.045, 0.048, 0.050, 0.052])       # an upward-sloping curve

# The rate at 3 years is not quoted: interpolate linearly between 2 years (4.5%) and 5 years (4.8%)
print(float(np.interp(3, maturities, rates)))                        # 0.046
print(np.interp([0.25, 7, 40], maturities, rates).tolist())          # [0.04, 0.0488, 0.052]   flat outside the quoted range

Discount factors and the forward rate

A zero rate r for maturity t gives the discount factor (1 + r) ** -t: what one unit paid at t is worth today. The curve also hides expectations. Lending for 2 years at 4.5% should equal lending for 1 year at 4.2% and then lending again for a year at an agreed rate f: (1 + 4.5%)² = (1 + 4.2%)(1 + f), so f = 4.80%. That forward rate is the break-even rate for the second year, locked in today. In general f = ((1 + r2) ** t2 / (1 + r1) ** t1) ** (1 / (t2 - t1)) - 1.

def discount_factor(rate, t):
    return (1 + rate) ** -t

print(round(float(discount_factor(0.045, 2)), 5))                  # 0.91573   one unit paid in 2 years

r1, t1, r2, t2 = 0.042, 1, 0.045, 2
forward = ((1 + r2) ** t2 / (1 + r1) ** t1) ** (1 / (t2 - t1)) - 1
print(round(forward, 5))                                            # 0.04801   the rate for year 2, implied by the curve
print(round((1 + r1) ** t1 * (1 + forward) ** (t2 - t1), 5), round((1 + r2) ** t2, 5))   # 1.09 1.09   both routes agree

Pricing on the whole curve

Pricing with a single yield treats a 1-year and a 10-year payment alike. The curve approach is more faithful: for each payment read the zero rate of its own date (interpolating if needed), turn it into a discount factor and sum flow * factor. Two bonds with the same cash flows must have the same price whatever their quoted yields, and this is how banks mark bonds to market. For a 3-year 5% bond on our curve the price is 1011.25, versus 1010.98 with a flat 4.6%: close here because the curve is gentle, but different bonds react differently when the curve twists.

times = np.array([1.0, 2.0, 3.0])
flows = np.array([50.0, 50.0, 1050.0])                              # a 3-year 5% bond with annual coupons

zero = np.interp(times, maturities, rates)
print(zero.tolist())                                                 # [0.042, 0.045, 0.046]
print((discount_factor(zero, times)).round(5).tolist())              # [0.95969, 0.91573, 0.87379]
print(round(float((flows * discount_factor(zero, times)).sum()), 4))   # 1011.2462   on the curve
print(round(float((flows / 1.046 ** times).sum()), 4))               # 1010.9752   with one flat 4.6% instead
# The forward rates between the quoted maturities, in one array expression
t = np.array([1.0, 2.0, 5.0])
r = np.array([0.042, 0.045, 0.048])
forwards = ((1 + r[1:]) ** t[1:] / (1 + r[:-1]) ** t[:-1]) ** (1 / np.diff(t)) - 1
print(forwards.round(5).tolist())       # [0.04801, 0.05]   year 2 and years 3 to 5: an upward-sloping curve implies rising forwards
Interpolation is a modelling choiceLinear interpolation of zero rates is the simplest rule, but it is not the only one: practitioners also interpolate log discount factors or use splines, and the choice changes forward rates and prices between the quoted points. Beyond the last quoted maturity np.interp keeps the rate flat, which is a convention, not a fact. State the method whenever you publish a curve.
  • A yield curve is the zero rate per maturity; read missing maturities by interpolation (np.interp, flat outside the range).
  • Discount factor (1 + r) ** -t; forward rate ((1 + r2) ** t2 / (1 + r1) ** t1) ** (1 / (t2 - t1)) - 1.
  • Price = the sum of flow * discount factor with each flow at the zero rate of its own date; state the interpolation method.

Next up: option payoffs, what a call or a put pays at expiry, the starting point of option pricing.

Lab · what's graded

  • Read a rate between the quoted maturities with interpolate_rate25 pts
  • Turn a rate into a discount factor with discount_factor25 pts
  • Read the future rate hidden in the curve with forward_rate25 pts
  • Price a payment stream on the whole curve with price_on_curve25 pts
Explorer · lab-70-1
▾ lab-70-1
…
bash · in-browser
learner@quantcraft:~/lab-70-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-70-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