SigmaField

Practice Fixed income

Match a liability schedule with bonds

Medium+25 pointsAbout 35 minutesFixed income

The brief

A pension fund must pay its retired members a fixed sum at the end of each of the next six years. It wants to buy bonds today whose coupons and redemptions meet every payment exactly. Then no move in interest rates can leave it short of cash.

The desk can choose from six bonds, one maturing in each year. Each pays its coupon once a year and $100 at maturity.

Your task

  1. Write cash_flow_matrix(bonds, years): a NumPy array with one row per year, from 1 to years, and one column per bond. Row tt holds what one of each bond pays at the end of year tt. Raise ValueError if a maturity is not a whole number of years from 1 to years.
  2. Write matching_holdings(bonds, liabilities): how many of each bond to hold, in the order of bonds, so that the cash flows equal liabilities in every year. Raise ValueError unless exactly one portfolio does this.
  3. Write matching_cost(bonds, liabilities): what that portfolio costs today, at the bonds' prices.
  4. Write short_bonds(bonds, liabilities): the names of the bonds with a negative holding, in the order of bonds. The fund may not sell bonds short, so the desk must know when an exact match needs a short position.

The data

BONDS lists the six bonds with their annual coupon rates, maturities in years and prices per bond. LIABILITIES holds the six payments, in dollars.

Hint: Where to start

Case study 3.3.4 copied a payoff by solving Dh=xD\mathbf{h} = \mathbf{x}, with one row per state and one column per asset. Here each row is a year instead of a state. The price of the portfolio is the dot product of prices and holdings.

Hint: A triangle of cash flows

A bond pays nothing after it matures, so the bonds sorted by maturity give an upper triangular matrix. Back substitution (lesson 3.3.1) solves it from the bottom: the longest bond alone meets the last payment.

Hint: When no single portfolio works

Lesson 3.3.2 says Ax=bA\mathbf{x} = \mathbf{b} has exactly one solution for every b\mathbf{b} when AA is square with full rank. Check np.linalg.matrix_rank before you call solve, which can return nonsense for a singular matrix.