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