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.

# 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.
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).

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.

 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 #=========================================================================## 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 graphsrm(list = ls()) # remove all files from your workspace #————————————————————————–# Functions – Definition – Start#————————————————————————– # IRS swap pricerf_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 minimizedobjf <– 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 maturitydf.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 curvedf.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 depositdf.zero$DF[1] <– 1/(1+df.zero$swap_rate[1]*df.zero$tau[1]/360) # 2) convert DF to spot ratedf.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 madefor(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 comparisondf.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 comparisondf.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 Colored by Color Scripter 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$$