Exercise · m11_bonds.py
Bond pricing, yield to maturity, duration and convexity
not run yet
Not saved: sign up to keep your code"""M11 exercise: bond pricing, yield to maturity, duration and convexity.
Read notes.md §11.1–11.4 first. Run:
cd exercises && .venv/bin/python -m pytest modules/M11-macro-rates-fx/tests/test_m11_bonds.py
Conventions (used throughout):
* face value F, annual coupon rate c (decimal), m coupons per year,
maturity T years with N = round(m*T) remaining coupon dates,
yield y quoted as an annual rate compounded m times a year.
* Valuation is on a coupon date (no accrued interest), so cash flows fall
at times t_k = k/m, k = 1..N, and CF_k = F*c/m (+ F at k = N).
* P(y) = sum_k CF_k * (1 + y/m)^(-m t_k) (eq. 11.2)
Use numpy only. Do not use scipy's root finders in yield_to_maturity.
"""
from __future__ import annotations
import numpy as np
def cash_flows(face: float, coupon_rate: float, years: float, freq: int = 2) -> tuple[np.ndarray, np.ndarray]:
"""Return (t, cf): numpy arrays of payment times t_k = k/freq (years) and
cash flows CF_k = face*coupon_rate/freq, with face added to the last one.
N = round(years*freq); raise ValueError if N < 1."""
raise NotImplementedError
def bond_price(face: float, coupon_rate: float, ytm: float, years: float, freq: int = 2) -> float:
"""Price on a coupon date, eq. (11.2): sum_k CF_k (1 + ytm/freq)^(-freq t_k)."""
raise NotImplementedError
def yield_to_maturity(
price: float,
face: float,
coupon_rate: float,
years: float,
freq: int = 2,
tol: float = 1e-12,
max_iter: int = 200,
) -> float:
"""Solve P(y) = price for y (Proposition 11.1: P is strictly decreasing and
convex in y, so the root is unique).
Method: bracket [lo, hi] with P(lo) > price > P(hi), then iterate
Newton steps y <- y - (P(y)-price)/P'(y) with P'(y) = -P(y) D_mod(y),
falling back to bisection whenever Newton leaves the bracket.
Must be accurate to 1e-10 in yield."""
raise NotImplementedError
def macaulay_duration(face: float, coupon_rate: float, ytm: float, years: float, freq: int = 2) -> float:
"""D_mac = sum_k t_k PV(CF_k) / P, in years (eq. 11.4)."""
raise NotImplementedError
def modified_duration(face: float, coupon_rate: float, ytm: float, years: float, freq: int = 2) -> float:
"""D_mod = -(1/P) dP/dy = D_mac / (1 + ytm/freq) (Proposition 11.2)."""
raise NotImplementedError
def convexity(face: float, coupon_rate: float, ytm: float, years: float, freq: int = 2) -> float:
"""C = (1/P) d^2P/dy^2
= (1/P) sum_k t_k (t_k + 1/freq) CF_k (1 + ytm/freq)^(-freq t_k - 2) (eq. 11.8)."""
raise NotImplementedError
def dv01(face: float, coupon_rate: float, ytm: float, years: float, freq: int = 2) -> float:
"""First-order price fall for a +1 bp yield move: P * D_mod * 1e-4 (a positive number)."""
raise NotImplementedError
def approx_price_change(price: float, mod_dur: float, conv: float, dy: float, order: int = 2) -> float:
"""Taylor approximation, eq. (11.9):
order=1: P * (-D_mod dy)
order=2: P * (-D_mod dy + 0.5 C dy^2)
Raise ValueError for any other order."""
raise NotImplementedError
def gordon_price(d1: float, r: float, g: float) -> float:
"""Gordon growth value D1/(r - g). Raise ValueError unless r > g."""
raise NotImplementedError
def gordon_equity_duration(r: float, g: float, kind: str = "modified") -> float:
"""Duration of a Gordon-growth equity (Proposition 11.5).
kind='modified': -(1/P) dP/dr = 1/(r - g)
kind='macaulay': (1 + r)/(r - g) (annual discounting, first dividend at t=1)
Raise ValueError unless r > g, or for an unknown kind."""
raise NotImplementedError
No runs yet
The stub's functions raise
NotImplementedError until you write them. Fill in one, press
Run tests, and watch its tests turn green.Reference solution
Available after your first run, with an account approved for code runs.