Nelson-Siegel-Svensson Yield Curve model using R code

This post introduces Nelson-Siegel-Svensson (NSS) yield curve model which is an extension of Nelson-Siegel (NS) model with an additional curvature factor. It aims to fit longer term maturities well.


Nelson-Siegel-Svensson model



Svensson (1995) suggests an extended NS model which is typically called NSS model. Many central banks use the NSS model and Gürkaynak et al. (2007) also use it to fit U.S. Treasury yield curve. The reason why NSS model is widely used is evident. As can be seen the following dynamic figure, It fits yields at long-term maturities well.




Nelson-Siegel-Svensson model


Nelson-Siegel-Svensson model is a non-linear least square problem with 6 parameters with some inequality constraints.
\[\begin{align} y(\tau) &= \beta_1 + \beta_2 \left( \frac{1-e^{- \tau \lambda_1 }}{\tau \lambda_1 }\right) \\ &+ \beta_3 \left(\frac{1-e^{- \tau \lambda_1 }}{\tau \lambda_1 }-e^{- \tau \lambda_1 }\right) \\ &+ \beta_4 \left(\frac{1-e^{- \tau \lambda_2 }}{\tau \lambda_2 }-e^{- \tau \lambda_2 }\right) \end{align}\] \[\begin{align} \beta_1 > 0, \beta_1 + \beta_2 >0 \\ \lambda_1 > \lambda_2 > 0 \end{align}\]
Here, \(\tau\) is a maturity and \(y(\tau)\) is a continuously compounded spot rate with \(\tau\) maturity. \(\beta_1, \beta_2, \beta_3\, \beta_4\) are coefficient parameters. \(\lambda_1\) and \(\lambda_2 \) are the decay parameters.

\(\lambda_1\) and \(\lambda_2\) determine the shapes of one slope and two curvature factor loadings as follows.
Nelson-Siegel-Svensson factor loadings

In the above figure, I use \(\lambda_1 = 0.0609\) and \(\lambda_2 = 0.01 \), which represent the maximum of curvature loadings are attained at nearly 30-month and 180-month respectively. Smaller \(\lambda_2\) fits the yield curve at longer maturities well but it lowers the interpretability of the level factor. For this reason, too small \(\lambda_2\) is not appropriate and is needed to be constrained by some reasonable upper and lower bounds. Of course, too high \(\lambda_1\) is not good since it tends to result in too excessive curvature estimate. 



Restrictions on \(\lambda_1\) and \(\lambda_2\)


The most difficult part is how to choose \(\lambda_1\) and \(\lambda_2\). Without suitable restrictions on these two parameters, estimates of these parameters exhibit too excessive or erratic. It is typical to determine \(\lambda_1\) from ranges of medium-term maturities and \(\lambda_2\) from ranges of long-term maturities. I set two ranges of maturities as 1 ~ 5 years and 6 ~ 20 year for example.

However, reasonable constrains on two decay parameters are dependent on which data is used.

R code


The following R code estimates parameters of NSS model with yield curves at four dates and compares these results with that of NS model.

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
#========================================================#
# Quantitative Financial Econometrics & Derivatives 
# ML/DL using R, Python, Tensorflow by Sang-Heon Lee 
#
# https://shleeai.blogspot.com
#--------------------------------------------------------#
# Estimation of Nelson-Siegel-Svensson model
#========================================================#
 
graphics.off(); rm(list = ls())
library(alabama)
 
#-----------------------------------------------
# Nelson-Siegel-Svensson function
#-----------------------------------------------
 
# NSS factor loading
nss_loading <- function(la,m) {
    la1 <- la[1]; la2 <- la[2]
    C  <- cbind(
        rep(1,length(m)),    
        (1-exp(-la1*m))/(la1*m),             
        (1-exp(-la1*m))/(la1*m)-exp(-la1*m),
        (1-exp(-la2*m))/(la2*m)-exp(-la2*m))
    return(C)
}
 
# fitting function
nss_fit <- function(para, m) {
    beta <- para[1:4]
    return(nss_loading(para[5:6],m)%*%beta)
}
 
# objective function
nss_objf <- function(para, y, m) {
    return(sqrt(mean((y - nss_fit(para, m))^2)))
}
 
# constrOptim.nl constraint function ( > )
nss_constf <- function(para, y, m) {
    
    beta <- para[1:4
    la1  <- para[5]; la2 <- para[6]
    h    <- rep(NA, 6)
    
    # b1 > 0, b1+b2 > 0
    h[1<- beta[1]         
    h[2<- beta[1]+beta[2]
    
    # 0.15 > la1 > 0.03
    h[3<- 0.15-la1
    h[4<- la1-0.03
    
    # 0.025 > la2 > 0.0075
    h[5<- 0.025-la2
    h[6<- la2-0.0075
    
    return(h)
}
 
#===========================================================
# 1. Read data
#===========================================================
    
# Estimated parameters of Nelson-Siegel (Benchmark)
str.ns_est<- "
    beta1      beta2       beta3     lambda       rmse
    7.949446  0.2933681 -0.08749462 0.11555381 0.07665367
    7.052864 -1.5778886 -0.42253575 0.01823114 0.06432032
    5.586903 -0.5600670 -1.43976821 0.03357762 0.06423196
    6.011739  0.3470154 -1.00018107 0.04565435 0.09314726"
 
m.ns_est <- read.table(text = str.ns_est, header=TRUE)
 
str.zero <- "
    mat 19890630 19950929 19980831 20000929
    3   8.171964 5.413123 4.931036 6.180960
    6   8.143305 5.509881 4.949141 6.207158
    9   8.123782 5.592753 4.962873 6.204367
    12  8.111922 5.654558 4.962513 6.176521
    24  7.974091 5.782513 4.824109 5.932045
    36  7.943757 5.841313 4.868401 5.862433
    48  7.959505 5.920732 4.888494 5.825789
    60  7.942511 5.967207 4.900205 5.799035
    72  8.000704 6.013260 4.980854 5.826354
    84  8.010956 6.055806 4.977891 5.847383
    96  8.020059 6.151399 5.007556 5.859937
    108 8.027901 6.224049 5.054961 5.875451
    120 8.037343 6.265079 5.100250 5.912506
    144 8.063573 6.364383 5.200412 6.002328
    180 8.052892 6.512078 5.347487 6.093959
    240 7.971860 6.718515 5.482335 6.091155
    300 7.861711 6.789321 5.422557 5.972525
    360 7.721083 6.599990 5.246325 5.697709"
 
df <- read.table(text = str.zero, header=TRUE)
m  <- df$mat; nmat <- length(m)
 
#===========================================================
# 2. Parameter estimation
#===========================================================
 
ctrl.optim <- list(maxit=50000, trace=0
                   reltol = 1e-10, abstol = 1e-10)
ctrl.outer <- list(eps = 1e-10, itmax = 50000
                   method = "Nelder-Mead", NMinit = TRUE)
 
# output container
m.nss_est <- matrix(NA, 47)
m.nss_fit <- matrix(NA, length(m), 4)
 
# Estimation of Nelson-Siegel-Svensson
for(i in 1:4) {
    
    y <- df[,1+i]
    x_init <- c(y[nmat], y[1]-y[nmat], 
                2*y[6-y[1]-y[nmat], 
                2*y[15]-y[1]-y[nmat], 0.06090.01)
 
    opt <- constrOptim.nl(y = y, m = m,
        par=x_init, fn=nss_objf, hin=nss_constf,
        control.optim =ctrl.optim,
        control.outer=ctrl.outer) 
    
    m.nss_est[i,] <- c(opt$par, opt$value)
    m.nss_fit[,i] <- nss_fit(opt$par, m)
    colnames(m.nss_est) <- c("beta1""beta2"
        "beta3""beta4""lambda1""lambda2","rmse")
}
 
colnames(m.nss_fit) <- paste0("fitting-",1:4)
 
#=======================================================
# 3. Estimation results
#=======================================================
 
# NSS factor loading
mi <- 1:360
x11(width = 16/2.5, height = 5); 
matplot(mi, nss_loading(c(0.06090.01),mi), 
        type = "l", lwd=6, xlab = "Maturity (month)",
        ylab = "Factor loading")
legend("right"
       legend=c("Level""Slope""Curvature1""Curvature2"),
       pch = c(15,16), border="white", box.lty=0, cex=1,
       col = c("black","#DF536B""#61D04F""#2297E6"))
 
# Parameter estimates
m.ns_est
m.nss_est
 
# data and fitted yields
round(cbind(df[,-1], m.nss_fit),3)
    
cs


The following estimation results show that NSS model provides good in-sample fit than NS model except the second date. This is related to the restrictions on the decay parameters. When these constraints are relaxed, the results will changed. I expect that.

Nelson-Siegel-Svensson Yield Curve model using R code


Four selected dates, fitting results of NSS model show relative good fitting results. Instead, we can find that level factor (\(\beta_1\)) is affected by the introduction of the second curvature factor (\(\beta_4\)). In other words, there is some distinct difference in level factors between NS model and NSS model.

Nelson-Siegel-Svensson Yield Curve model using R code
Using the same data, it seems that NS model has some difficulties in fitting yields at longer-term maturities. Unlike NSS model, the level factor of NS model represent current long-term rate well.
Nelson-Siegel-Svensson Yield Curve model using R code
However, in most cases, NS model delivers sufficiently good fitting results. In other words, it is likely that the second curvature factor can be a superfluous factor in many normal days, which is considered an overfitting.

In particular, when the second curvature factor is not identified by data, so called compounding effect can take place. This is the case when two curvature estimates are too large in magnitude with the opposite signs. This compounding effect can be occurred between the slope and the curvature factors.



Concluding Remarks


This post estimates Nelson-Siegel-Svensson yield curve model which aims to fit longer term maturities well. It is worth noting that to get reasonable parameter estimates, proper restrictions on two decay parameters are needed. I think that research on estimation of NSS model is under way.



Reference


Gürkaynak, R. S., B. Sack, and J. H. Wright (2007), "The U.S. Treasury Yield Curve: 1961 to the Present," Journal of Monetary Economics 54-8, 2291–2304.

Svensson, L.E. (1995), "Estimating Forward Interest Rates with the Extended Nelson and Siegel Method," Sveriges Riksbank Quarterly Review 3, 13–26.


1 comment:

  1. vegabook
    2023. 5. 28.
    Where is the input data? Is it already in zero curve format? The main use case for Nelson Siegel et al is for BONDS. Swaps are already very well arbitraged. But bonds curve fit needs to be done by minimizing DCF cashflow minimization to market price, which is quite a lot more complex on the data preparation side, and you'll need to weight by the inverse of duration so that long bonds don't dominate the optimization.

    Sang-Heon Lee
    2023. 5. 29.
    The input data is in the code after the line: str.zero <- ".

    I use the data from Gürkaynak et al. (2007), which is the US Treasury zero curve.

    I agree that using the market price of the bond as the input data and the inverse of the duration as the weight is a good choice.

    In particular, Andreasen et al. (2019) use the market price of the bond as the input instead of the zero rate and show the feasibility and desirability of the one-step approach you suggest.

    Thank you for good suggestion.

    Andreasen, M. M., J. H. E. Christensen, and G. D. Rudebusch (2019). Term Structure Analysis with Big Data: One-Step Estimation Using Bond Prices, Journal of Econometrics 212(1): 26-46.

    ReplyDelete