Bootstrapping the Zero Curve from IRS Swap Rates using R code
[This article was first published on K & L Fintech Modeling, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
This post explains how to generate the zero curve from market swap rates using bootstrapping. For the same 5-Year Libor IRS which is dealt with the previous post, we use Excel illustrations for clear understanding and then make a R code. Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Bootstrapping the IRS Zero Curve from LIBOR Interest Swap Rates
For detailed information about Libor IRS swap, refer to the following post.
At this previous post, we have priced a 5Y Libor IRS swap given the zero curve. But in this post we generate this zero curve from market IRS swap rates by using bootstrapping. Swap specification and R code for swap pricing in the previous post are used here.
Market Instruments and Swap Rates
As of 2021/06/30, consider the following 5-year IRS (Pay Float & Rec Fixed) swap rates, zero rates, sources, which are from the Bloomberg.
Market swap rates have three kinds according to its sources such as cash (deposit), futures, swap. Zero rates in the above table is only used for comparison.
Bootstrapping – Deposit
As market swap rate for deposit is quarterly compounding rate, discount factor is derived from this swap rate and zero rate is calculated from the discount factor as follows.
\[\begin{align} DF(s,t_i) & = \left(1+R^{mkt}_{t_i}\times \frac{\tau(s,t_i)}{360}\right)^{-1} \\ R(s,t_i) & = \frac{365}{\tau(s,t_i)}\times \log \left(\frac{1}{DF(s,t_i)} \right) \end{align}\] \[\begin{align} DF(s,t_i) &= \text{ discount factor from } t_i \text{ to } s \\ R(s,t_i) &= \text{ zero or spot rate from } t_i \text{ to } s \\ R^{mkt}_{t_i} &= \text{ market swap rate at } t_i \\ \tau(s,t_i) &= \text{ day count } \end{align}\]
Bootstrapping – Futures
Bloomberg provides market swap rate for Euro dollar futures as a rate, not a price (of course, some screens will provide it as a price). In principle, this rate is needed to be adjusted for convexity bias. But since we don’t know Bloomberg methodology exactly, convexity adjustments is not considered. If you know this Bloomberg method, please let us know.
Since maturities of futures are successive from 3M and non-overlapping, zero rates can be found in the following order.
- discount factor from \(t_{i-1}\) to \(t_i\)
- discount factor from spot date to \(t_i\)
- zero rate from discount factor
These three steps can be represented as the following equations
\[\begin{align} DF(t_{i-1}, t_i) & = \left(1+R^{mkt}_{t_i}\times \frac{\tau(t_{i-1},t_i)}{360}\right)^{-1} \\ DF(s, t_i) & = DF(s, t_{i-1}) \times DF(t_{i-1}, t_i) \\ R(s,t_i) & = \frac{365}{\tau(s,t_i)}\times \log \left(\frac{1}{DF(s,t_i)} \right) \end{align}\]
Since an optimization technique for finding zero rates is not needed for deposit and futures, its zero rates are recovered directly by using the above equations. Therefore, we can calculate zero rates for this range of maturities and make the following table (left part).
Zero rate for deposit is calculated directly from the market zero rate but for futures there is some difference since convexity adjustment is not applied. But just because there is some discrepancy, it doesn’t follow that this result is not accepted. It rather seems that this difference is smaller than expected. As we will find out later, the futures effect on zero rates for swap is negligible.
We already have calculated zero rates of deposit and futures and only need to calculate 4 zero rates for swaps, which are 4 unknown variables. Since 4 unknown equations are swap prices of these 4 swaps, this nonlinear 4-variable and 4-equation problem is solved numerically by using optimization.
Bootstrapping – Swaps
The slightly difficult part is to bootstrap zero rates from market swap rates for IRS. Deposit and futures have one bullet payment at maturity but IRS has in-between cash flows.
For example, 3-year zero rates is calculated by using the 3-year swap pricing. This process needs information of 0.25, 0.5, 0.75, …, 2.5, 2.75, 3 year zero rates. But we can only observe market swap rates for 2 and 3 year and some maturities less than 1 year. The zero rates for other remaining maturities are unobserved and should be interpolated.
For this characteristics we need to interpolate unobserved zero rates using adjacent unknown zero rates which will be found numerically and are corresponding to market observed maturities such as 2-, 3-, …, n-year.
For example 3.25-year swap rate is not observed but zero rates at 3.25-year is necessary for other swap pricing. In this case, zero rates at 3.25-year is interpolated using 3-year and 4-year zero rates.
This process is described at the right part of the above table, which shows the interpolated zero rates with 4 unknown zero rates. Unknown zero rates are found by using optimization but unobserved zero rates are found by using interpolation. Maturities of all zero rates consist of deposit, futures, swap maturities, and cash flow payment dates of all swaps.
For clear understanding, we show useful Excel illustrations for bootstrapping swap rates. In particular, since efficient vector operation is used, row-wise enumerations of swap cash flows are not necessary.
Fixed leg
From the previous post, we already know the present value of cash flow in fixed leg as follows.
\[\begin{align} PV(CF_{t_i}^{fixed}) = DF(s,t_i) \times R^{mkt}_{t=5Y} \times \frac{\tau(t_{i-1},t_i)}{360} \times NA \end{align}\]
Summing these up results in the fixed leg’s value. This process is illustrated in the following Excel calculations.
Floating leg
From the previous post, we already know the present value of cash flow in floating leg as follows.
\[\begin{align} PV(CF_{t_j}^{float}) = DF(s,t_i) \times FD(s, t_{j-1},t_j) \times \frac{\tau(t_{j-1},t_j)}{360} \times NA \end{align}\]
Summing these up results in the fixed leg’s value. This process is illustrated in the following Excel calculations.
Equations for discount factor and forward rate are the same as in the previous post.
\[\begin{align} DF(s,t_i) &= \exp \left(-R(s,t_i) \times \frac{t_i – s}{365} \right) \\ FD(s, t_{j-1},t_j) &= \frac{365}{t_j – t_{j-1}} \times \left(\frac{DF(s,t_{j-1})}{DF(s,t_j)}-1 \right) \end{align}\]
Optimization Result
From the above two legs, 4 zero rates are found numerically by making 2, 3, 4, 5 year swap prices are all equal to zeros as shown in the last column of the following Excel illustration.
Finally, we can compare bootstrapped zero rates with market zero rates (Bloomberg) as follows. We can find that for the range of swap, two zero rates are very similar.
R code
The following R code implements the zero curve bootstrapping of 5-year LIBOR IRS with the curve date of 2021/06/30 and the spot date of 2021/07/02.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | #=========================================================================# # Financial Econometrics & Derivatives, ML/DL using R, Python, Tensorflow # by Sang-Heon Lee # # https://kiandlee.blogspot.com #————————————————————————-# # Generate Libor 3M IRS zero curve by using Bootstrapping #=========================================================================# graphics.off() # clear all graphs rm(list = ls()) # remove all files from your workspace #————————————————————————– # Functions – Definition – Start #————————————————————————– # IRS swap pricer f_zero_pricer_IRS <– function( fixed_rate, # fixed rate vd.fixed_date, vd.float_date, # date for two legs vd.zero_date, v.zero_rate, # zero curve (dates, rates) d.spot_date, # spot date no_amt) { # nominal principal amount #———————————————————- # 0) Preprocessing #———————————————————- # convert spot date from date(d) to numeric(n) n.spot_date <– as.numeric(d.spot_date) # Interpolation of zero curve vn.zero_date <– as.numeric(vd.zero_date) f_linear <– approxfun(vn.zero_date, v.zero_rate, method=“linear”) vn.zero_date.inter <– n.spot_date:max(vn.zero_date) v.zero_rate.inter <– f_linear(vn.zero_date) # number of CFs ni <– length(vd.fixed_date) nj <– length(vd.float_date) # output dataframe with CF dates and its interpolated zero df.fixed = data.frame(d.date = vd.fixed_date, n.date = as.numeric(vd.fixed_date)) df.float = data.frame(d.date = vd.float_date, n.date = as.numeric(vd.float_date)) #———————————————————- # 1) Fixed Leg #———————————————————- # zero rate for discounting df.fixed$zero_DC = f_linear(as.numeric(df.fixed$d.date)) # discount factor df.fixed$DF <– exp(–df.fixed$zero_DC* (df.fixed$n.date–n.spot_date)/365) # tau, CF for(i in 1:ni) { ymd <– df.fixed$d.date[i] ymd_prev <– df.fixed$d.date[i–1] if(i==1) ymd_prev <– d.spot_date d <– as.numeric(strftime(ymd, format = “%d”)) m <– as.numeric(strftime(ymd, format = “%m”)) y <– as.numeric(strftime(ymd, format = “%Y”)) d_prev <– as.numeric(strftime(ymd_prev, format = “%d”)) m_prev <– as.numeric(strftime(ymd_prev, format = “%m”)) y_prev <– as.numeric(strftime(ymd_prev, format = “%Y”)) # 30I/360 tau <– (360*(y–y_prev) + 30*(m–m_prev) + (d–d_prev))/360 # cash flow rate df.fixed$rate[i] <– fixed_rate # Cash flow at time ti df.fixed$CF[i] <– fixed_rate*tau*no_amt # day fraction } # Present value of CF df.fixed$PV = df.fixed$CF*df.fixed$DF #———————————————————- # 2) Floating Leg #———————————————————- # zero rate for discounting df.float$zero_DC = f_linear(as.numeric(df.float$d.date)) # discount factor df.float$DF <– exp(–df.float$zero_DC* (df.float$n.date–n.spot_date)/365) # tau, forward rate, CF for(i in 1:nj) { date <– df.float$n.date[i] date_prev <– df.float$n.date[i–1] DF <– df.float$DF[i] DF_prev <– df.float$DF[i–1] if(i==1) { date_prev <– n.spot_date DF_prev <– 1 } # ACT/360 tau <– (date – date_prev)/360 # forward rate fwd_rate <– (1/tau)*(DF_prev/DF–1) # cash flow rate df.float$rate[i] <– fwd_rate # Cash flow amount at time ti df.float$CF[i] <– fwd_rate*tau*no_amt # day fraction } # Present value of CF df.float$PV = df.float$CF*df.float$DF return(sum(df.fixed$PV) – sum(df.float$PV)) } # objective function to be minimized objf <– function( v.unknown_swap_zero_rate, # unknown zero curve (rates) vn.unknown_swap_maty, # unknown swap maturity v.swap_rate, # fixed rate vd.fixed_date, # date for fixed leg vd.float_date, # date for float leg vd.zero_date_all, # all dates for zero curve v.zero_rate_known, # known zero curve (rates) d.spot_date, # spot date no_amt) { # nominal principal amount # zero curve augmented with zero rates for swaps v.zero_rate_all <– c(v.zero_rate_known, v.unknown_swap_zero_rate) v.swap_price <– NULL k <– 1 for(i in vn.unknown_swap_maty) { # calculate IRS swap price swap_price <– f_zero_pricer_IRS( v.swap_rate[k], # fixed rate, vd.fixed_date[1🙁2*i)], # semi-annual date vd.float_date[1🙁4*i)], # quarterly date vd.zero_date_all, # zero curve (dates) v.zero_rate_all, # zero curve (rates) d.spot_date, # spot date, no_amt) # nominal principal amount print(paste0(“Swap Price at spot date = “, round(swap_price,6))) # concatenate swap prices v.swap_price <– c(v.swap_price, swap_price) k <– k + 1 } return(sum(v.swap_price^2)) } #————————————————————————– # Functions – Definition – End #————————————————————————– #————————————————————————– # 1. Market Information #————————————————————————– # Zero curve from Bloomberg as of 2021-06-30 until 5-year maturity df.market <– data.frame( d.date = as.Date(c(“2021-10-04”,“2021-12-15”, “2022-03-16”,“2022-06-15”, “2022-09-21”,“2022-12-21”, “2023-03-15”,“2023-07-03”, “2024-07-02”,“2025-07-02”, “2026-07-02”)), # we use swap rate not zero rate. swap_rate= c(0.00145750000000000, 0.00139609870272047, 0.00203838571440434, 0.00197747863867587, 0.00266249271921742, 0.00359490949297661, 0.00512603194652204, 0.00328354999423027, 0.00571049988269806, 0.00793000012636185, 0.00964949995279312 ), # zero rate is only used for comparison. zero_rate = c(0.00147746193495074, 0.00144337757980778, 0.00166389741542625, 0.00175294804717070, 0.00196071374597585, 0.00224582504806747, 0.00264462838911974, 0.00328408008984121, 0.00571530169527018, 0.00795496282359075, 0.00970003866673104 ) ) #————————————————————————– # 2. Libor Swap Specification #————————————————————————– d.spot_date <– as.Date(“2021-07-02”) # spot date (date type) n.spot_date <– as.numeric(d.spot_date) # spot date (numeric type) no_amt <– 10000000 # notional principal amount # swap cash flow schedule from Bloomberg lt.cf_date <– list( fixed = as.Date(c(“2022-01-04”,“2022-07-05”, “2023-01-03”,“2023-07-03”, “2024-01-02”,“2024-07-02”, “2025-01-02”,“2025-07-02”, “2026-01-02”,“2026-07-02”)), float = as.Date(c(“2021-10-04”,“2022-01-04”, “2022-04-04”,“2022-07-05”, “2022-10-03”,“2023-01-03”, “2023-04-03”,“2023-07-03”, “2023-10-02”,“2024-01-02”, “2024-04-02”,“2024-07-02”, “2024-10-02”,“2025-01-02”, “2025-04-02”,“2025-07-02”, “2025-10-02”,“2026-01-02”, “2026-04-02”,“2026-07-02”)) ) # for bootstrapped zero curve df.zero <– data.frame( d.date = df.market$d.date, n.date = as.numeric(df.market$d.date), tau = as.numeric(df.market$d.date) – n.spot_date, taui = as.numeric(df.market$d.date) – n.spot_date, swap_rate = df.market$swap_rate, zero_rate = rep(0,length(df.market$d.date)), DF = rep(0,length(df.market$d.date))) # tau(i) = t(i) – t(i-1) df.zero$taui[2:nrow(df.zero)] <– df.zero$n.date[2:nrow(df.zero)] – df.zero$n.date[1:(nrow(df.zero)–1)] #————————————————————————– # 3. Bootstrapping – Deposit : row 1 #————————————————————————– # 1) calculate discount factor for deposit df.zero$DF[1] <– 1/(1+df.zero$swap_rate[1]*df.zero$tau[1]/360) # 2) convert DF to spot rate df.zero$zero_rate[1] <– 365/df.zero$tau[1]*log(1/df.zero$DF[1]) df.zero #————————————————————————– # 4. Bootstrapping – Futures : rows from 2 to 7 #————————————————————————– # No convexity adjustment is made for(i in 2:7) { # 1) discount factor from t(i-1) to t(i) df.zero$DF[i] <– 1/(1+df.zero$swap_rate[i]*df.zero$taui[i]/360) # 2) discount factor from spot date to t(i) df.zero$DF[i] <– df.zero$DF[i–1]*df.zero$DF[i] # 3) zero rate from discount factor df.zero$zero_rate[i] <– 365/df.zero$tau[i]*log(1/df.zero$DF[i]) } df.zero_until_futures <– df.zero #————————————————————————– # 5. Bootstrapping – Swaps : rows from 8 to 11 #————————————————————————– #=================================================================== # method 1 : Sequential Optimization for each observed swap maturity #=================================================================== # Bootstrapping zero rates sequentially using Brent minimization # with known (already bootstrapped) zero rates #——————————————————————- # initialization for fair comparison df.zero <– df.zero_until_futures for(i in 8:11) { # 1) find one unknown zero rate for one swap maturity m<–optim(0.01, objf, control = list(abstol=10^(–20), reltol=10^(–20), maxit=50000, trace=2), method = c(“Brent”), lower = 0, upper = 0.1, # for Brent vn.unknown_swap_maty = 2:(i–6), # unknown zero maturity v.swap_rate = df.zero$swap_rate[8:i], # observed swap rate vd.fixed_date = lt.cf_date$fixed, # date for fixed leg vd.float_date = lt.cf_date$float, # date for float leg vd.zero_date_all = df.zero$d.date[1:i],# all dates for zero curve v.zero_rate_known = df.zero$zero_rate[1:(i–1)], # known zero rates d.spot_date = d.spot_date, no_amt = no_amt) # 2) update this zero curve with the newly found zero rate df.zero$zero_rate[i] <– m$par # 3) convert this new zero rate to discount factor df.zero$DF[i] <– exp(–df.zero$zero_rate[i]*df.zero$tau[i]/365) } df.zero_seq <– df.zero # output for sequential optimization #=================================================================== # method 2 : Global Optimization #=================================================================== # initialization for 2nd optimization for fair comparison df.zero <– df.zero_until_futures # 1) find 4 unknown zero rates for each swap maturity m<–optim(c(0.01, 0.01, 0.01, 0.01), objf, control = list(abstol=10^(–20), reltol=10^(–20), maxit=50000, trace=2), method = c(“Nelder-Mead”), vn.unknown_swap_maty = 2:5, # unknown zero maturity v.swap_rate = df.zero$swap_rate[8:11], # observed swap rate vd.fixed_date = lt.cf_date$fixed, # date for fixed leg vd.float_date = lt.cf_date$float, # date for float leg vd.zero_date_all = df.zero$d.date[1:11],# all dates for zero curve v.zero_rate_known = df.zero$zero_rate[1:7], # known zero rates d.spot_date = d.spot_date, no_amt = no_amt) # 2) update this zero curve with the newly found 4 zero rates df.zero$zero_rate[8:11] <– m$par # 3) convert this new zero rates to discount factors df.zero$DF[8:11] <– exp(–df.zero$zero_rate[8:11]* df.zero$tau[8:11]/365) df.zero_glb <– df.zero # output for global optimization #————————————————————————– # 6. Comparison of two zero curves #————————————————————————– df.output <– data.frame(date = df.market$d.date, zero_mkt = df.market$zero_rate, zero_seq = df.zero_seq$zero_rate, zero_glb = df.zero_glb$zero_rate) # to avoid redundant expressions of df.output$ …. df.output <– within(df.output, { diff_seq = zero_mkt – zero_seq; diff_glb = zero_mkt – zero_glb }) print(“Comparison with Bloomberg Zero Curve”) df.output | cs |
Results
The following results show the market zero rate curve (Bloomberg) , the bootstrapped zero rate curve from sequential optimization, and the bootstrapped zero rate curve from global optimization with differences between them.
Except maturities of futures, there is no significant differences between them. But even for the range of futures, differences between market and bootstrapped zero curves are not so large despite the absence of the consideration of convexity adjustment. Of course, when we know the Bloomberg approach for adjusting convexity bias later, some modifications will be made at the range of futures.
Conclusion
From this post, we have generated the zero curve from market swap rates by using bootstrapping. Bootstrapping is implemented as the sequential or global optimization for unknown zero rates and we have found no evidence of significant differences in two approaches.
In fact, the reason why we cover this topic is that SIMM requires the market Greeks, not zero Greeks. Market Greeks are calculated by bumping the market swap rates and repricing but zero Greeks by bumping the zero curve and repricing. Next post will discuss how to calculate Greeks of interest rate swap by using these two methods. \(\blacksquare\)
To leave a comment for the author, please follow the link and comment on their blog: K & L Fintech Modeling.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.