Bond Valuation

Examples by Prof. Jacob Escobar, EGADE Business School, Tec de Monterrey

Explicit DCF Formula

Build a function to calculate the price of a bond by accumulating the present value of all its coupon payments and the principal (discounting all the cash flows).

\[ P = \frac{c}{(1+r)^1} + \frac{c}{(1+r)^2} + ... + \frac{c}{(1+r)^n} + \frac{FV}{(1+r)^n} \]


\[ P = \sum_{i=1}^n \frac{c}{(1+r)^i} + \frac{FV}{(1+r)^n} \]


Where: * c: coupon paid in each period * FV: face or par value, principal amount paid at maturity * r: required rate of return by the market, for each period * n: number of periods to maturity


However, the data we will receive is: * cr: coupon rate (per year) * ytm: yield-to-maturity (per year) * t: time-to-maturity (in years) * m: coupon payments per year


To get the missing inputs:

\[ c = \frac{cr}{m}FV \]


\[ r = \frac{ytm}{m} \]


\[ n = t*m \]

bond1 <- function(ytm = 0.05, cr = 0.05, t = 4, m = 2, fv = 100) {
  c <- _____
  r <- _____
  n <- _____
  
  p <- 0
  for (i in 1:n) {
    p <- _____ + c/(1+r)^_____
  }
  p <- _____ + fv/(1+r)^_____
  return(_____)
}
bond1()
## [1] 100

Validating the code

Compare the coupon rate vs yield-to-maturity (required market rate):

  • cr < ytm, c < r -> Discount bond or bond selling below par (P < FV)
  • cr = ytm, c = r -> Par bond or bond selling at par (P = FV)
  • cr > ytm, c > r -> Premium bond or bond selling above par (P > FV)

Some quick checks for this example if FV = 100, t = 4 and m = 2

  • cr = 5%, ytm = 3% -> P = 107.49
  • cr = 5%, ytm = 5% -> P = 100.00
  • cr = 5%, ytm = 7% -> P = 93.13
bond1(ytm = c(0.03, 0.05, 0.07))
## [1] 107.48593 100.00000  93.12604

Annuities Formula

\[ P = \frac{c}{r}\left(1 - \frac{1}{(1+r)^n}\right) + \frac{FV}{(1+r)^n} \]

bond <- function(ytm = 0.05, cr = 0.05, t = 4, m = 2, fv = 100) {
  c <- _____
  r <- _____
  n <- _____
  p <- (_____/r)*(1 - 1/(1+r)^n) + _____/(1+r)^n
  return(_____)
}
bond(ytm = c(0.03, 0.05, 0.07))
## [1] 107.48593 100.00000  93.12604

Price-YTM Relationship

Confirm they have an inverse relationship:

i <- seq(0.01, 0.40, 0.01)
print(i)
##  [1] 0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.10 0.11 0.12 0.13 0.14 0.15
## [16] 0.16 0.17 0.18 0.19 0.20 0.21 0.22 0.23 0.24 0.25 0.26 0.27 0.28 0.29 0.30
## [31] 0.31 0.32 0.33 0.34 0.35 0.36 0.37 0.38 0.39 0.40
p <- bond(ytm = ___, cr = 0.10, t = 20, m = 2, fv = 100)
print(round(p,2))
##  [1] 262.78 231.34 204.71 182.07 162.76 146.23 132.03 119.79 109.20 100.00
## [11]  91.98  84.95  78.78  73.34  68.51  64.23  60.40  56.97  53.89  51.10
## [21]  48.58  46.29  44.20  42.29  40.54  38.92  37.43  36.05  34.77  33.58
## [31]  32.47  31.43  30.46  29.54  28.68  27.87  27.11  26.39  25.70  25.05
plot(x = _____, 
     y = p, 
     type = "l", 
     col = "blue", 
     main = "Price/YTM Relationship")

Interest-rate risks

  • Price risk: If interest rates go up, the price of bond holdings go down, incurring on a loss if you sell them before maturity (also called “maturity risk”).
  • Reinvestment risk: If interest rates go down, the coupons received can’t be reinvested at the original rate (at least the bond’s price went up).

Sensibility to interest-rate risk

Stronger sensibility (% price change) with long time-to-maturity than with short time-to-maturity:

bond(ytm = 0.05, t = 8)/bond(ytm = 0.10, t = 8) - 1
## [1] 0.3716372
bond(ytm = 0.05, t = 2)/bond(ytm = 0.10, t = 2) - 1
## [1] 0.09727179

Stronger sensibility (% price change) with small coupon than with large coupons:

bond(ytm = 0.05, cr = 0.10)/bond(ytm = 0.10, cr = 0.10) - 1
## [1] 0.1792534
bond(ytm = 0.05, cr = 0.0)/bond(ytm = 0.10, cr = 0.0) - 1
## [1] 0.2126165

Stronger sensibility (% price change) when rates decrease than when they increase:

bond(ytm = 0.15)/bond(ytm = 0.10) - 1
## [1] -0.1565861
bond(ytm = 0.05)/bond(ytm = 0.10) - 1
## [1] 0.1927201

Binary Search for YTM

Define an initial lower bound (lb) and an initial upper bound (ub) such that both are just outside the range of reasonable possibilities of YTM.

While calculated price <> market price: 1. Use middle point as guess for YTM: \(mp = (lb + ub)/2\) 2. Redefine lb-ub range: - If the calculated price should be higher: \(lb = mp\) - If the calculated price should be lower: \(ub = mp\)

TIPS: * Many values can be used as the lower and upper bounds, for example, 0% and 50% work fine for lb and ub respectively. * Since ytm is not necessarily an exact integer, the search could go on and on while the “market price” (pmkt) and the “calculated price” (pcalc) are not exactly equal. To avoid this, a tolerance can be used to establish a range of acceptable answers: - While abs(pcalc – pmkt) > 0.000001 is TRUE, continue search… * An additional condition of “maximum iterations” can also be used to avoid an infinite While loop: - While iter < 1000 is TRUE, continue search…

ytm <- function(cr = 0.05, t = 4, pmkt = 100, fv = 100, m = 2) {
  lb <- _____
  ub <- _____
  tol <- 0.000001
  pcalc <- 0
  iter <- 0
  
  while(abs(pcalc - pmkt) > tol & iter < 1000) {
    mp <- _____
    pcalc <- bond(ytm = _____, cr = cr, t = t, m = m, fv = fv)
    iter <- _____
    ifelse(pcalc < pmkt, ub <- _____, lb <- _____)
    #print(paste("Iteration:", iter, "ytm_guess:", mp))
  }
  return(_____)
}
# In this case, sending pmkt as a vector doesn't work because of the relational
# operations the comparison of abs(pcalc - pmkt) > tol
ytm()
## [1] 0.05
ytm(pmkt = 93)
## [1] 0.07038301
ytm(pmkt = 107)
## [1] 0.03124738

Implied Volatility

  • The implied volatility of an option is the volatility for which the Black-Scholes price equals the market price
  • The is a one-to-one correspondence between prices and implied volatilities
  • Traders and brokers often quote implied volatilities rather than dollar prices

Black-Scholes Function

We will leverage the Black-Scholes function we created previously, to calculate the option’s price for each guessed volatility, until the calculated option price is the same as the market option price.

Binary Search works, and it would be like the YTM function we already did.

bs <- function(type = "c", s = 52, k = 50, 
               t = 0.5, v = 0.20, r = 0.10) {

  op <- NA
  d1 <- (log(s/k)+(r+(v^2)/2)*t)/(v*sqrt(t))
  d2 <- d1 - v*sqrt(t)
  if(type == "c")   {
    op <- s*pnorm(d1) - k*exp(-r*t)*pnorm(d2)
  }
  if(type == "p") {
    op <- k*exp(-r*t)*pnorm(-d2) - s*pnorm(-d1)
  }
  return(op)
}
bs()
## [1] 5.564811
bs(type = "p")
## [1] 1.126282

Binary Search for Implied Volatility

  • How does volatility affect option prices?
  • Is there a positive or a negative relationship?
  • Does it depend on the option type or style?
iv <- function(type = "c", op_mkt = 5.5648, s = 52, 
               k = 50, t = 0.5, r = 0.10) {
  ub <- _____
  lb <- _____
  tol <- 0.000001
  maxiter <- 1000
  iter <- 0
  op_calc <- 0
  while (abs(op_mkt - op_calc) _____ tol & iter _____ maxiter) {
    v_guess <- _____
    op_calc <- bs(type = type, s = s, k = k,
                            t = t, r = r, v = _____)
    
    ifelse(op_calc < op_mkt, _____ <- v_guess, _____ <- v_guess)
    iter <- _____ + 1
    #print(paste("iter:", iter,"vol:", v_guess))
  }
  ifelse(abs(op_mkt - op_calc) <= tol, return(v_guess),
         return("Implied volatility not found, max iterations reached"))
}
iv(type = "c", op_mkt = 5.5648)
## [1] 0.1999991
iv(type = "p", op_mkt = 1.1263)
## [1] 0.2000016

Volatility Smile

VolatilitySmile

  • Why do we see this shape empirically?
  • Shouldn’t the volatility be the same, regardless of K, as long as it is the same underlying asset?
  • What about the Normal distribution vs the real distribution?