σμ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.3·~45 minIntermediate

Yield to maturity

Skill positionModule 4 of 11 · Topic 3 of 16
4.3 Yield to maturity
—Lab score— / 100Total XP—Streak—

From the price back to the yield

In the market a bond is quoted by its price, but investors compare bonds by yield. The yield to maturity (YTM) is the single discount rate at which the present value of all the bond's cash flows equals its price: the annualised return you earn if you buy now, hold to maturity and every payment is made. For a 10-year bond priced at 925.61 with a 5% coupon, it is 6%. Unlike the price, the YTM has no closed formula, because the unknown sits inside a polynomial of high degree.

import numpy as np

def bond_price(face, coupon_rate, ytm, years, freq=2):
    n = int(round(years * freq))
    times = np.arange(1, n + 1) / freq
    flows = np.full(n, face * coupon_rate / freq)
    flows[-1] += face
    return (flows / (1 + ytm / freq) ** (freq * times)).sum()

price = bond_price(1000, 0.05, 0.06, 10)
print(round(price, 4))                                       # 925.6126   the market price we pretend to observe

# A back-of-the-envelope yield: annual coupon plus the yearly share of the discount, over the average of price and par
coupon = 1000 * 0.05
print(round((coupon + (1000 - price) / 10) / ((1000 + price) / 2), 5))     # 0.05966   close to 6%, but not exact

Searching: bisection

The price falls steadily as the yield rises, so we can search. Take an interval [low, high] that surely contains the answer (0% to 100%). Price the bond at the midpoint: if the price is above the target, the yield must be higher, so move low up to the midpoint; otherwise move high down. Each step halves the interval, so 40 steps shrink it by a factor of a trillion. Bisection is slow compared with other methods, but it cannot fail as long as the answer is inside the interval, which makes it the safe fallback.

low, high = 0.0, 1.0
for step in range(6):
    mid = (low + high) / 2
    price_mid = bond_price(1000, 0.05, mid, 10)
    print(step, round(mid, 5), round(price_mid, 2))
    if price_mid > price:      # priced too high: the yield must be higher
        low = mid
    else:
        high = mid
# 0 0.5 110.38
# 1 0.25 275.86
# 2 0.125 578.47
# 3 0.0625 908.08
# 4 0.03125 1159.97
# 5 0.04688 1024.72      the interval keeps shrinking around 6% (the target price is 925.61)

Searching faster: Newton

Newton's method uses the slope. Define f(y) = bond_price(y) - target; we want f(y) = 0. At the current guess, follow the tangent line down to zero: y_new = y - f(y) / f'(y). The slope can be estimated numerically with a tiny step h: (f(y + h) - f(y - h)) / (2h). Near the answer the number of correct digits roughly doubles each step, so four or five steps suffice where bisection needs forty. Newton can fail if started badly or if the slope is flat, so keep a step limit, and fall back to bisection if it does not converge.

y, h = 0.05, 1e-6                            # start at the coupon rate
for step in range(5):
    f = bond_price(1000, 0.05, y) - price
    slope = (bond_price(1000, 0.05, y + h) - bond_price(1000, 0.05, y - h)) / (2 * h)
    print(step, round(y, 8), round(f, 6))
    y = y - f / slope
# 0 0.05 74.387374
# 1 0.05954347 3.245932
# 2 0.05999903 0.006916
# 3 0.06 0.0
# 4 0.06 0.0        converged to 6% after 3 steps
# Check the answer by pricing the bond at the yield we found
print(round(bond_price(1000, 0.05, y, 10), 4), round(price, 4))      # 925.6126 925.6126   the same price: the yield is right
Yields are comparable, prices are notA yield to maturity assumes you can reinvest every coupon at the same yield and that the issuer pays in full, so it is a promise, not a forecast. A bond trading at a very high yield is usually one the market doubts will be repaid. Compare yields only between bonds of similar risk and maturity.
  • YTM is the discount rate that makes the bond's present value equal its price; it has no closed formula, so it is searched for.
  • Bisection: keep halving [low, high] (price above target: raise low); slow but safe.
  • Newton: y = y - f / slope, with a numerical slope; it converges in a few steps but needs a step limit and a good start.

Next up: duration and convexity, measuring how sensitive a bond's price is to the yield.

Lab · what's graded

  • Estimate the yield on the back of an envelope with approx_ytm25 pts
  • Find the yield by halving the interval with bisect_yield25 pts
  • Find the yield faster with newton_yield25 pts
  • Name the situation of the bond with classify_price25 pts
Explorer · lab-68-1
▾ lab-68-1
…
bash · in-browser
learner@quantcraft:~/lab-68-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

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