Bond Basics1

Pricing Bonds

Bond prices are determined by using the coupon rates and cash flows to find the present value of the bond.

\[PV(CF_t) = B(0,t)CF_t\]

where \(CF_t\) is the cash flow at time \(t\) and \(B(0,t)\) is the discount factor or the price at time \(0\) on the dollar.

\[PV(CF_t) = \dfrac{CF_t}{[1+R(0,t)]^t}\]

where \(R(0,t)\) is the annual spot rate at time \(0\) with the maturity of \(t\). We can rearrange to

\[B(0,t) = \dfrac{1}{[1+R(0,t)]^t}\]

\(B(0,t)\) can also be referred to as the price of the zero coupon bond. Most bonds are not zero coupon but it is possible to construct almost all payment structures using zero coupon bonds.

We can imply zero coupon rates with different maturities from the bonds on the market. Then we can use these rates to build a term structure model to price any bond. Strict violations of the term structure can be a buy/sell opportunity as well as an arbitrage opportunity.

calculate_bond_price<-function(face_value=1000,coupon_rate=0.05,maturity=1,yearly_coupons=0){
    #This function calculates the price of the bond B(0,t) given
    #its face value, maturity, annual coupon rate and equidistant payment
    #if yearly_coupons == 0, it only pays out at the maturity
    #if yearly_coupons == 1, it pays out annually
    #if yearly_coupons == 2, it pays out semiannually
    if(yearly_coupons==0){
        face_value/((1+coupon_rate)^maturity)
    }else{
        face_value/((1+coupon_rate/yearly_coupons)^(yearly_coupons*maturity))
    }

}
calculate_bond_price()
## [1] 952.381

We can also construct zero coupon bonds from coupon paying bonds if we have the appropriate securities. From the lecture notes suppose we have two bonds.

\[99 = \dfrac{8}{(1+R(0,1))} + \dfrac{108}{(1+R(0,2))^2} = \dfrac{8}{1.0526} + \dfrac{108}{(1+R(0,2))^2}\]

\[R(0,2) = 8.07\%\]

The price of the 2 year pure discount bond would be \(99 - 0.08(95) = 91.4\) by going long with the two year bond and short with one year bond by 0.08 units (to offset gains from the first year coupon).

Compounding Interest Types

Simple Compounding

This is the method where interest rate is applied only once. Suppose an interest rate of 0.05 and a maturity of 2 years. What would be the price of $100 will be at the maturity.

\[100*(1+0.05)^(1.5) = 107.59\]

Periodic Compounding

If the interest is perpetually added to the principal investment, then we have a compounding interest rate. Suppose the same example but compounded semiannually.

\[100*(1+0.05/2)^(2*1.5) = 107.6891\]

which yields a annual nominal interest rate of \((1.076891)^(1/1.5)-1 = 0.050625\).

Continuous Compounding

Now suppose frequency of the compounding is so high that at the time period between added interest is infinitesimal (close to zero). Then at the limiting case

\[\lim_{n->\infty}\left(1+r/n\right)^{nT} = e^{rT}\]

Looks familiar?

So with our example continuously compounding annual interest rate is \(e^(0.05)-1=0.051271\).

Given a set of zero coupon bond prices we can calculate the continuous yield values with \(-log(B(0,T)/T)\).

calculate_yield<-function(bond_price,maturity){

    -log(bond_price)/maturity

}
#Example bond price is 0.987 and maturity is half a year.
calculate_yield(0.987,0.5)
## [1] 0.02617048

Forward Rate

Suppose there are two bonds with different maturities (say \(t\) and \(T\), \(T > t\)) and spot interest rates (\(R(0,t) and R(0,T)\)). The additional rate between the maturities (\(R(t,T)\)) is called the forward rate, denoted as \(F(0,t,T-t)\).

\[(1+R(0,T))^T = (1+R(0,t))^t(1+F(0,t,T-t))^{(T-t)}\]

can be rearranged into

\[F(0,t,T-t) = \left(\dfrac{(1+R(0,T))^T}{(1+R(0,t))^t}\right)^{1/(T-t)}-1\]

imply_forward_rate<-function(R0t1=0.04,R0t2=0.045,t1=1,t2=2){

    ((1+R0t2)^t2/(1+R0t1)^t1)^(1/(t2-t1)) -1

}
imply_forward_rate()
## [1] 0.05002404

\(F(0,t,T-t)\) is also equivalent to the zero coupon rate at the future \(R(t,T-t)\).

Correlation of Maturities

Interest rates do not only change with maturity but they also change in time. We will also use an R package called YieldCurve for some data and calculations.

install.packages("YieldCurve")

Let’s load the library and check the Fed Yield Curve data.

library(YieldCurve)
#Call the data
data(FedYieldCurve)
head(FedYieldCurve)
##             R_3M  R_6M  R_1Y  R_2Y  R_3Y  R_5Y  R_7Y R_10Y
## 1981-12-31 12.92 13.90 14.32 14.57 14.64 14.65 14.67 14.59
## 1982-01-31 14.28 14.81 14.73 14.82 14.73 14.54 14.46 14.43
## 1982-02-28 13.31 13.83 13.95 14.19 14.13 13.98 13.93 13.86
## 1982-03-31 13.34 13.87 13.98 14.20 14.18 14.00 13.94 13.87
## 1982-04-30 12.71 13.13 13.34 13.78 13.77 13.75 13.74 13.62
## 1982-05-31 13.08 13.76 14.07 14.47 14.48 14.43 14.47 14.30

The correlation matrix shows that yield rates are not perfectly correlated, so there are shifts and changes in shape in time.

R_3M R_6M R_1Y R_2Y R_3Y R_5Y R_7Y R_10Y
R_3M 1.0000000 0.9983390 0.9940045 0.9837559 0.9744780 0.9546189 0.9399504 0.9230412
R_6M 0.9983390 1.0000000 0.9981715 0.9899820 0.9817197 0.9632268 0.9491761 0.9332366
R_1Y 0.9940045 0.9981715 1.0000000 0.9959937 0.9900195 0.9746174 0.9621895 0.9478956
R_2Y 0.9837559 0.9899820 0.9959937 1.0000000 0.9984844 0.9896811 0.9808896 0.9694621
R_3Y 0.9744780 0.9817197 0.9900195 0.9984844 1.0000000 0.9958583 0.9896185 0.9804575
R_5Y 0.9546189 0.9632268 0.9746174 0.9896811 0.9958583 1.0000000 0.9983629 0.9936744
R_7Y 0.9399504 0.9491761 0.9621895 0.9808896 0.9896185 0.9983629 1.0000000 0.9981232
R_10Y 0.9230412 0.9332366 0.9478956 0.9694621 0.9804575 0.9936744 0.9981232 1.0000000

Term Structure

In this part we will see the methods of extracting and constructing bond prices and yields.

Direct Method

Suppose you are given the following bond rates (from lecture notes). Remember nominal rate is 100.

Coupon Maturity Price
Bond 1 5.0 1 101.0
Bond 2 5.5 2 101.5
Bond 3 5.0 3 99.0
Bond 4 6.0 4 100.0

The zero coupon bond prices (\(B(0,t)\)) can be found with the following system of equations.

\[\begin{align*} 101 &= 105B(0,1) \\ 101.5 &= 5.5B(0,1) + 105.5B(0,2) \\ 99 &= 5B(0,1) + 5B(0,2) + 105B(0,3) \\ 100 &= 6B(0,1) + 6B(0,2) + 6B(0,3) + 106B(0,4) \end{align*}\]

Then we obtain \(B(0,1)=0.9619, B(0,2)=0.9114, B(0,3) = 0.85363, B(0,4) = 0.789\). Their corresponding zero coupon yield rates are \(R(0,1)=3.96\%, R(0,2)=4.717\%, R(0,3)=5.417\%, R(0,4)=6.103\%\).

get_zero_coupon<-function(coupons=c(5,5.5,5,6),BondPrices=c(101,101.5,99,100),nominal_value=100){

    #We assume both coupons and BondPrices vectors are arranged to 1 year increasing maturity.
    price_matrix <- matrix(0,nrow=length(coupons),ncol=length(coupons))

    #Assign the coupons for each year
    for(i in 1:length(coupons)){
        price_matrix[i,1:i] <- coupons[i]
    }

    #Add the maturity nominal value
    diag(price_matrix) <- diag(price_matrix) + nominal_value

    #Solve the system of equations to get B(0,t)
    zero_coupon_prices<-solve(price_matrix,BondPrices)

    #Get zero coupon yields R(0,t)
    zero_coupon_yields <- (1/zero_coupon_prices)^(1/1:length(coupons))-1

    return(list(B0t=zero_coupon_prices,R0t=zero_coupon_yields))
}

get_zero_coupon()
## $B0t
## [1] 0.9619048 0.9119386 0.8536265 0.7890111
## 
## $R0t
## [1] 0.03960396 0.04717001 0.05417012 0.06103379

Linear Interpolation

R03<-0.055
R04<-0.06

R03p75<-((4-3.75)*0.055+(3.75-3)*0.06)/(4-3)
R03p75
## [1] 0.05875
##Or use the R function
yield_interpolate<-approxfun(x=c(3,4),y=c(0.055,0.06))
yield_interpolate(3.75)
## [1] 0.05875

Cubic Interpolation

Suppose our rates are as following: \(R(0,1)=0.03, R(0,2)=0.05, R(0,3)=0.055, R(0,4)=0.06\). We are going to solve the following structure for a, b, c and d.

\[\begin{align*} R(0,t_1) &= at_1^3 + bt_1^2 + ct_1 + d \\ R(0,t_2) &= at_2^3 + bt_2^2 + ct_2 + d \\ R(0,t_3) &= at_3^3 + bt_3^2 + ct_3 + d \\ R(0,t_4) &= at_4^3 + bt_4^2 + ct_4 + d \end{align*}\]
A_mat<-t(matrix(1:4,nrow=4,ncol=4,byrow=TRUE)^(3:0))
abcd_vec<-solve(A_mat,c(0.03,0.05,0.055,0.06))

#Interpolate for a bond of 2.5 years
t_val<-2.5
sum(abcd_vec*((2.5)^(3:0)))
## [1] 0.0534375
#Or we can use the cubic spline function of R
yield_spline<-splinefun(x=1:4,y=c(0.03,0.05,0.055,0.06))
yield_spline(2.5)
## [1] 0.0534375

Indirect Methods (Nelson Siegel)

Instead of bootstrap techniques we will use models. Nelson Siegel model is a popular way of modeling interest rate yield curve.

\[R(0,\theta) = \beta_0 + \beta_1\left(\dfrac{1-exp(-\theta/\tau)}{\theta/\tau}\right) + \beta_2\left[\dfrac{1-exp(-\theta/\tau)}{\theta/\tau} - exp(-\theta/\tau)\right]\]

where \(\theta\) is the maturity, \(\beta_0\) is the level parameter (long-term yield rate), \(\beta_1\) is the slope parameter (long/short term spread), \(\beta_2\) is the curvature parameter and \(\tau\) is the scale parameter.

nelson_siegel_calculate<-function(theta,tau,beta0,beta1,beta2){
    beta0 + beta1*(1-exp(-theta/tau))/(theta/tau) + beta2*((1-exp(-theta/tau))/(theta/tau) - exp(-theta/tau))
}

###Let's plot the yield curve of NS with the following parameters

ns_data <-
data.frame(maturity=1:30) %>%
mutate(ns_yield=nelson_siegel_calculate(theta=maturity,tau=3.3,beta0=0.07,beta1=-0.02,beta2=0.01))

head(ns_data)
##   maturity   ns_yield
## 1        1 0.05398726
## 2        2 0.05704572
## 3        3 0.05940289
## 4        4 0.06122926
## 5        5 0.06265277
## 6        6 0.06376956
ggplot(data=ns_data, aes(x=maturity,y=ns_yield)) + geom_point() + geom_line()

You can play with the parameters to get better estimates of the yield curve.

Estimation of Nelson Siegel Parameters

R package YieldCurve mentioned above has a Nelson Siegel curve estimation function. See the help file for details.

Nelson.Siegel(as.matrix(FedYieldCurve[1:3,]),maturity=c(3,6,12,24,36,60,72,120))
##              beta_0     beta_1   beta_2    lambda
## 1981-12-31 14.70711 -5.3917409 3.269125 0.5123605
## 1982-01-31 14.35240 -0.7602066 2.834508 0.1887807
## 1982-02-28 13.74481 -0.9247232 2.681840 0.1236869

Note: We refer lambda as tau (\(\tau\)), the shape parameter.

Beta Sensitivity

Consider the price of a bond delivering future cash flows \(F_i\) as \(P_0 = \sum_iF_ie^{-\theta_iR(0,\theta_i)}\). So the change in the price with beta parameters is as follows.

\[\dfrac{\partial P_{0}}{\partial \beta_{0}} = -\sum_{i}\theta_{i}F_i e^{-\theta_{i} R(0,\theta_{i})}\]

\[\dfrac{\partial P_0}{\partial \beta_1} = -\sum_i\theta_i \left[ \dfrac{1 - exp(- \theta / \tau)}{\theta/\tau}\right]F_ie^{-\theta_i R(0,\theta_i)}\]

\[\dfrac{\partial P_0}{\partial \beta_2} = - \sum_i \theta_i \left[ \dfrac{1 - exp(- \theta / \tau)}{\theta / \tau} - exp(- \theta / \tau) \right]F_ie^{-\theta_iR(0,\theta_i)}\]

nelson_siegel_sensitivities<-function(tau=3,beta0=0.08,beta1=-0.03,beta2=-0.01,nominal_rate=100,coupon_rate,maturity){

        f_i <- nominal_rate*rep(coupon_rate,maturity)
        f_i[length(f_i)]<-f_i[length(f_i)]+nominal_rate
        theta_i <- 1:maturity

        r_i <- 0
        for(i in 1:maturity){
            r_i[i] <- nelson_siegel_calculate(theta=i,tau=tau,beta0=beta0,beta1=beta1,beta2=beta2)
        }

        beta0_sens <- -sum(f_i*theta_i*exp(-theta_i*r_i))
        beta1_sens <- -sum(f_i*theta_i*(1-exp(-theta_i/tau))/(theta_i/tau)*exp(-theta_i*r_i))
        beta2_sens <- -sum(f_i*theta_i*((1-exp(-theta_i/tau))/(theta_i/tau) - exp(-theta_i/tau))*exp(-theta_i*r_i))

        return(c(Beta0=beta0_sens,Beta1=beta1_sens,Beta2=beta2_sens))
}

nelson_siegel_sensitivities(coupon_rate=0.05,maturity=2)
##      Beta0      Beta1      Beta2 
## -192.51332 -141.08199  -41.27936
nelson_siegel_sensitivities(coupon_rate=0.05,maturity=7)
##     Beta0     Beta1     Beta2 
## -545.4198 -224.7767 -156.7335
nelson_siegel_sensitivities(coupon_rate=0.05,maturity=15)
##     Beta0     Beta1     Beta2 
## -812.6079 -207.1989 -173.0285

  1. These R lecture notes will also be following Fixed-Income Securities: Valuation Risk Management and Portfolio Strategies by Martellini, Priaulet and Priaulet.

  2. Bonds can be bought or sold at an extra premium or discount on top of their yields. Pure discount means there is no extra discount or premium.