Introduction

These study notes are based on learning objectives B1 and B2 of the Exam 9 syllabus, containing source material from Chapters 14 - 16 of Investments by Bodie, Kane, and Marcus. These notes introduce core concepts related to bond pricing and interest rates.

easypackages::packages("dplyr", "ggplot2", "DT", "data.table")
options(scipen = 999)

Yields of Coupon Bonds

Terminology

  • The par value or face value is the amount paid to the bondholder at the maturity date. This is the principal of the bond.

  • The coupon rate determines the interest payment on the bond: it is multiplied by the par value to determine the annual amount of the coupon, which may be paid annually or semi-annually.

  • The price of a bond is the amount at which it may be bought or sold. This may differ from the par value, most notably for zero coupon bonds, in which the entirety of the interest is accumulated through the difference between the price and par value of the bond.

    • A premium bond has a price that exceeds its par value. All else equal, these will see capital losses as the bond matures.

    • A discount bond has a price that is less than its par value. All else equal, these will see capital gains as the bond matures.

  • The yield-to-maturity is the internal rate of return for the bond; in other words, it is the interest rate that makes the present value of the bond payments equal to its price. In the following, this is denoted by \(y\).

  • The current yield of a bond is ratio of the annual coupon payment to the price.

    • For premium bonds, this is less than the coupon rate but greater than the yield to maturity.

    • For discount bonds, this is greater than the coupon rate but less than the yield to maturity.

Bond Price

The general approach to determining the price of a bond, given a set of interest rates at each maturity, is:

  1. Calculate the coupon paid in each time period

  2. Discount each coupon to present value, using the interest rate corresponding to the time at which the coupon is paid

  3. Discount the par value to present value, using the interest rate corresponding to the maturity of the bond

  4. The sum of steps 2 and 3 is the price of the bond.

Flat Yield Curve

In the special case of a flat yield curve, the interest rate is constant, so in step 2, the present value of the coupon payments is equal to the present value of an annuity: \[ C \frac{v - v^{n+1}}{1-v} \] where

  • \(C\) is the amount of the coupon

  • \(v\) is the discount factor. For annual payments and a fixed interest rate of \(y\), this is \(v = \frac{1}{1+y}\). For semi-annual payments, \(v = \frac{1}{1 + y/2}\). (To be more precise, and to reflect compounding interest, we should use \(v = \frac{1}{\sqrt{1+y}}\). However, using \(y/2\) as the interest rate is more consistent with the way that the coupon is split.)

This function can be used to calculate the price of a bond when the yield curve is flat:

flat.yield.bond.price = function(par.value, maturity, coupon.rate, interest.rate, semiannual = TRUE) {
  payment.count = ifelse(semiannual, 2 * maturity, maturity)
  discount.factor = ifelse(semiannual, 1 / (1 + interest.rate / 2), 1 / (1 + interest.rate))
  coupon.payment = ifelse(semiannual, coupon.rate * par.value / 2, coupon.rate * par.value)
  PV.coupons = coupon.payment * (discount.factor - discount.factor^(payment.count + 1)) / (1 - discount.factor)
  PV.par.value = par.value * discount.factor^payment.count
  return(PV.coupons + PV.par.value)
}

Replicating the example from the textbook:

flat.yield.bond.price(par.value = 1000, maturity = 30, coupon.rate = 0.08, interest.rate = 0.08, semiannual = TRUE)
## [1] 1000

Provided that the coupon rate equals the interest rate, and the interest rate is split in half for semiannual coupons, then the par value will equal the price. However, this can change if the interest rate increases:

flat.yield.bond.price(par.value = 1000, maturity = 30, coupon.rate = 0.08, interest.rate = 0.10, semiannual = TRUE)
## [1] 810.7071

When the interest rate increases, the price of the bond decreases, because if the prevailing interest rate is 10%, then an 8% coupon bond will be in less demand. Conversely, a decrease in interest rates will result in a capital gain:

flat.yield.bond.price(par.value = 1000, maturity = 30, coupon.rate = 0.08, interest.rate = 0.06, semiannual = TRUE)
## [1] 1276.756

Non-Flat Yield Curve

When the yield curve is not flat, the annuity formula cannot be used and instead, each individual coupon payment must be discounted at the appropriate rate. This is equivalent to viewing each coupon payment as a zero-coupon bond with a different maturity, a process known as stripping. The reverse process, combining zero-coupon bonds of varying maturities to create a synthetic coupon bond, is reconstitution.

The textbook illustrates this approach using the following yield curve:

yield.curve.example = data.frame(maturity = 1:4, yield_to_maturity = c(0.05, 0.06, 0.07, 0.08))
yield.curve.example
##   maturity yield_to_maturity
## 1        1              0.05
## 2        2              0.06
## 3        3              0.07
## 4        4              0.08

As an example, we can calculate the price of a 3-year bond with a par value of $1000 and 10% annual coupons as follows:

non.flat.bond.example = yield.curve.example %>% mutate(coupon_payment = ifelse(maturity <= 3, 0.10 * 1000, 0), par_payment = ifelse(maturity == 3, 1000, 0), PV_payments = (coupon_payment + par_payment) / (1 + yield_to_maturity)^maturity)
non.flat.bond.example
##   maturity yield_to_maturity coupon_payment par_payment PV_payments
## 1        1              0.05            100           0    95.23810
## 2        2              0.06            100           0    88.99964
## 3        3              0.07            100        1000   897.92766
## 4        4              0.08              0           0     0.00000
non.flat.bond.price = sum(non.flat.bond.example$PV_payments)
print(paste0("The price of the bond when the yield curve is not flat is ", non.flat.bond.price))
## [1] "The price of the bond when the yield curve is not flat is 1082.16540381946"

The reverse problem, of finding the yield to maturity in each year given a collection of annual coupon bonds of various maturities, can be calcultaed recursively as follows:

  1. Begin by calculating the yield in the first year by dividing the face value by the price plus the final coupon, and subtracting 1.

  2. For each subsequent year, discount all coupons except the last one to present value using the yields that have been previously calculated.

  3. Subtract the present value of coupons from the price of the bond plus the final coupon.

  4. Divide the face value by the price net of coupons, take the root corresponding to the maturity, and subtract 1, to get the yield corresponding to the maturity of the bond.

The spreadsheet accompanying the textbook illustrates the process using the following list of coupon rates. Assume that all bonds sell for par value, and that coupons are annual.

BKM.yield.example = read.csv("./Data/BKM_15.csv")
BKM.yield.example
##    maturity coupon_rate
## 1         1     0.08000
## 2         2     0.07990
## 3         3     0.07800
## 4         4     0.07500
## 5         5     0.07250
## 6         6     0.07150
## 7         7     0.07020
## 8         8     0.07000
## 9         9     0.06825
## 10       10     0.06750
## 11       11     0.06630
## 12       12     0.06540
## 13       13     0.06440
## 14       14     0.06400
## 15       15     0.06350
## 16       16     0.06300
## 17       17     0.06250
## 18       18     0.06200
## 19       19     0.06190
## 20       20     0.06180
spot_rate = rep(NA, 20)
for (i in 1:20) {
  coupon = BKM.yield.example[i, 'coupon_rate']
  discount_rate = 1 / (1 + spot_rate)
  PV_coupons = sum(discount_rate^(1:20) * rep(coupon, 20), na.rm = TRUE)
  spot_rate[i] = ((1 + coupon) / (1 - PV_coupons))^(1 / i) - 1
}
BKM.yield.example$spot_rate = spot_rate
BKM.yield.example
##    maturity coupon_rate  spot_rate
## 1         1     0.08000 0.08000000
## 2         2     0.07990 0.07989601
## 3         3     0.07800 0.07784576
## 4         4     0.07500 0.07453740
## 5         5     0.07250 0.07175958
## 6         6     0.07150 0.07069942
## 7         7     0.07020 0.06922658
## 8         8     0.07000 0.06909584
## 9         9     0.06825 0.06692393
## 10       10     0.06750 0.06605798
## 11       11     0.06630 0.06454969
## 12       12     0.06540 0.06343700
## 13       13     0.06440 0.06215733
## 14       14     0.06400 0.06173019
## 15       15     0.06350 0.06112582
## 16       16     0.06300 0.06049786
## 17       17     0.06250 0.05984835
## 18       18     0.06200 0.05917900
## 19       19     0.06190 0.05915909
## 20       20     0.06180 0.05912181

Yield to Maturity

The yield to maturity is the internal rate of return on the bond. To determine it, find the roots of the polynomial corresponding to the present value of cash flows equalling zero. To define this polynomial, let:

  • \(P\) be the price of the bond

  • \(F\) be the face value of the bond

  • \(C\) be the coupon payment of the bond

  • \(n\) be the number of coupon payments

  • \(v\) be the discount factor

Then \[ 0 = -F + \sum_{1\leq i \leq n} C v^i + Fv^n \] Solving for \(v = \frac{1}{1+r}\), we can then determine the yield to maturity \(r\) as \[ r = \frac{1}{v} - 1 \] At this point, if the coupons were semi-annual, double the result to get the annual interest rate.

The following R function can calculate the yield to maturity by setting up a vector correpsonding to the cash flows, finding the roots of the polynomial, identifying the unique real root, and converting it to a yield-to-maturity. Note that when setting up the cash flows, the final coupon payment is made at the same time as the principal. (The “sensitivity” parameter is needed because, due to numerical approximation, the real root may have a very small complex part. This parameter determines the number of digits to round to prior to checking for realness; if this process does not produce a real root, then decrease the sensitivity.)

yield.to.maturity = function(price, coupon, maturity, par.value, semiannual = TRUE) {
  number.of.payments = ifelse(semiannual, maturity * 2, maturity)
  cash.flow = c(-price, rep(coupon, number.of.payments - 1), coupon + par.value)
  roots = polyroot(cash.flow)
  discount.factor = Re(roots[round(Im(roots), 8) == 0])
  interest.rate = ifelse(semiannual, 2 * (1 / discount.factor - 1), 1 / discount.factor - 1)
  return(interest.rate)
}

Continuing the earlier example of the 30-year $1000 bond with 10% annual coupons, if this bond sells for $1276.76 then its yield-to-maturity is as follows:

yield.to.maturity(price = 1276.76, coupon = 40, maturity = 30, par.value = 1040, semiannual = TRUE)
## [1] 0.06040077

This is consistent with the earlier example, where the price was determined based on the assumption of an annual interest rate of 6%.

Continuing the example of the bond priced using an non-flat yield curve, its yield-to-maturity is:

yield.to.maturity(price = non.flat.bond.price, coupon = 100, maturity = 3, par.value = 1000, semiannual = FALSE)
## [1] 0.06876155

Compare this to the yield to maturity of the three-year zero-coupon bond:

yield.to.maturity(price = 816.30, coupon = 0, maturity = 3, par.value = 1000, semiannual = FALSE)
## [1] 0.06999907

As expected, the yield on the coupon bond is lower because the earlier coupons correspond to a smaller interest rate. Suppose the coupon rate were 4% instead of 10%:

non.flat.bond.example.v2 = yield.curve.example %>% mutate(coupon_payment = ifelse(maturity <= 3, 0.04 * 1000, 0), par_payment = ifelse(maturity == 3, 1000, 0), PV_payments = (coupon_payment + par_payment) / (1 + yield_to_maturity)^maturity)
non.flat.bond.example.v2
##   maturity yield_to_maturity coupon_payment par_payment PV_payments
## 1        1              0.05             40           0    38.09524
## 2        2              0.06             40           0    35.59986
## 3        3              0.07             40        1000   848.94979
## 4        4              0.08              0           0     0.00000
non.flat.bond.price.v2 = sum(non.flat.bond.example.v2$PV_payments)
print(paste0("The price of the bond when the yield curve is not flat is ", non.flat.bond.price.v2))
## [1] "The price of the bond when the yield curve is not flat is 922.644887662294"

Now, the yield to maturity is

yield.to.maturity(price = non.flat.bond.price.v2, coupon = 40, maturity = 3, par.value = 1000, semiannual = FALSE)
## [1] 0.06944649

With the smaller coupon payment, the yield is closer to the three-year zero-coupon bond because a greater proportion of the bond’s payments are made at maturity.

Treasury Inflation Protected Securities

In an indexed bond, the payments increase based on the value of some index, such as the consumer price index. The principal increases each year, and the coupon payment does as well, since it is a fixed percentage of the principal. An example of an indexed bond is the Treasury Inflation Protected Security (TIPS) which increases at the rate of inflation. The nominal return in a given year is the ratio of the sum of coupon and principal appreciation to the initial principal. The real return is obtained by backing out the effect of inflation from the nominal return. To calculate the price of an indexed bond:

  1. Calculate the principal in each year, adjusting by the change in the index.

  2. Calculate the coupon payment in each year by multiplying the end-of-year principal for that year by the coupon rate.

  3. Discount all cash flows to present value.

Alternately, if the price is given, the cash flows stated above can be used to determine the yield to maturity.

A three-year $1000 par-value bond with a coupon rate of 4% will have the following cash flows if the rate of inflation in the first three years is 2%, 3%, and 1%.

TIPS.example = data.frame(time_period = 0:3, inflation = c(0, 0.02, 0.03, 0.01))
TIPS.example = TIPS.example %>% mutate(cumulative_inflation = cumprod(1 + inflation), principal = 1000 * cumulative_inflation, coupon = ifelse(time_period > 0, 0.04 * principal, 0), nominal_rate = (coupon + principal - lag(principal)) / lag(principal), real_rate = (1 + nominal_rate) / (1 + inflation) - 1)
TIPS.example
##   time_period inflation cumulative_inflation principal   coupon
## 1           0      0.00             1.000000  1000.000  0.00000
## 2           1      0.02             1.020000  1020.000 40.80000
## 3           2      0.03             1.050600  1050.600 42.02400
## 4           3      0.01             1.061106  1061.106 42.44424
##   nominal_rate real_rate
## 1           NA        NA
## 2       0.0608      0.04
## 3       0.0712      0.04
## 4       0.0504      0.04

Note that the nominal rate changes based on inflation, but the real rate is constant. Compare the price of the bond when it is discounted at 4% and 7%:

TIPS.example = TIPS.example %>% mutate(cash_flow = ifelse(time_period == 3, principal + coupon, coupon), PV_4 = cash_flow / 1.04^time_period, PV_7 = cash_flow / 1.07^time_period)
TIPS.example
##   time_period inflation cumulative_inflation principal   coupon
## 1           0      0.00             1.000000  1000.000  0.00000
## 2           1      0.02             1.020000  1020.000 40.80000
## 3           2      0.03             1.050600  1050.600 42.02400
## 4           3      0.01             1.061106  1061.106 42.44424
##   nominal_rate real_rate cash_flow      PV_4      PV_7
## 1           NA        NA     0.000   0.00000   0.00000
## 2       0.0608      0.04    40.800  39.23077  38.13084
## 3       0.0712      0.04    42.024  38.85355  36.70539
## 4       0.0504      0.04  1103.550 981.05214 900.82572
TIPS.price.4 = sum(TIPS.example$PV_4)
print(paste0("The price of the TIPS bond at a discount rate of 4% is ", TIPS.price.4))
## [1] "The price of the TIPS bond at a discount rate of 4% is 1059.13646449704"
TIPS.price.7 = sum(TIPS.example$PV_7)
print(paste0("The price of the TIPS bond at a discount rate of 7% is ", TIPS.price.7))
## [1] "The price of the TIPS bond at a discount rate of 7% is 975.661948192839"

Note that when the discount rate matches the coupon rate, the bond sells at a premium due to the inflation protection. We can also determine the yield to maturity, assuming that the bond is issued at par value:

TIPS.cash.flow = TIPS.example$cash_flow - c(1000, 0, 0, 0)
TIPS.roots = polyroot(TIPS.cash.flow)
TIPS.discount.factor = Re(TIPS.roots[round(Im(TIPS.roots), 8) == 0])
TIPS.ytm = 1 / TIPS.discount.factor - 1
print(paste0("The yield to maturity for the TIPS bond is ", TIPS.ytm))
## [1] "The yield to maturity for the TIPS bond is 0.0609006048517184"

Without inflation protection, the nominal yield to maturity would be 4%.

Realized Compound Return

The yield-to-maturity implicitly assumes that any coupons received can be reinvested at the same rate. The realized compound return incorporates the interest of re-invested coupons into the yield calculation. If interest rates decrease, then this will be lower than the yield-to-maturity since coupons will have to be re-invested at a lower rate – this introduces reinvestment risk, which offsets the risk of capital gains / losses as interest rates change.

To calculate realized compound return:

  1. Calculate the value of all coupons as of the maturity date, using the re-investment rate at the time the coupon is paid.

  2. Add the par value to get the total maturity value of the bond plus reinvested coupons, \(V_f\).

  3. Where \(V_i\) is the initial price of the bond, calculate the realized compound return \(r\) as \(r = (V_f / V_i)^{1/n} - 1\).

The textbook contains an example of a 2-year $1000 par value bond with a coupon rate of 10%, paying annual coupons. Assume the bond sells at par value. Compare the situations in which the reinvestment rate equals the coupon rate, and when it is less / greater than it.

rcr.example = data.frame(time.period = c(1, 2), cash.flow = c(100, 1100))
rcr.example = rcr.example %>% mutate(future_value_8 = 1.08^(2 - time.period)*cash.flow, future_value_10 = 1.10^(2 - time.period)*cash.flow, future_value_12 = 1.12^(2 - time.period)*cash.flow)
rcr.example
##   time.period cash.flow future_value_8 future_value_10 future_value_12
## 1           1       100            108             110             112
## 2           2      1100           1100            1100            1100
rcr.8 = (sum(rcr.example$future_value_8) / 1000)^(1/2) - 1
print(paste0("The realized compound return at a reinvestment rate of 8% is ", rcr.8))
## [1] "The realized compound return at a reinvestment rate of 8% is 0.099090533122727"
rcr.10 = (sum(rcr.example$future_value_10) / 1000)^(1/2) - 1
print(paste0("The realized compound return at a reinvestment rate of 10% is ", rcr.10))
## [1] "The realized compound return at a reinvestment rate of 10% is 0.1"
rcr.12 = (sum(rcr.example$future_value_12) / 1000)^(1/2) - 1
print(paste0("The realized compound return at a reinvestment rate of 12% is ", rcr.12))
## [1] "The realized compound return at a reinvestment rate of 12% is 0.100908715561831"

Holding Period Return

The holding period is the sum of coupons and capital gains / losses, divided by the initial price of the bond. When interest rates are constant over the holding period, this is the same as the yield to maturity, but if interest rates change, then it will differ due to capital gains or losses on the bond.

To calculate the holding period return:

  1. Calculate the value of the bond, as of the end of the holding period, using the interest rate as of the end of the period.

  2. Subtract the initial price of the bond and add the coupons earned during the holding period.

  3. Divide by the initial price of the bond.

The change in price of a bond over time can be visualized by calculating the price of the bond at the end of each coupon payment. This example tracks the price of a 30-year bond with par value $1000 and annual coupon payments, under an interest rate of 8%, with coupon rates of 4%, 8%, and 12%.

bond.price.comparison = merge(data.frame(coupon_rate = c(0.04, 0.08, 0.12)), data.frame(time_period = 0:30))
bond.price.comparison$price = apply(bond.price.comparison, 1, function(y) {flat.yield.bond.price(1000, 30 - y['time_period'], y['coupon_rate'], 0.08, semiannual = FALSE)})
ggplot(data = bond.price.comparison, aes(x = time_period, y = price, col = as.factor(coupon_rate))) + geom_line()

The textbook provides an example of calculating the holding period return of a 30-year bond selling at the par value of $1000, with an annual coupon rate of $80. If the initial yield to maturity is 8%, but increases to 8.5% over the course of a year, the one-year holding period return can be calculated as follows:

holding.period.return = (80 + flat.yield.bond.price(1000, 29, 0.08, 0.085, semiannual = FALSE) - 1000) / 1000
print(paste0("The holding period return is ", holding.period.return))
## [1] "The holding period return is 0.0266983722899877"

Term Structure of Interest Rates

Spot Rates, Short Rates, and Forward Rates

The study of the term structure of interest rates relies on distinguishing between different types of rates:

  • Spot Rate: is the yield-to-maturity for a zero-coupon bond that prevails today, for a given maturity. The spot rate for an \(n\)-year bond is denoted by \(y_n\).

  • Short Rate: is the interest rate for a year at different points in time. The short rate in year \(n\) is denoted by \(r_n\).

  • Forward Rate: is the future short rate that is implied by the current yield curve. It may differ from the actual future realization of the short rate and the expected future realization of the short rate.

In general, the spot rate is the geometric average of the short rates over the maturity of the bond. The forward rate can be calculated as \[ 1 + f_n = \frac{(1 + y_n)^n}{(1 + y_{n-1})^{n-1}} \]

The rates shown in this table are the spot rates:

yield.curve.example
##   maturity yield_to_maturity
## 1        1              0.05
## 2        2              0.06
## 3        3              0.07
## 4        4              0.08

Forward rates can be calculated as follows:

yield.curve.example = yield.curve.example %>% mutate(forward_rate = (1 + yield_to_maturity) ^ maturity / (1 + lag(yield_to_maturity, default = 0)) ^{maturity-1} - 1)
yield.curve.example
##   maturity yield_to_maturity forward_rate
## 1        1              0.05   0.05000000
## 2        2              0.06   0.07009524
## 3        3              0.07   0.09028391
## 4        4              0.08   0.11056425

The 4-year spot rate can be obtained by taking the geometric average of the forward rates:

print(paste0("The 4-year spot rate is ", prod((1 + yield.curve.example$forward_rate))^(1/4) - 1))
## [1] "The 4-year spot rate is 0.0800000000000001"

Continuing the example from the textbook spreadsheet, we can calculate the 1-year forward rates for each year as follows:

BKM.yield.example = BKM.yield.example %>% mutate(forward_rate_1 = (1 + lead(spot_rate)) ^ (maturity + 1) / (1 + spot_rate) ^ maturity - 1)
BKM.yield.example
##    maturity coupon_rate  spot_rate forward_rate_1
## 1         1     0.08000 0.08000000     0.07979202
## 2         2     0.07990 0.07989601     0.07375694
## 3         3     0.07800 0.07784576     0.06467312
## 4         4     0.07500 0.07453740     0.06071993
## 5         5     0.07250 0.07175958     0.06541432
## 6         6     0.07150 0.07069942     0.06043196
## 7         7     0.07020 0.06922658     0.06818113
## 8         8     0.07000 0.06909584     0.04970679
## 9         9     0.06825 0.06692393     0.05829590
## 10       10     0.06750 0.06605798     0.04958375
## 11       11     0.06630 0.06454969     0.05127384
## 12       12     0.06540 0.06343700     0.04692088
## 13       13     0.06440 0.06215733     0.05619299
## 14       14     0.06400 0.06173019     0.05270060
## 15       15     0.06350 0.06112582     0.05112294
## 16       16     0.06300 0.06049786     0.04951015
## 17       17     0.06250 0.05984835     0.04786447
## 18       18     0.06200 0.05917900     0.05880078
## 19       19     0.06190 0.05915909     0.05841390
## 20       20     0.06180 0.05912181             NA

Forward Contracts

A forward interest rate contract is an agreement to offer a loan at some point in the future at an interest rate that is agreed upon today. The rate must be the forward rate, or otherwise an arbitrage opportunity exists. This can be illustrated by constructing a synthetic forward loan out of zero-coupon bonds:

  1. Suppose the agreement is to offer a loan of amount \(L\), \(m\) years from now, with a maturity of \(n\) years.

  2. Purchase an \(m\)-year zero-coupon bond with face value \(L\). This costs \(L / (1 + y_m)^m\).

  3. Sell enough \(m+n\)-year zero coupon bonds with face value \(L\) to fully fund the purchase in step 2. Each bond sold generates \(L / (1 + y_{n+m})^{n+m}\) at time 0, so the amount needed is \(\frac{(1 + y_{n+m})^{n+m}}{(1 + y_m)^m}\). As a result, net cash flow at time zero is 0.

  4. At time \(m\), the first zero-coupon bond matures and you receive \(L\).

  5. At time \(m+n\), the bonds sold become due and you pay \(L \frac{(1 + y_{n+m})^{n+m}}{(1 + y_m)^m}\).

As a result, the fair interest rate on the forward loan is \[ \left(\frac{(1 + y_{n+m})^{n+m}}{(1 + y_m)^m}\right)^{1/n} -1 \] If a forward contract were available with a different rate, then a portfolio consisting of opposite positions in the forward contract and synthetic loan would present an arbitrage opportunity.

To illustrate, consider the following example:

  1. The agreement is to offer a loan of $1000, two years from now, with a duration of one year.

  2. Purchase a 2-year zero-coupon bond with a face value of $1000. Using the yield curve in the table above, the cost of this bond is:

synthetic.purchase = 1000 / 1.06^2
synthetic.purchase
## [1] 889.9964
  1. To fund the purchase, we will sell 3-year bonds with the same face value. Each one generates
synthetic.sale = 1000 / 1.07^3
synthetic.sale
## [1] 816.2979

Therefore, the amount sold is

amount.sold = synthetic.purchase / synthetic.sale
amount.sold
## [1] 1.090284
  1. In 2 years, the purchased bond matures and generates $1000.

  2. In 3 years, the sold bonds must be repaid. The amount repaid is

amount.repaid = 1000 * amount.sold
amount.repaid
## [1] 1090.284

Therefore, the interest rate on a forward contract must be

amount.sold - 1
## [1] 0.09028391

A forward contract may involve an agreement to make a loan at a rate \(r\) other than the forward rate. The value of this contract at time zero can be calucalted as follows:

  1. Determine the excess (or deficiency) in the payment at the end of the loan, \(P * (r - f_n)\)

  2. Discount the amount from Step 1 to time zero using \(f_n\) and the yields from earlier years.

Reasons for Term Structure

Expectations Hypothesis

The expectations hypothesis states that the forward rate represents a market consensus expectation of future short rates. In other words, \(E[r_n] = f_n\). Consequences of this hypothesis include:

  • There is no liquidity premium

  • Yields on long-term bonds can be completely derived from expected future short rates.

  • The slope of the yield curve represents investor anticipation of interest rate changes.

Liquidity Preference

Liquidity preference theory is based on the idea that there are two classes of investors:

  1. Short-term investors, who will be unwilling to hold long-term bonds unless the forward rate exceeds the expected short rate, i.e. \(f_n > E[r_n]\)

  2. Long-term investors, who would be unwilling to hold short-term bond unless \(f_n < E[r_n]\).

The theory is based on the idea that short-term investors dominate the market, so the forward rate should exceed the expected short rate by a liquidity premium, equal to \(\ell_n = f_n - E[r_n]\).

The impact of the liquidity premium on the yield curve can be assessed by calculating the yield to maturity under various assumptions about the direction of the expected short rate and liquidity premium. A key consideration when calculating the yield to maturity is that the short rate in the first year is known, and it receives no liquidity premium: \[ (1 + y_n)^n = (1 + r_1) \prod_{2 \leq i \leq n} (1 + f_n) \]

The following function calculates and graphs the yield curve. In these graphs, green represents the yield curve, red represents the forward rate, and blue the expected short rate.

yield.cuvre.graph = function(expected.short.int, expected.short.slope, liquidity.int, liquidity.slope) {
  yield.curve = data.frame(maturity = 1:10)
  yield.curve = yield.curve %>% mutate(expected_short_rate = expected.short.int + maturity * expected.short.slope, liquidity_premium = liquidity.int + maturity * liquidity.slope, forward_rate = expected_short_rate + liquidity_premium, rate_for_calculation = ifelse(maturity == 1, expected_short_rate, forward_rate), ytm = cumprod(1 + rate_for_calculation)^(1 / maturity) - 1)
  return(ggplot(data = yield.curve, aes(x = maturity, y = ytm)) + geom_line(colour = "green") + geom_line(aes(y = expected_short_rate), colour = "blue") + geom_line(aes(y = forward_rate), colour = "red") + ylim(0.04, 0.07) + scale_x_continuous(breaks=seq(0, 10, 1)))
}

In the first scenario, a constant expected short rate and liquidity premium lead to an increasing yield curve, that converges to the (constant) forward rate.

plot(yield.cuvre.graph(expected.short.int = 0.05, expected.short.slope = 0, liquidity.int = 0.01, liquidity.slope = 0))

Even if the short rate is expected to decrease in the future, a liquidity premium can still result in an increasing yield curve:

plot(yield.cuvre.graph(expected.short.int = 0.05, expected.short.slope = -0.0005, liquidity.int = 0.005, liquidity.slope = 0.0015))

If the liquidity premium is small, then a decreasing expected short rate can lead to a hump-shaped yield curve:

plot(yield.cuvre.graph(expected.short.int = 0.055, expected.short.slope = -0.001, liquidity.int = 0.005, liquidity.slope = 0))

Naturally, if both the liquidity premium and expected short rate are increasing, there will be a sharply increasing yield curve:

plot(yield.cuvre.graph(expected.short.int = 0.0425, expected.short.slope = 0.0005, liquidity.int = 0.005, liquidity.slope = 0.001))

Horizon analysis

Horizon analysis involes forecasting the realized compound yield over various holding periods, considering both the capital gains and losses on the bond and the coupon reinvestment rate. Forecast rates may be obtained from the yield curve. The approach involves:

  1. Calculate the price of the bond as of the end of the holding period, using the forecasted interest rate.

  2. Calculate the value of all coupons as of the end of the holding period, using the forecasted reinvestment rate.

  3. Sum the two calculations above, and divide by the price of the bond at the beginning of the holding period.

  4. Where \(n\) is the duration of the holding period, take the \(n\)-th root of the above and subtract 1.

The textbook illustrates the above using an example of a 20-year bond with a 2-year holding period. The bond has a par value of $1000, and 10% annual coupons. The current yield-to-maturity for 20-year bonds is 9%. The two-year forecast for the yield on 18-year maturity bonds is 10%, and the reinvestment rate is 8%. First, determine the current price of the bond:

horizon.example.current.price = flat.yield.bond.price(par.value = 1000, maturity = 20, coupon.rate = 0.10, interest.rate = 0.09, semiannual = FALSE)
horizon.example.current.price
## [1] 1091.285

Calculate the price of the bond at the end of the holding period:

horizon.example.future.price = flat.yield.bond.price(par.value = 1000, maturity = 18, coupon.rate = 0.10, interest.rate = 0.10, semiannual = FALSE)
horizon.example.future.price
## [1] 1000

Calculate the value of the coupons as of the end of the second year:

horizon.example.coupon.value = 1.08 * 1000 * 0.1 + 1000 * 0.1
horizon.example.coupon.value
## [1] 208

The realized compound yield is:

horizon.example.rcy = ((horizon.example.coupon.value + horizon.example.future.price) / horizon.example.current.price)^(1/2) - 1
horizon.example.rcy
## [1] 0.05211759