SigmaField

Practice Fixed income

Bootstrap a zero curve from par yields

Medium+25 pointsAbout 40 minutesFixed income

The brief

A fixed-income desk values its bonds on a zero curve, built every morning from government par yields. This morning's screen shows six quotes in the order they arrived, and no bond at 4, 6, 8 or 9 years.

A client asks for a price on an off-the-run bond, an old issue that no screen quotes. It pays a 5.25% coupon once a year and has 6.5 years left to run. The desk values it on the curve.

Your task

  1. Write par_curve(quotes): the par yields for years 1, 2, …, NN as a NumPy array, NN being the longest maturity. quotes holds (maturity, par_yield) pairs in any order, and each missing year is filled by linear interpolation. Raise ValueError for no quotes, a repeated maturity, a missing 1-year quote, or a maturity that is not a positive whole number.
  2. Write bootstrap(par_yields): the pair (factors, zeros) of NumPy arrays for years 1 to NN. factors holds the discount factors DnD_n and zeros the zero rates zn=Dn−1/n−1z_n = D_n^{-1/n} - 1, compounded once a year. Raise ValueError if a factor comes out zero or below.
  3. Write discount_factor(t, factors): the factor at any time t from 0 to NN years. Between whole years, interpolate ln⁡Dt\ln D_t linearly, with D0=1D_0 = 1 today. Raise ValueError for a t outside that range: the desk does not extrapolate.
  4. Write bond_value(coupon_rate, maturity, factors, face=100): the value today of a bond paying face * coupon_rate once a year and face at maturity. Coupons fall at maturity, one year before it, and so on, back to the earliest date still after today. Raise ValueError for a maturity of zero or below, or past the end of the curve.

For n≤t≤n+1n \le t \le n + 1, the desk's rule reads, with s=t−ns = t - n:

ln⁡Dt=(1−s)ln⁡Dn+sln⁡Dn+1\ln D_t = (1 - s) \ln D_n + s \ln D_{n+1}

The data

QUOTES in the code holds the six (maturity, par_yield) pairs as they arrived, maturities in years and yields as decimals. Each par bond pays its coupon once a year.

Hint: Where to start

Case study 1.6.4 bootstraps one maturity at a time, shortest first: Dn=1−cn∑t<nDt1+cnD_n = \frac{1 - c_n \sum_{t < n} D_t}{1 + c_n}. It also shows what goes wrong when the quotes are out of order or a year is missing. Fill a gap with np.interp (lesson 1.6.1).

Hint: Between the years

With continuous compounding Dt=e−rttD_t = e^{-r_t t}, so −ln⁡Dt=rtt-\ln D_t = r_t t, the rate times the time (case studies 1.4.6 and 1.6.4). Interpolate ln⁡Dt\ln D_t with np.interp, adding the point (0,0)(0, 0) for today, then take exp to get the factor back.

Hint: Pricing the bond

List the payment dates from maturity back in steps of one year, while they are after today. Discount each payment with the factor for its own date, as case study 1.6.4 prices its par bonds. As a check, a bond paying a quoted par yield over that maturity is worth exactly face.