Nelson-Siegel-Svennson model for interest rates

Fixed income
Author

Beniamino Sartini

Published

September 30, 2023

Modified

November 23, 2023

A common approach that can be used to fit the interest rates for different maturities given a set of key-rates is linear interpolation. However a more flexible approach, often used by central banks, is to calibrate the parameters of a model and then to use it to recover the interest rate for any time to maturity. In Equation 1 we present a modified version of the original Nelson-Siegel model, called Nelson-Siegel-Svennson model, in which are introduced two additional parameters, \(\beta_3\) and \(\tau_2\), in order to have more flexibility.

\[\hat{h}(0,T) = \beta_0 + \beta_1 f^1(T, \tau_1) + \beta_2 f^2(T, \tau_1) + \beta_3 f^2(T, \tau_2) \tag{1}\]

where:

\[f^1(T,\tau_1) = \frac{1 - e^{-\frac{T}{\tau_1}}}{\frac{T}{\tau_1}} \tag{2}\]

and

\[f^2(T,\tau) = \frac{1 - e^{-\frac{T}{\tau}}}{\frac{T}{\tau}} - e^{-\frac{T}{\tau}} \tag{3}\]

Under this model we can interpret the first two parameters \(\beta_0\) and \(\beta_1\) in the following way:

In order to have this constraints valid we will need to perform a constraint optimization problem in which the function to optimize is given by the mean square error of the fitted value with respect to the real values (over discrete maturities \((T_a, ....T_b)\)):

\[\underset{\tiny \beta_0, \beta_1, \beta_2, \beta_3, \tau_1, \tau_2}{\text{argmin}}\biggl\{ \sum_{i = a}^{i = b} \bigl[ h(0,T_i) - \hat{h}(0,T_i) \bigl]^2 \biggl\} \tag{4}\] Subject to the constraint:

\[\underset{\beta_0, \beta_1}{argmin}\bigl\{ | \beta_0 + \beta_1 - h(0,T)| \leq \alpha\bigl\} \tag{5}\]

where \(\alpha = 0.01\). is an arbitrary threshold level that establish the accuracy of the constraint. To a lower threshold correspond a better fit, however if we set it to low we may not reach a set of parameters that satisfy it.

In order to find the optimal parameters, it is necessary to solve a minimization problem starting from a set of 6 parameters, in general arbitrary or randomly generated. In this case, the following algorithm was implemented:

  1. Fix \(\beta_0\) and \(\beta_1\) both equal to \(\frac{h(0,t_1)}{2}\), in such a way the initial parameters respect the constraint in Equation 5.
  2. Perform the minimization problem in Equation 4 subject the constraints.

1 Implementation in R

nelson_siegel <- function(params){
  # Parameters 
  beta0 = params[1]
  beta1 = params[2]
  beta2 = params[3]
  beta3 = params[4]
  tau1 = params[5]
  tau2 = params[6]

  # Auxiliary function 1
  f1 <- function(tau, theta){
    exp(-tau/theta)
  }
  # Auxiliary function 2
  f2 <- function(tau, theta){ 
    (1 - f1(tau, theta))/(tau/theta)
  }
  # Auxiliary function 3
  f3 <- function(tau, theta){ 
    f2(tau, theta) - f1(tau, theta)
  }
  # Fit function  
  function(tau){
    beta0 + beta1*f2(tau, tau1) + beta2*f3(tau, tau1) + beta3*f3(tau, tau2)
  }
}
fit_nelson_siegel <- function(term_structure = NULL, tau = NULL, params = NULL, threshold = 0.01){
  
  if (missing(params) || is.null(params)) {
    params <- rep(0.1, 6)
  }
  
  # Set the contraints for beta0 and beta1 
  params <- c(term_structure[1]/2, term_structure[1]/2, params[3:6])
  
  loss_function <- function(params){
    
    beta0 = params[1]
    beta1 = params[2]
    beta3 = params[3]
    tau1 = params[4]
    tau2 = params[5]
    
    # Check the contraint
    contraint <- abs(beta0 + beta1 - term_structure[1])
    if (contraint > threshold ) {
      return(NA)
    }
    ns <- nelson_siegel(params)
    sum((term_structure - ns(tau))^2)
  }
  optimization <- optim(params, loss_function)
  optim_params <- c(beta0 = optimization$par[1], 
                    beta1 = optimization$par[2], 
                    beta2 = optimization$par[3],
                    beta3 = optimization$par[4],
                    tau1  = optimization$par[5], 
                    tau2  = optimization$par[6])
  
  ns <- nelson_siegel(params)
  fitted_values <- ns(tau)
  mse <- sd(fitted_values - term_structure)
  
  list( 
    term_structure = list(term_structure),
    params = list(names(optim_params)),
    start_params = list(params),
    optim_params = list(optim_params),
    fit = list(fitted_values),
    mse = mse
  )
}
Show the code
# euribor 2022-11-16
y <- c(1.1091, 1.6094, 1.7661, 1.8521, 1.881)
# times to maturities in years
tau <- c(1, 5, 10, 20, 30)

# calibrate parameters 
fit <- fit_nelson_siegel(term_structure = y/100, tau = tau, params = NULL, threshold = 0.01)
# create a fitting function with optimal parameters 
ns <- nelson_siegel(fit$optim_params[[1]])
# fitted yields under Nelson-Siegel 
y_pred <- ns(tau)*100

library(ggplot2)
ggplot()+
  geom_line(aes(tau, y))+
  geom_point(aes(tau, y), color = "red")+
  geom_line(aes(tau, y_pred), linetype="dashed", color="red")+
  theme_bw()

Note that it is always a good practice to rescale the rates/yield in the interval \([0,1]\). To see the potential problems generated by expressing the data on a percentage scale. We do not rescale the yield, letting them expressed in percentage. As shown in the following plot this causes fitting issues on the red curve.

Show the code
# calibrate parameters 
fit <- fit_nelson_siegel(term_structure = y, tau = tau, params = NULL, threshold = 0.01)
# create a fitting function with optimal parameters 
ns <- nelson_siegel(fit$optim_params[[1]])
# fitted yields under Nelson-Siegel 
y_pred <- ns(tau)

ggplot()+
  geom_line(aes(tau, y))+
  geom_point(aes(tau, y), color = "red")+
  geom_line(aes(tau, y_pred), linetype="dashed", color="red")+
  theme_bw()

Back to top

Citation

BibTeX citation:
@online{sartini2023,
  author = {Sartini, Beniamino},
  title = {Nelson-Siegel-Svennson Model for Interest Rates},
  date = {2023-09-30},
  url = {https://cryptoverser.org/articles/pricing-nelson-siegel/nelson_siegel.html},
  langid = {en}
}
For attribution, please cite this work as:
Sartini, Beniamino. 2023. “Nelson-Siegel-Svennson Model for Interest Rates.” September 30, 2023. https://cryptoverser.org/articles/pricing-nelson-siegel/nelson_siegel.html.