Jul 19, 2021

Bootstrapping the Zero Curve from IRS Swap Rates using R code

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.




Introduction


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.
Bootstrapping the Zero Curve from IRS

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 adjustment 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.
  1. discount factor from \(t_{i-1}\) to \(t_i\)
  2. discount factor from spot date to \(t_i\)
  3. 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).

Bootstrapping the Zero Curve from IRS

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 complicated 4-variable and 4-equation problem is solved numerically by using optimization. Before performing an optimization, 4 unknown zero rates are filled with initial guesses such as 0.1, 0.2, 0.3, and 0.4%.


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 can be found by using a numerical optimization but unobserved zero rates are to be 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.

Bootstrapping the Zero Curve from IRS


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.

Bootstrapping the Zero Curve from IRS

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.

Bootstrapping the Zero Curve from IRS


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.

Bootstrapping the Zero Curve from IRS


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. In this R code, we perform two approaches: a sequential optimization and a global optimization.

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
383
384
385
386
387
388
389
390
391
392
#=========================================================================#
# 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
#=========================================================================
 
#--------------------------------------------------------------
# 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)
    v.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 v.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))
}
 
#=========================================================================
# Main 
#=========================================================================
 
#--------------------------------------------------------------
# 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
        v.unknown_swap_maty = (i-6),           # unknown zero maturity
        v.swap_rate = df.zero$swap_rate[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
    
    print(df.zero$zero_rate)
 
    # 3) convert this new zero rate to discount factor
    df.zero$DF[i] <- exp(-df.zero$zero_rate[i]*df.zero$tau[i]/365)
}
 
# output for sequential optimization
df.zero_seq <- df.zero
 
 
#----------------------------------------------
# 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.010.010.010.01), objf,
        control = list(abstol=10^(-20), reltol=10^(-20), 
                       maxit=50000, trace=2), 
        method = c("Nelder-Mead"),
        v.unknown_swap_maty = 2:5,              # unknown swap 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)
 
# output for global optimization
df.zero_glb <- df.zero 
 
#--------------------------------------------------------------------------
# 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("Comparisons 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.

Bootstrapping the Zero Curve from IRS

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.

It is necesaray to check whether swap prices of each maturities at spot date are at par (= zero). We can use the following R code and find out the correct results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Check whether swap prices at spot date are at par (= zero)
 
for(i in 2:5) {
    
    swap_price <- f_zero_pricer_IRS(
        df.market$swap_rate[i+6],  # fixed rate, 
        lt.cf_date$fixed[1:(2*i)], # semi-annual date
        lt.cf_date$float[1:(4*i)], # quarterly   date
        df.zero$d.date,            # zero curve (dates)
        df.zero$zero_rate,         # zero curve (rates)
        d.spot_date,               # spot date, 
        no_amt)              # nominal principal amount
    
    print(paste0(i,"-year Swap Price at spot date = ", swap_price))
}
 
>
[1"2-year Swap Price at spot date = -1.29446561913937e-06"
[1"3-year Swap Price at spot date = -0.000399942800868303"
[1"4-year Swap Price at spot date = -0.000693943642545491"
[1"5-year Swap Price at spot date = -0.000749870436266065"
 
cs

In the practical point of view, since instruments are related to hedging activities, many financial institutions use deposits and swaps as instruments without taking futures into account. Therefore it is typical to construct the swap zero curve by using deposits and swap rates.


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\)

Related Posts



No comments:

Post a Comment