Examples by Prof. Jacob Escobar, EGADE Business School, Tec de Monterrey
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
Compare the coupon rate vs yield-to-maturity (required market rate):
Some quick checks for this example if FV = 100, t = 4 and m = 2
bond1(ytm = c(0.03, 0.05, 0.07))
## [1] 107.48593 100.00000 93.12604
\[ 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
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")
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
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
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
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
VolatilitySmile