Chapter 6 — Bonds and Fixed Income Instruments

"The bond market is larger than the stock market and arguably more important to the economy."


After this chapter you will be able to:

  • Price a coupon bond given its yield to maturity and explain why price and yield move in opposite directions
  • Compute Macaulay duration, modified duration, DV01, and convexity and interpret each as a risk measure
  • Explain the difference between clean and dirty price and calculate accrued interest
  • Understand credit spreads and the Z-spread as measures of issuer credit risk
  • Price floating rate notes and understand their near-par pricing at coupon reset dates

In September 2008, at the peak of the global financial crisis, the US government issued Treasury bills at near-zero yields. Investors accepted almost no return in exchange for the certainty of getting their money back. At the same moment, corporate bonds with similar maturities were trading at yields of 8%, 10%, or higher, as buyers demanded enormous compensation for the possibility of default. The spread between these yields — perhaps 900 basis points, or nearly 10% per year — was the market's instantaneous assessment of systemic financial distress, translated into a price.

Fixed income markets are where interest rates, credit risk, inflation expectations, and monetary policy are simultaneously expressed in a single number: the yield. The bond market globally exceeds \$100 trillion in outstanding notional, dwarfing equity markets, and its daily fluctuations move mortgage rates, corporate borrowing costs, and pension fund valuations for hundreds of millions of people. Understanding bonds is not optional for a quantitative practitioner — it is foundational.

This chapter builds the core bond analytics toolkit from scratch. We start with the fundamental premise that a bond's fair value equals the present value of its future cash flows, work through duration and convexity as tools for measuring interest rate risk, and extend to credit spreads, floating rate notes, and inflation-linked bonds. All of these instruments will re-appear when we bootstrap yield curves in Chapter 7 and price interest rate derivatives in Chapter 8.


6.1 Bond Fundamentals

A bond is a promise by the issuer to pay:

  1. Coupon payments: periodic interest payments (usually semi-annual)
  2. Face value (par): return of principal at maturity

Key terms:

  • Face / Par value: amount repaid at maturity (\$1,000 typical for US corporate bonds, \$100 for UK gilts and most conventions used in code)
  • Coupon rate: annual interest rate as a percentage of face; fixed for plain-vanilla bonds
  • Maturity: the date on which the issuer repays the face value
  • Yield to maturity (YTM): the single discount rate that equates all future cash flows to the current market price — it is the bond's internal rate of return
  • Clean price: the quoted price, excluding accrued interest since the last coupon
  • Dirty price (full price): the actual settlement amount = clean price + accrued interest

The distinction between clean and dirty prices is purely a market convention, but it matters in practice. If bonds were quoted at dirty prices, the price would jump upward on every coupon payment date as accrued interest resets to zero — creating the appearance of volatility where there is only a mechanical cash flow. Clean prices remove this accounting noise, making it easier to see genuine changes in interest rate or credit sentiment. When you buy a bond mid-coupon period, you pay the previous holder for the portion of the next coupon they have earned but not yet received.


6.2 Bond Pricing

The fundamental principle of bond pricing is no-arbitrage: if you know the market's discount rate for cash flows at each future date, the bond price must equal the present value of all its cash flows at those rates. Any other price would allow a riskless profit by buying the underpriced bond and synthetically replicating it with Treasury strips (or vice versa).

In practice, bond yields are quoted as a single flat rate $y$ that discounts all cash flows equally — a simplification, but one that is standard market convention. Under this assumption, the price of a bond paying semi-annual coupons over $n$ periods is:

$$P = \sum_{t=1}^{n} \frac{C/m}{(1 + y/m)^t} + \frac{F}{(1 + y/m)^n}$$

where:

  • $C$ = annual coupon = $F \cdot c$ ($c$ = coupon rate)
  • $m$ = coupon frequency per year
  • $n$ = total number of coupon periods
  • $F$ = face value
  • $y$ = yield to maturity

Using the annuity formula to sum the geometric series, this simplifies to:

$$P = \frac{C/m}{y/m} \cdot \left[1 - (1 + y/m)^{-n}\right] + F \cdot (1 + y/m)^{-n}$$

Two special cases are worth internalising. When the coupon rate equals the yield ($c = y$), the annuity formula collapses to $P = F$: the bond prices at par. When $c > y$ (coupon is generous relative to prevailing rates), the discounted coupon stream is worth more than par, so $P > F$: a premium bond. When $c < y$, the coupons fall short of what the market now requires, so $P < F$: a discount bond. This inverse relationship between yields and prices — prices fall when yields rise — is the central fact of fixed income risk.

module Bond = struct

  type t = {
    face        : float;
    coupon_rate : float;
    frequency   : int;         (* coupons per year *)
    n_periods   : int;         (* total coupon periods outstanding *)
    day_count   : day_count_convention;
  }

  (** Full bond price given yield (dirty price) *)
  let price { face; coupon_rate; frequency; n_periods; _ } ~yield =
    let m     = float_of_int frequency in
    let y_m   = yield /. m in
    let coupon = face *. coupon_rate /. m in
    let df_n  = (1.0 +. y_m) ** (-. float_of_int n_periods) in
    if Float.abs y_m < 1e-12 then
      coupon *. float_of_int n_periods +. face
    else
      coupon /. y_m *. (1.0 -. df_n) +. face *. df_n

  (** Par coupon rate: the coupon rate that prices the bond at par *)
  let par_coupon_rate { face = _; frequency; n_periods; _ } ~yield =
    let m   = float_of_int frequency in
    let y_m = yield /. m in
    let df_n = (1.0 +. y_m) ** (-. float_of_int n_periods) in
    let annuity = (1.0 -. df_n) /. y_m in
    y_m *. m /. (annuity *. m)  (* simplifies to: yield *)
    (* A bond priced at par: coupon rate = yield *)

  (** Yield to maturity via Newton-Raphson *)
  let yield { face; coupon_rate; frequency; n_periods; _ } ~market_price =
    let m = float_of_int frequency in
    let coupon = face *. coupon_rate /. m in
    let f y =
      let y_m   = y /. m in
      let df_n  = (1.0 +. y_m) ** (-. float_of_int n_periods) in
      (if Float.abs y_m < 1e-12 then coupon *. float_of_int n_periods +. face
       else coupon /. y_m *. (1.0 -. df_n) +. face *. df_n)
      -. market_price
    in
    let f' y =
      let y_m   = y /. m in
      let g = float_of_int n_periods in
      let df_n  = (1.0 +. y_m) ** (-. (g +. 1.0)) in
      (* Derivative of price w.r.t. yield *)
      -. (coupon *. g *. df_n /. m +. face *. g /. m *. df_n)
    in
    newton_raphson ~f ~f' coupon_rate

  (** Current yield: annual coupon / market price *)
  let current_yield { face; coupon_rate; _ } ~market_price =
    face *. coupon_rate /. market_price

The price function correctly handles the degenerate case of near-zero yield (where the annuity formula would divide by zero) by returning the sum of undiscounted cash flows. The Newton-Raphson solver for yield needs a good initial guess; using the coupon rate is effective because the yield is usually close to the coupon rate for recently issued bonds. For distressed bonds trading far from par, a starting guess of (annual_coupon + (face - price) / years) / ((face + price) / 2) — the approximate yield formula — can be more robust.

Worked example. Consider a 10-year bond with face value \$100, 5% semi-annual coupon rate, and a yield of 6%. Here $C/m = 2.50$, $y/m = 3%$, $n = 20$ periods:

$$P = \frac{2.50}{0.03}\left[1 - (1.03)^{-20}\right] + \frac{100}{(1.03)^{20}} = 2.50 \times 14.877 + 55.37 = 37.19 + 55.37 = 92.57$$

The bond trades at a discount (below par) because the coupon rate (5%) is below the prevailing yield (6%). A buyer at $92.57 who holds to maturity earns the 5% coupon stream plus a capital gain of $7.43 as the price pulls to par — these two together deliver a 6% yield. This pull-to-par effect makes discount bonds accumulate capital gains and premium bonds accumulate capital losses as they approach maturity, which has important tax and accounting implications.

Duration: From Macaulay to Modified

Duration is one of the most important concepts in fixed income, and it exists in two forms that are often confused.

Macaulay duration is defined as the weighted-average time to receive the bond's cash flows, where each weight is the present value of that cash flow as a fraction of the total bond price:

$$D_{\text{Mac}} = \frac{1}{P} \sum_{t=1}^{n} t \cdot \frac{CF_t}{(1 + y/m)^t}$$

This has a beautiful physical interpretation: it is the centre of gravity of the bond's cash flow stream on a time axis. A zero-coupon bond has $D_{\text{Mac}}$ exactly equal to its maturity — all the cash is at one point in time. A coupon bond has $D_{\text{Mac}}$ less than its maturity — the coupons pull the centre of gravity back towards the present.

However, Macaulay duration is not directly the interest rate sensitivity measure that practitioners need. Modified duration is the percentage price change per unit change in yield:

$$D_{\text{mod}} = -\frac{1}{P} \frac{dP}{dy} = \frac{D_{\text{Mac}}}{1 + y/m}$$

The bridge between Macaulay and modified duration comes from differentiating the bond pricing formula with respect to $y$ and simplifying. The factor $(1 + y/m)$ in the denominator arises because the bond pays semi-annual coupons: the yield $y$ is a nominal annual rate, but the compounding is per period. The practical relationship is:

$$\frac{\Delta P}{P} \approx -D_{\text{mod}} \cdot \Delta y$$

A 10-year par bond with 5% semi-annual coupons has a Macaulay duration of approximately 7.99 years and a modified duration of approximately 7.79 years (at 5% yield). This means a 1% (100bp) increase in yield will cause approximately a 7.79% decline in price — from \$100 to approximately \$92.21.

DV01 (Dollar Value of a Basis Point) is the simpler operative measure for traders: it is the dollar price change for a 1bp change in yield: $$\text{DV01} = -\frac{dP}{dy} \times 0.0001 \approx D_{\text{mod}} \times P \times 0.0001$$

For a \$100 10-year bond at 7.79 modified duration, DV01 $\approx$ \$100 $\times$ 7.79 $\times$ 0.0001 = $0.0779 per basis point. A trader holding \$100M face value of this bond loses approximately \$77,900 for every 1bp adverse move in yields.

Convexity captures the fact that duration itself changes as yields move. The price-yield curve is not a straight line — it curves upward. When yields fall, prices rise more than the duration approximation predicts (positive convexity surprise). When yields rise, prices fall less than duration predicts. This asymmetry is always favourable for the bond holder and always costly for the bond issuer. The second-order approximation is:

$$\frac{\Delta P}{P} \approx -D_{\text{mod}} \cdot \Delta y + \frac{1}{2} C \cdot (\Delta y)^2$$

where $C$ is convexity. For large yield moves (±100bp), the convexity term can add or save 30–60bp in addition to the duration effect. For option-free bonds, convexity is always positive. For mortgage-backed securities, negative convexity can arise from the homeowner's prepayment option, creating a bond that can lose more than duration suggests when rates rally.

Duration and Convexity Illustration Figure 6.1 — Duration vs Convexity. The linear duration approximation (red dashed line) underestimates the actual bond price for large yield shocks because the true price-yield curve is convex.

  (**
      Macaulay duration:
      D_mac = (1/P) * sum_t [ t * CF_t * DF(t) ]
      Weighted average time to receive cash flows.
  *)
  let macaulay_duration { face; coupon_rate; frequency; n_periods; _ } ~yield =
    let m      = float_of_int frequency in
    let y_m    = yield /. m in
    let coupon  = face *. coupon_rate /. m in
    let p       = price { face; coupon_rate; frequency; n_periods;
                          day_count = Act365 } ~yield in
    let weighted_sum = ref 0.0 in
    for t = 1 to n_periods do
      let df = (1.0 +. y_m) ** (-. float_of_int t) in
      let cf = if t = n_periods then coupon +. face else coupon in
      let time_years = float_of_int t /. m in
      weighted_sum := !weighted_sum +. time_years *. cf *. df
    done;
    !weighted_sum /. p

  (**
      Modified duration: D_mod = D_mac / (1 + y/m)
      Percentage change in price per 1% change in yield:
      ΔP/P ≈ -D_mod · Δy
  *)
  let modified_duration bond ~yield =
    let d_mac = macaulay_duration bond ~yield in
    let m = float_of_int bond.frequency in
    d_mac /. (1.0 +. yield /. m)

  (**
      Dollar duration (DV01): price change per 1 basis point (0.01%) yield change.
      dP/dy evaluated at yield.
      DV01 = -dP/dy * 0.0001
  *)
  let dv01 bond ~yield =
    let dy = 0.0001 in
    let p_up   = price bond ~yield:(yield +. dy) in
    let p_down = price bond ~yield:(yield -. dy) in
    -.  (p_up -. p_down) /. 2.0

  (**
      Convexity: measures the curvature of the price-yield relationship.
      A higher-convexity bond gains more in rallies and loses less in sell-offs.
      
      C = (1/P) * d²P/dy²
      ΔP/P ≈ -D_mod·Δy + (1/2)·C·(Δy)²
  *)
  let convexity { face; coupon_rate; frequency; n_periods; _ } ~yield =
    let m      = float_of_int frequency in
    let y_m    = yield /. m in
    let coupon  = face *. coupon_rate /. m in
    let p       = price { face; coupon_rate; frequency; n_periods;
                          day_count = Act365 } ~yield in
    let sum = ref 0.0 in
    for t = 1 to n_periods do
      let df = (1.0 +. y_m) ** (-. float_of_int t) in
      let cf = if t = n_periods then coupon +. face else coupon in
      let t_f = float_of_int t in
      sum := !sum +. cf *. t_f *. (t_f +. 1.0) *. df
    done;
    !sum /. (p *. m *. m *. (1.0 +. y_m) *. (1.0 +. y_m))

  (** 
      Price change approximation using duration and convexity:
      ΔP ≈ -D_mod · P · Δy + (1/2) · C · P · (Δy)²
  *)
  let price_change_approx bond ~yield ~delta_yield =
    let p    = price bond ~yield in
    let dmod = modified_duration bond ~yield in
    let conv = convexity bond ~yield in
    let dy   = delta_yield in
    -. dmod *. p *. dy +. 0.5 *. conv *. p *. dy *. dy

end

6.3 The Price-Yield Relationship

The price-yield relationship is convex — as yields fall, prices rise at an accelerating rate:

Bond Price vs Yield Curve Figure 6.1 — Price-Yield curves for 2Y, 10Y, and 30Y bonds with 5% semi-annual coupons. Note the convex shape and how longer maturities exhibit greater price sensitivity (steeper slope).

let plot_price_yield_curve bond =
  let yields = Array.init 201 (fun i -> 0.001 +. float_of_int i *. 0.001) in
  let prices = Array.map (fun y -> Bond.price bond ~yield:y) yields in
  Printf.printf "%-8s %-12s %-12s %-12s\n" "Yield" "Price" "D_mod" "DV01";
  Printf.printf "%s\n" (String.make 48 '-');
  Array.iteri (fun i y ->
    if i mod 20 = 0 then begin
      let p    = prices.(i) in
      let dmod = Bond.modified_duration bond ~yield:y in
      let d1   = Bond.dv01 bond ~yield:y in
      Printf.printf "%-8.3f %-12.4f %-12.4f %-12.4f\n"
        (y *. 100.0) p dmod d1
    end
  ) yields

let example_bond = Bond.{
  face        = 100.0;
  coupon_rate = 0.05;    (* 5% coupon *)
  frequency   = 2;       (* semi-annual *)
  n_periods   = 20;      (* 10-year maturity *)
  day_count   = Thirty360;
}

Key observations:

  • When yield = coupon rate (5%), price = par (100) — by construction of the annuity formula
  • When yield < coupon rate, price > par (premium bond): the coupon stream is worth more than prevailing rates justify, so the price is bid up above par
  • When yield > coupon rate, price < par (discount bond): investors demand a compensating capital gain
  • The price-yield curve is convex: it curves upward, meaning prices fall less steeply as yields rise than they rise when yields fall by the same amount. This asymmetry is favourable for long bond holders and is quantified by the convexity measure
  • Duration decreases as yield increases: higher yields cause investors to value nearer cash flows proportionally more, shortening the effective maturity

6.4 Zero-Coupon Bonds

A zero-coupon bond pays only the face value at maturity, with no intermediate coupons. It is the purest possible instrument: a single cash flow on a known date. Its price is simply the present value of that cash flow:

$$P_{\text{zero}} = \frac{F}{(1 + y/m)^{n}} = F \cdot DF(t)$$

Zero-coupon bonds are theoretically fundamental because any coupon bond can be decomposed as a portfolio of zeros (one per cash flow date). In practice, US Treasury STRIPS (Separate Trading of Registered Interest and Principal of Securities) are exact zero-coupon instruments created by stripping coupon bonds. They are used to build the discount factor curve, as we see in Chapter 7.

let zero_coupon_price ~face ~yield ~maturity ~frequency =
  let m = float_of_int frequency in
  let n = maturity *. m in
  face /. (1.0 +. yield /. m) ** n

let zero_coupon_yield ~price ~face ~maturity ~frequency =
  let m = float_of_int frequency in
  let n = maturity *. m in
  m *. ((face /. price) ** (1.0 /. n) -. 1.0)

let zero_mac_duration maturity = maturity  (* always equals maturity *)

Zero-coupon bonds have three notable properties. First, their Macaulay duration equals their maturity exactly — since there is only one cash flow, the weighted-average time to receive it is the maturity itself. Second, they have no reinvestment risk: a coupon bond's actual yield depends on the rate at which coupons are reinvested, but a zero has no intermediate cash flows to reinvest. Third, they have maximum interest rate sensitivity for their maturity: since all the value is locked up in the terminal payment, a change in yields has the largest proportional effect on price.


6.5 Credit Spreads and the Z-Spread

Treasury bonds are considered risk-free: the US government has never defaulted on its obligations and can in principle create dollars to repay them. Corporate and sovereign bonds from other issuers carry credit risk — the chance that the issuer defaults before maturity — and investors demand a higher yield to compensate. This extra yield above risk-free rates is the credit spread.

The simplest measure of credit spread is the yield spread: the difference between the bond's YTM and the Treasury yield of the same maturity. This is intuitive but imprecise, because it uses a single benchmark yield and ignores the shape of the curve. A more precise measure is the Z-spread (zero-volatility spread), which finds the constant spread $z$ that, when added to every point on the risk-free zero-coupon yield curve, prices the bond exactly:

$$P = \sum_{t} \frac{CF_t}{(1 + (r_t + z)/m)^t}$$

(**
    Compute z-spread given a discount function and bond price.
    discount: tau -> DF (the risk-free discount factor)
*)
let z_spread { Bond.face; coupon_rate; frequency; n_periods; _ } ~market_price ~discount =
  let m = float_of_int frequency in
  let coupon = face *. coupon_rate /. m in
  let f z =
    let sum = ref 0.0 in
    for t = 1 to n_periods do
      let tau = float_of_int t /. m in
      let rf_df = discount tau in
      (* Augment the risk-free df by the spread *)
      let rf_yield = -. log rf_df /. tau in
      let df = exp (-. (rf_yield +. z) *. tau) in
      let cf = if t = n_periods then coupon +. face else coupon in
      sum := !sum +. cf *. df
    done;
    !sum -. market_price
  in
  match brent ~f (-. 0.05) 0.20 with
  | Converged z -> z
  | FailedToConverge { last; _ } -> last

6.6 Floating Rate Notes

Floating rate notes (FRNs) pay a coupon linked to a reference rate (LIBOR, SOFR) plus a spread:

$$\text{Coupon}_t = (R_t + s) \cdot \tau_t \cdot F$$

where $R_t$ is the reset rate, $s$ is the spread, and $\tau_t$ is the day fraction.

Key insight: at each reset date, a FRN with zero spread prices at par. The current price is:

$$P_{\text{FRN}} = F + \text{accrued spread PV}$$

(**
    FRN price assuming flat forward curve.
    This is the exact formula for next-coupon pricing.
*)
let frn_price ~face ~spread ~maturity ~frequency ~discount =
  let m = float_of_int frequency in
  let n = int_of_float (Float.round (maturity *. m)) in
  let spread_coupon = face *. spread /. m in
  (* PV of spread payments only; floating leg prices at par *)
  let spread_pv = ref 0.0 in
  for t = 1 to n do
    let tau = float_of_int t /. m in
    spread_pv := !spread_pv +. spread_coupon *. discount tau
  done;
  (* At par for zero spread; adjust for spread *)
  let df_maturity = discount maturity in
  face *. df_maturity +. !spread_pv

6.7 Inflation-Linked Bonds

An inflation-linked bond (ILB, TIPS in the US) adjusts its principal by the Consumer Price Index (CPI). The real yield $r$ and inflation rate $\pi$ combine via:

$$(1 + y_{\text{nominal}}) = (1 + r_{\text{real}}) \cdot (1 + \pi)$$

$$r_{\text{real}} \approx y_{\text{nominal}} - \pi \quad \text{(Fisher equation)}$$

(** 
    Index ratio: current CPI / base CPI 
    Adjusts the face value and all cash flows 
*)
let index_ratio ~current_cpi ~base_cpi = current_cpi /. base_cpi

let tips_price ~real_yield ~coupon_rate ~n_periods ~frequency ~index_ratio =
  let nominal_face = 1000.0 in
  let adjusted_face = nominal_face *. index_ratio in
  let bond = Bond.{ face = adjusted_face; coupon_rate; frequency;
                    n_periods; day_count = ActAct_ICMA } in
  Bond.price bond ~yield:real_yield

6.8 Portfolio of Bonds

type bond_position = {
  bond     : Bond.t;
  notional : float;    (* face value held *)
  yield    : float;    (* current yield *)
}

type fixed_income_portfolio = bond_position list

let portfolio_dv01 positions =
  List.fold_left (fun acc { bond; notional; yield } ->
    let dv01_per_face = Bond.dv01 bond ~yield in
    acc +. dv01_per_face *. notional /. bond.Bond.face
  ) 0.0 positions

let portfolio_market_value positions =
  List.fold_left (fun acc { bond; notional; yield } ->
    let price_pct = Bond.price bond ~yield in
    acc +. price_pct /. 100.0 *. notional
  ) 0.0 positions

6.9 Chapter Summary

A bond is a package of future cash flows, and its fair price is their present value. This simple principle generates a rich set of analytics. The yield to maturity is the bond's compressed risk descriptor — a single number that captures the entire price level, enabling comparison across bonds of different coupons and maturities. But it obscures the term structure: two bonds with the same YTM but different maturity profiles have very different interest rate risk, which is where duration and convexity come in.

Duration is the first-order interest rate sensitivity: a bond with modified duration 7 years loses roughly 7% of its value for every 1% rise in yields. Convexity is the second-order correction: because the price-yield curve is convex, the duration approximation overestimates losses (and underestimates gains), and convexity adds back this positive curvature benefit. Together, duration and convexity form a second-order Taylor expansion of the price-yield relationship that is accurate for yield moves up to roughly 100bp.

DV01 is the practitioner's risk metric: the dollar change in portfolio value for a 1 basis point yield change. It is additive across positions and enables direct comparison of risk across instruments of different sizes, maturities, and coupons. A portfolio manager who wants interest rate exposure proportional to a given benchmark will match DV01s.

Beyond vanilla bonds, the framework extends to zero-coupon instruments (pure discount factors), floating rate notes (which reset to par at each coupon date and are priced primarily on credit spread), inflation-linked bonds (where the principal accretes with CPI), and credit instruments characterised by their Z-spread above the risk-free curve. Chapter 7 shows how to build the full zero-coupon yield curve from market bond prices — the curve used in all subsequent pricing.


Exercises

6.1 Price a 10-year 6% semi-annual coupon bond (face = \$1000) at yields of 5%, 6%, and 7%. Verify that when yield = coupon rate, price = face value.

6.2 Compute Macaulay duration, modified duration, DV01, and convexity for the bond in 6.1 at a yield of 5%. Verify the duration-convexity approximation vs actual price change for a 100bp yield shift.

6.3 A portfolio manager has a \$10M DV01 exposure. Price a 5-year Treasury at par (4% coupon, 4% yield) and compute how many face value of bonds to sell to reduce DV01 by $50,000.

6.4 Bootstrap a z-spread for a BBB corporate bond trading at 96.50 with a 5% coupon, 5-year maturity, given a flat Treasury curve at 4.2%.


Next: Chapter 7 — The Yield Curve