Computing yield-to-maturity

This example is taken from Chapter 14 of Gilli/Maringer/Schumann, 2011. The functions ytm and vanillaBond are included in the NMOF package (since version 0.27-1).

require("NMOF")

A plain-vanilla bond can be represented as a list of cashflows, cf, with associated payment dates. The bond's theoretical price b0 is the present value of these payments. As an example, we calculate b0 with a single yield y.

cf <- c(5, 5, 5, 5, 5, 105) ## cashflows
times <- 1:6                ## times to payment
y <- 0.047                  ## the "true" yield
b0 <- sum(cf/(1 + y)^times)
b0

Since y is below the coupon rate, the theoretical price should be higher than par.

[1] 101.5374

The function vanillaBond shows a simple implementation for computing the present value of cashflows.

vanillaBond <- function(cf, times, df, yields) {
    if (missing(times))
        times <- seq_len(length(cf))
    if (missing(df))
        df <- 1/(1+yields)^times      
    drop(cf %*% df)
}

Some examples.

cf <- c(rep(5, 9), 105)
vanillaBond(cf, yields = 0.05)
vanillaBond(cf, yields = 0.03)
[1] 100
[1] 117.0604

If only a single yield is given, the function acts as if the term structure were flat. But we did not explicitly check for this case; R's recycling rule will handle this for us. Here is an example to show this more clearly:

2^(1:5)
[1]  2  4  8 16 32

(the ^ operator has precedence over : which is why we need the parentheses.)

Another example; this time we value the bond according a Nelson-Siegel curve. With the given parameters, the curve should be flat.

vanillaBond(cf, 1:10, yield = NS(c(0.03,0,0,2), 1:10))
[1] 117.0604

Back to our problem: to recover y from b0, we append b0 to the cashflow vector, but switch its sign (since we need to buy the bond). The is now to find discount factors for which the sum over all cashflows (the net present value) is just zero.

cf <- c(-b0, cf); times <- c(0, times)  
data.frame(times=times, cashflows=cf)
 
 times cashflows
1     0 -101.5374
2     1    5.0000
3     2    5.0000
4     3    5.0000
5     4    5.0000
6     5    5.0000
7     6  105.0000

The function ytm evaluates the derivative of the discounted cashflows analytically; ytm2 uses a finite difference.

ytm <- function(cf, times, y0 = 0.05,
                tol = 1e-05, h = 1e-05, maxit = 1000L) {        
    dr <- 1
    for (i in seq_len(maxit)) {
        y1 <- 1 + y0
        g <- cf / y1 ^ times
        g <- sum(g)
        t1 <- times - 1
        dg <- times * cf * 1/y1 ^ t1
        dg <- sum(dg)
        dr <- g/dg
        y0 <- y0 + dr
        if (abs(dr) < tol)
            break
    }
    y0
}
ytm2 <- function(cf, times, y0 = 0.05,
                tol = 1e-04, h = 1e-08, maxit = 1000L) {        
    dr <- 1
    for (i in seq_len(maxit)) {
        y1 <- 1 + y0
        g <- sum(cf/y1^times)
        y1 <- y1 + h
        dg <- (sum(cf/y1^times) - g)/h
        dr <- g/dg
        y0 <- y0 - dr
        if (abs(dr) < tol)
            break
    }
    y0
}
system.time(for (i in 1:2000) ytm(cf, times, y0=0.06))
system.time(for (i in 1:2000) ytm2(cf, times, y0=0.06))
ytm(cf, times, y0=0.062, maxit = 5000)
ytm2(cf, times, y0=0.062, maxit = 5000)
   user  system elapsed 
  0.064   0.000   0.064
   user  system elapsed 
  0.052   0.000   0.051
[1] 0.0470007
[1] 0.047

The only reason for not using a finite difference is that with extreme rates or extremely far off starting values, the numerically-evaluated derivative is more stable. But note that far-off really means far-off: something like the true yield is 5 percent and we use a starting value of 50 percent. (A reasonable starting value is the coupon divided by the price.)

(initial.value <- 5/b0)
ytm(cf,  times, y0 = 0.7, maxit = 5000)
ytm(cf,  times, y0 = initial.value)
ytm2(cf, times, y0 = 0.7, maxit = 5000)
ytm2(cf, times, y0 = initial.value)
[1] 0.04699928
[1] 0.04700013
[1] Inf
[1] 0.047

(the tangled R-code)

return to main page...

return to R page...