# 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