Smith-Wilson Model Overview#

This Jupyter notebook shows you how to load the smithwilson model included in the smithwilson project. It also walks you through steps to create the same model from scratch.

The Smith-Wilson model calculates extraporated interest rates using the Smith-Wilson method.

The Smith-Wilson method is used for extraporating risk-free interest rates under the Solvency II framework. The method is described in details in QIS 5 Risk-free interest rates – Extrapolation method, a technical paper issued by CEIOPS(the predecessor of EIOPA). The technical paper is available on EIOPA’s web site. Formulas and variables in this notebook are named consistently with the mathmatical symbols in the technical paper.

This project is inspired by a pure Python implementation of Smith-Wilson yield curve fitting algorithm created by Dejan Simic. His original work can be found on his github page.

About this notebook#

This notebook is included in lifelib package as part of the smithwilson project.

Click the badge below to run this notebook online on Google Colab. You need a Google account and need to be logged in to it to run this notebook on Google Colab. Run on Google Colab

The next code cell below is relevant only when you run this notebook on Google Colab. It installs lifelib and creates a copy of the library for this notebook.

[1]:
import sys, os

if 'google.colab' in sys.modules:
    lib = 'smithwilson'; lib_dir = '/content/'+ lib
    if not os.path.exists(lib_dir):
        !pip install lifelib
        import lifelib; lifelib.create(lib, lib_dir)

    %cd $lib_dir

Reading in the complete model#

The complete model is included under the smithwilson project in lifelib package. Load a model from model folder in your project folder by modelx.read_model function. The example blow shows how to do it. Note that you need two backslashes to separate folders:

[2]:
import modelx as mx
m = mx.read_model("model")   #  Need 2 backslashes as a separator on Windows e.g. "C:\\Users\\fumito\\model"

The model has only one space, named SmithWilson. The space contains a few cells:

[3]:
s = m.SmithWilson
s.cells
[3]:
{u,
 m,
 mu,
 W,
 m_vector,
 mu_vector,
 W_matrix,
 zeta_vector,
 zeta,
 P,
 R}

It also contains references (refs), such as spot_rates, N, UFR and alpha. By default, these values are set equal to the values used in Dejan’s reference model. The original source of the input data is Switzerland EIOPA spot rates with LLP 25 years available from the following URL.

Source: https://eiopa.europa.eu/Publications/Standards/EIOPA_RFR_20190531.zip; EIOPA_RFR_20190531_Term_Structures.xlsx; Tab: RFR_spot_no_VA

[4]:
s.N
[4]:
25
[5]:
s.UFR
[5]:
0.028587456851912472
[6]:
s.alpha
[6]:
0.128562
[7]:
s.spot_rates
[7]:
[-0.00803,
 -0.00814,
 -0.00778,
 -0.00725,
 -0.00652,
 -0.00565,
 -0.0048,
 -0.00391,
 -0.00313,
 -0.00214,
 -0.0014,
 -0.00067,
 -8e-05,
 0.00051,
 0.00108,
 0.00157,
 0.00197,
 0.00228,
 0.0025,
 0.00264,
 0.00271,
 0.00274,
 0.0028,
 0.00291,
 0.00309]

R calculates the extrapoted spot rate for a give time index \(i\).

[8]:
[s.R[i] for i in range(10, 151, 10)]
[8]:
[-0.0021400000002478325,
 0.0026399999998294454,
 0.004987777012509076,
 0.009589281258343796,
 0.013152667277319896,
 0.0157106404653784,
 0.01758288328289881,
 0.018999270994646267,
 0.020104712394634294,
 0.020990537324858893,
 0.021716028320261982,
 0.02232103673631003,
 0.02283325665510394,
 0.023272509104879324,
 0.023653347800582036]

For \(i = 1,\dots,N\), R[i] is the same as spot_rates[i-1].

[9]:
[s.spot_rates[i-1] for i in range(10, 26, 5)]
[9]:
[-0.00214, 0.00108, 0.00264, 0.00309]

Building the Smith-Wilson model from scratch#

We now try to create the smithwilson model from scratch. The model we create is essentially the same as the model included in the smithwilson project, excpt for docstrings.

Below are the steps to create the model. 1. Create a model and space. 2. Input values to as references. 3. Define cells. 4. Get the results. 5. Save the model.

1. Create a model and space#

First, we create an empty model named smithwilson2, and also an empty space named SmithWilson in the model. The following statement creates the model and space, and assign the space to a name s2.

[10]:
s2 = mx.new_model(name="smithwilson2").new_space(name="SmithWilson")

2. Input values to as references#

In this step, we create references in the SmithWilson space, and assign input values to the references. We will create cells and define their formulas in the sapce in the next step, and those references are referred by the formulas of the cells.

The values are taken from simicd/smith-wilson-py

[11]:
# Annual compound spot rates for time to maturities from 1 to 25 years
s2.spot_rates = [
    -0.00803, -0.00814, -0.00778, -0.00725, -0.00652,
    -0.00565, -0.0048, -0.00391, -0.00313, -0.00214,
    -0.0014, -0.00067, -0.00008, 0.00051, 0.00108,
    0.00157, 0.00197, 0.00228, 0.0025, 0.00264,
    0.00271, 0.00274, 0.0028, 0.00291, 0.00309]

s2.N = 25   # Number of time to maturities.

s2.alpha = 0.128562  # Alpha parameter in the Smith-Wilson functions

ufr = 0.029    # Annual compound

from math import log
s2.UFR = log(1 + ufr) # Continuous compound UFR, 0.028587456851912472

You also nee to import log and exp from math module for later use. We also use numpy later, so import numpy as np. These functions and module need to be accessible from cells in SmithWilson space, so assign them to refs.

[12]:
from math import log, exp
import numpy as np

s2.log = log
s2.exp = exp
s2.np = np

3. Define cells#

In the previous step, we have assigned all the necessary inputs in the SmithWilson space. In this step we move on to defining cells.

We use defcells decorator to define cells from Python functions. defcells decorator creates cells in the current space, so confirm the SmithWilson space we just created is set to the current space by the following code.

[13]:
mx.cur_space()
[13]:
<UserSpace smithwilson2.SmithWilson>

The names of the cells below are set consistent with the mathmatical symbols in the technical paper.

  • u(i): Time at each i in years. Time steps can be uneven. For the maturities of the zero coupon bonds with known prices \(u_i\)

  • m(i): The market prices of the zero coupon bonds, \(m_i\)

  • mu(i): Ultimate Forward Rate (UFR) discount factors, \(\mu_i\)

  • W(i, j): The Wilson functions, \(W(t_i, u_j)\)

[14]:
@mx.defcells
def u(i):
    """Time to maturities"""
    return i
[15]:
@mx.defcells
def m(i):
    """Observed zero-coupon bond prices"""
    return (1 + spot_rates[i-1]) ** (-u[i])


@mx.defcells
def mu(i):
    """Ultimate Forward Rate (UFR) discount factors"""
    return exp(-UFR * u[i])
[16]:
@mx.defcells
def W(i, j):
    """The Wilson functions"""

    t = u[i]
    uj = u[j]

    return exp(-UFR * (t+uj)) * (
            alpha * min(t, uj) - 0.5 * exp(-alpha * max(t, uj)) * (
                    exp(alpha*min(t, uj)) - exp(-alpha*min(t, uj))))

We want to use Numpy’s vector and matrix operations to solve for \(\zeta\), so we create a vector or matrix version of cells for each of m, mu, W. These cells have no parameter and return numpy arrays.

[17]:
@mx.defcells
def m_vector():
    return np.array([m(i) for i in range(1, N+1)])

@mx.defcells
def mu_vector():
    return np.array([mu(i) for i in range(1, N+1)])

@mx.defcells
def W_matrix():
    return np.array(
        [[W(i, j) for j in range(1, N+1)] for i in range(1, N+1)]
    )

zeta_vector cells carries out the matrix-vector calcuculation: \(\zeta = \bf W^{-1}(\bf m - {\mu})\).

zeta extracts from an element from zeta_vector for each i

[18]:
@mx.defcells
def zeta_vector():
    return np.linalg.inv(W_matrix()) @ (m_vector() - mu_vector())

@mx.defcells
def zeta(i):
    return zeta_vector()[i-1]

P(i) cells calculates bond prices from mu, zeta and W. The values of P(i) should be the same as those of m(i) for i=1,...,N .

R(i) are the extaporated annual compound rates. The values of R(i) should be the same as those of spot_rates[i-1] for i=1,...,N.

[19]:
@mx.defcells
def P(i):
    """Zero-coupon bond prices calculated by Smith-Wilson method."""
    return mu(i) + sum(zeta(j) * W(i, j) for j in range(1, N+1))


@mx.defcells
def R(i):
    """Extrapolated rates"""
    return (1 / P(i)) ** (1 / u(i)) - 1

4. Get the results#

You can check that the cells you define above exists in the SmithWilson space by getting the space’s cells attribute.

[20]:
s2.cells
[20]:
{u,
 m,
 mu,
 W,
 m_vector,
 mu_vector,
 W_matrix,
 zeta_vector,
 zeta,
 P,
 R}

R cells calculates or holds the extraporated spot rates. You can see that for i=1,...,25, the values are the same ase the sport_rates.

The code below outputs R(i) for i=10, 15, 20, ..., 100

[21]:
[s2.R[i] for i in range(10, 101, 5)]
[21]:
[-0.0021400000002478325,
 0.001079999999791026,
 0.0026399999998294454,
 0.0030899999998661443,
 0.004987777012509076,
 0.007366600230549469,
 0.009589281258343796,
 0.011517021559910967,
 0.013152667277319896,
 0.014535885669793025,
 0.0157106404653784,
 0.016715719536043006,
 0.01758288328289881,
 0.018337416110107085,
 0.018999270994646267,
 0.019584201846686966,
 0.020104712394634294,
 0.020570802184906256,
 0.020990537324858893]

5. Save the model#

You can write the model by write_model. The model is written to files under the folder you specify as the second paramter. Later you can read the model by read_model.

mx.write_model(mx.cur_model(), "your_folder")