Skip to content

Commit e8e9e0e

Browse files
authored
feature: Estimate convergence factor alpha
Estimate convergence factor alpha. The parameter determines the convergence speed of the yield curve towards the Ultimate Forward Rate (UFR). The parameter is estimated by finding the smallest value such that the difference between forward rate at convergence maturity and UFR is smaller than 1bps. Closes PR #4
2 parents af397de + 7c188c4 commit e8e9e0e

File tree

7 files changed

+161
-29
lines changed

7 files changed

+161
-29
lines changed

.github/workflows/python-package.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,5 @@ jobs:
7676
TWINE_USERNAME: __token__
7777
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
7878
run: |
79-
twine upload -r testpypi dist/*
80-
81-
# twine upload dist/*
79+
twine upload dist/*
80+
# For testing only: twine upload -r testpypi dist/*

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ This Python package implements the Smith-Wilson yield curve fitting algorithm. I
44
<br /><br />
55

66
## How to use the package
7-
1. To use the Smith-Wilson fitting algorithm, first import the Python package and specify the inputs. In the example below the inputs are zero-coupon rates with annual frequency up until year 25. The UFR is 2.9% and the convergence parameter alpha is 0.128562. The `terms` list defines the list of maturities, in this case `[1.0, 2.0, 3.0, ..., 25.0]`
7+
1. Install the package with `pip install smithwilson`
8+
2. To use the Smith-Wilson fitting algorithm, first import the Python package and specify the inputs. In the example below the inputs are zero-coupon rates with annual frequency up until year 25. The UFR is 2.9% and the convergence parameter alpha is 0.128562. The `terms` list defines the list of maturities, in this case `[1.0, 2.0, 3.0, ..., 25.0]`
89
```py
910
import smithwilson as sw
1011

@@ -22,7 +23,7 @@ This Python package implements the Smith-Wilson yield curve fitting algorithm. I
2223

2324
```
2425

25-
1. Specify the targeted output maturities. This is the set of terms you want to get rates fitted by Smith-Wilson.
26+
3. Specify the targeted output maturities. This is the set of terms you want to get rates fitted by Smith-Wilson.
2627
Expand the set of rates beyond the Last Liquid Point (e.g. extrapolate to 150 years with annual frequency):
2728
```py
2829
# Extrapolate to 150 years
@@ -41,14 +42,17 @@ This Python package implements the Smith-Wilson yield curve fitting algorithm. I
4142
terms_target = [0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0]
4243
```
4344

44-
1. Call the Smiwth-Wilson fitting algorithm. This returns the rates as numpy vector with each element corresponding to the maturity in `terms_target`
45+
4. Call the Smiwth-Wilson fitting algorithm. This returns the rates as numpy vector with each element corresponding to the maturity in `terms_target`
4546
```py
4647
# Calculate fitted rates based on actual observations and two parametes alpha & UFR
4748
fitted_rates = sw.fit_smithwilson_rates(rates_obs=rates, t_obs=terms,
48-
t_target=terms_target, alpha=alpha, ufr=ufr)
49+
t_target=terms_target, ufr=ufr,
50+
alpha=alpha) # Optional
4951
```
5052

51-
1. To display the results and/or processing them it can be useful to turn them into a table, here using the pandas library:
53+
The convergence parameter alpha is optional and will be estimated if not provided. The parameter determines the convergence speed of the yield curve towards the Ultimate Forward Rate (UFR). The parameter is estimated by finding the smallest value such that the difference between forward rate at convergence maturity and UFR is smaller than 1bps.
54+
55+
5. To display the results and/or processing them it can be useful to turn them into a table, here using the pandas library:
5256
```py
5357
# Ensure pandas package is imported
5458
import pandas as pd
@@ -102,6 +106,3 @@ In the last case, `t` can be any maturity vector, i.e. with additional maturitie
102106

103107
[EIOPA (2018). Technical documentation of the methodology to derive EIOPA’srisk-free interest rate term structures](https://eiopa.europa.eu/Publications/Standards/Technical%20Documentation%20(31%20Jan%202018).pdf); p.37-46
104108
<br /><br />
105-
106-
## Author
107-
[Dejan Simic](https://www.linkedin.com/in/dejsimic/)

requirements.txt

28 Bytes
Binary file not shown.

setup.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55

66
setuptools.setup(
77
name="smithwilson",
8-
version="0.1.0",
8+
version="0.2.0",
99
author="Dejan Simic",
10-
# author_email="dejan.simic",
1110
description=
1211
"Implementation of the Smith-Wilson yield curve fitting algorithm in Python for interpolations and extrapolations of zero-coupon bond rates",
1312
long_description=long_description,
@@ -23,5 +22,5 @@
2322
"Operating System :: OS Independent",
2423
],
2524
python_requires='>=3.7',
26-
install_requires=["numpy>=1.21.5"],
25+
install_requires=["numpy>=1.21.5", "scipy>=1.7.0"],
2726
)

smithwilson/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
2-
from smithwilson.core import *
1+
from smithwilson.core import calculate_prices, fit_convergence_parameter, fit_smithwilson_rates, ufr_discount_factor, fit_parameters, wilson_function

smithwilson/core.py

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from math import log
22
import numpy as np
3-
from typing import Union, List
3+
from scipy import optimize
4+
from typing import Union, List, Optional
45

56

67
def calculate_prices(rates: Union[np.ndarray, List[float]], t: Union[np.ndarray, List[float]]) -> np.ndarray:
@@ -49,7 +50,9 @@ def ufr_discount_factor(ufr: float, t: Union[np.ndarray, List[float]]) -> np.nda
4950
return np.exp(-ufr * t)
5051

5152

52-
def wilson_function(t1: Union[np.ndarray, List[float]], t2: Union[np.ndarray, List[float]], alpha: float, ufr: float) -> np.ndarray:
53+
def wilson_function(t1: Union[np.ndarray, List[float]],
54+
t2: Union[np.ndarray, List[float]],
55+
alpha: float, ufr: float) -> np.ndarray:
5356
"""Calculate matrix of Wilson functions
5457
5558
The Smith-Wilson method requires the calculation of a series of Wilson
@@ -94,7 +97,9 @@ def wilson_function(t1: Union[np.ndarray, List[float]], t2: Union[np.ndarray, Li
9497
return W
9598

9699

97-
def fit_parameters(rates: Union[np.ndarray, List[float]], t: Union[np.ndarray, List[float]], alpha: float, ufr: float) -> np.ndarray:
100+
def fit_parameters(rates: Union[np.ndarray, List[float]],
101+
t: Union[np.ndarray, List[float]],
102+
alpha: float, ufr: float) -> np.ndarray:
98103
"""Calculate Smith-Wilson parameter vector ζ
99104
100105
Given the Wilson-matrix, vector of discount factors and prices,
@@ -129,8 +134,10 @@ def fit_parameters(rates: Union[np.ndarray, List[float]], t: Union[np.ndarray, L
129134
return zeta
130135

131136

132-
def fit_smithwilson_rates(rates_obs: Union[np.ndarray, List[float]], t_obs: Union[np.ndarray, List[float]],
133-
t_target: Union[np.ndarray, List[float]], alpha: float, ufr: float) -> np.ndarray:
137+
def fit_smithwilson_rates(rates_obs: Union[np.ndarray, List[float]],
138+
t_obs: Union[np.ndarray, List[float]],
139+
t_target: Union[np.ndarray, List[float]],
140+
ufr: float, alpha: Optional[float] = None) -> np.ndarray:
134141
"""Calculate zero-coupon yields with Smith-Wilson method based on observed rates.
135142
136143
This function expects the rates and initial maturity vector to be
@@ -159,8 +166,9 @@ def fit_smithwilson_rates(rates_obs: Union[np.ndarray, List[float]], t_obs: Unio
159166
rates_obs: Initially observed zero-coupon rates vector before LLP of length n
160167
t_obs: Initially observed time to maturity vector (in years) of length n
161168
t_target: New targeted maturity vector (in years) with interpolated/extrapolated terms
162-
alpha: Convergence speed parameter
163169
ufr: Ultimate Forward Rate (annualized/annual compounding)
170+
alpha: (optional) Convergence speed parameter. If not provided estimated using
171+
the `fit_convergence_parameter()` function
164172
165173
Returns:
166174
Vector of zero-coupon rates with Smith-Wilson interpolated or extrapolated rates
@@ -173,6 +181,9 @@ def fit_smithwilson_rates(rates_obs: Union[np.ndarray, List[float]], t_obs: Unio
173181
t_obs = np.array(t_obs).reshape((-1, 1))
174182
t_target = np.array(t_target).reshape((-1, 1))
175183

184+
if alpha is None:
185+
alpha = fit_convergence_parameter(rates_obs=rates_obs, t_obs=t_obs, ufr=ufr)
186+
176187
zeta = fit_parameters(rates=rates_obs, t=t_obs, alpha=alpha, ufr=ufr)
177188
ufr_disc = ufr_discount_factor(ufr=ufr, t=t_target)
178189
W = wilson_function(t1=t_target, t2=t_obs, alpha=alpha, ufr=ufr)
@@ -183,3 +194,54 @@ def fit_smithwilson_rates(rates_obs: Union[np.ndarray, List[float]], t_obs: Unio
183194

184195
# Transform price vector to zero-coupon rate vector (1/P)^(1/t) - 1
185196
return np.power(1 / P, 1 / t_target) - 1
197+
198+
199+
def fit_convergence_parameter(rates_obs: Union[np.ndarray, List[float]],
200+
t_obs: Union[np.ndarray, List[float]],
201+
ufr: float) -> float:
202+
"""Fit Smith-Wilson convergence factor (alpha).
203+
204+
Args:
205+
rates_obs: Initially observed zero-coupon rates vector before LLP of length n
206+
t_obs: Initially observed time to maturity vector (in years) of length n
207+
ufr: Ultimate Forward Rate (annualized/annual compounding)
208+
209+
Returns:
210+
Convergence parameter alpha
211+
"""
212+
213+
# Last liquid point (LLP)
214+
llp = np.max(t_obs)
215+
216+
# Maturity at which forward curve is supposed to converge to ultimate forward rate (UFR)
217+
# See: https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/12092019-technical_documentation.pdf (chapter 7.D., p. 39)
218+
convergence_t = max(llp + 40, 60)
219+
220+
# Optimization function calculating the difference between UFR and forward rate at convergence point
221+
def forward_difference(alpha: float):
222+
# Fit yield curve
223+
rates = fit_smithwilson_rates(rates_obs=rates_obs, # Input rates to be fitted
224+
t_obs=t_obs, # Maturities of these rates
225+
t_target=[convergence_t, convergence_t + 1], # Maturity at which curve is supposed to converge to UFR
226+
alpha=alpha, # Optimization parameter
227+
ufr=ufr) # Ultimate forward rate
228+
229+
# Calculate the forward rate at convergence maturity - this is an approximation since
230+
# according to the documentation the minimization should be based on the forward intensity, not forward rate
231+
forward_rate = (1 + rates[1])**(convergence_t + 1) / (1 + rates[0])**(convergence_t) - 1
232+
233+
# Absolute difference needs to be smaller than 1 bps
234+
return -abs(forward_rate - ufr) + 1 / 10_000
235+
236+
# Minimize alpha w.r.t. forward difference criterion
237+
root = optimize.minimize(lambda alpha: alpha, x0=0.15, method='SLSQP', bounds=[[0.05, 1.0]],
238+
constraints=[{
239+
'type': 'ineq',
240+
'fun': forward_difference
241+
}],
242+
options={
243+
'ftol': 1e-6,
244+
'disp': True
245+
})
246+
247+
return float(root.x)

smithwilson/tests/test_core.py

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,10 @@ def test_fit_parameters(self):
145145

146146
def test_fit_smithwilson_rates_actual(self):
147147
"""Test estimation of yield curve fitted with the Smith-Wilson algorithm.
148-
This example uses an actual example from EIOPA. Deviations must be less than 1bps (0.01%).
149-
Source: https://eiopa.europa.eu/Publications/Standards/EIOPA_RFR_20190531.zip
150-
EIOPA_RFR_20190531_Term_Structures.xlsx; Tab: RFR_spot_no_VA; Switzerland
148+
149+
This example uses an actual example from EIOPA. Deviations must be less than 1bps (0.01%).
150+
Source: https://eiopa.europa.eu/Publications/Standards/EIOPA_RFR_20190531.zip
151+
EIOPA_RFR_20190531_Term_Structures.xlsx; Tab: RFR_spot_no_VA; Switzerland
151152
"""
152153

153154
# Input
@@ -186,11 +187,53 @@ def test_fit_smithwilson_rates_actual(self):
186187
np.testing.assert_almost_equal(actual, expected, decimal=4, err_msg="Fitted rates not matching")
187188

188189

189-
def test_fit_smithwilson_rates_random(self):
190-
"""Test estimation of yield curve fitted with the Smith-Wilson algorithm
191-
This test uses random data points.
190+
def test_fit_smithwilson_rates_incl_convergence(self):
191+
"""Test estimation of yield curve without known convergence factor alpha.
192+
193+
This example uses an actual example from EIOPA. Deviations must be less than 1bps (0.01%).
194+
Source: https://eiopa.europa.eu/Publications/Standards/EIOPA_RFR_20190531.zip
195+
EIOPA_RFR_20190531_Term_Structures.xlsx; Tab: RFR_spot_no_VA; Switzerland
192196
"""
193197

198+
# Input
199+
r = np.array([-0.00803, -0.00814, -0.00778, -0.00725, -0.00652,
200+
-0.00565, -0.0048, -0.00391, -0.00313, -0.00214,
201+
-0.0014, -0.00067, -0.00008, 0.00051, 0.00108,
202+
0.00157, 0.00197, 0.00228, 0.0025, 0.00264,
203+
0.00271, 0.00274, 0.0028, 0.00291, 0.00309]).reshape((-1, 1))
204+
t = np.array([float(y + 1) for y in range(len(r))]).reshape((-1, 1)) # 1.0, 2.0, ..., 25.0
205+
ufr = 0.029
206+
alpha = 0.128562
207+
208+
t_target = np.array([float(y + 1) for y in range(65)]).reshape((-1, 1))
209+
210+
# Expected Output
211+
expected = np.array([-0.00803, -0.00814, -0.00778, -0.00725, -0.00652,
212+
-0.00565, -0.0048, -0.00391, -0.00313, -0.00214,
213+
-0.0014, -0.00067, -0.00008, 0.00051, 0.00108,
214+
0.00157, 0.00197, 0.00228, 0.0025, 0.00264,
215+
0.00271, 0.00274, 0.0028, 0.00291, 0.00309,
216+
0.00337, 0.00372, 0.00412, 0.00455, 0.00501,
217+
0.00548, 0.00596, 0.00644, 0.00692, 0.00739,
218+
0.00786, 0.00831, 0.00876, 0.00919, 0.00961,
219+
0.01002, 0.01042, 0.01081, 0.01118, 0.01154,
220+
0.01189, 0.01223, 0.01255, 0.01287, 0.01318,
221+
0.01347, 0.01376, 0.01403, 0.0143, 0.01456,
222+
0.01481, 0.01505, 0.01528, 0.01551, 0.01573,
223+
0.01594, 0.01615, 0.01635, 0.01655, 0.01673]).reshape((-1, 1))
224+
225+
# Actual Output
226+
actual = sw.fit_smithwilson_rates(rates_obs=r, t_obs=t, t_target=t_target, ufr=ufr)
227+
228+
# Assert - Precision of 4 decimal points equals deviatino of less than 1bps
229+
self.assertEqual(type(actual), type(expected), "Returned types not matching")
230+
self.assertTupleEqual(actual.shape, expected.shape, "Shapes not matching")
231+
np.testing.assert_almost_equal(actual, expected, decimal=4, err_msg="Fitted rates not matching")
232+
233+
234+
def test_fit_smithwilson_rates_random(self):
235+
"""Test estimation of yield curve fitted with the Smith-Wilson algorithm using random data points."""
236+
194237
# Input
195238
r = np.array([0.02, 0.025, -0.033, 0.01, 0.0008]).reshape((-1, 1))
196239
t = np.array([0.25, 1.0, 5.0, 20.0, 25.0]).reshape((-1, 1))
@@ -209,4 +252,33 @@ def test_fit_smithwilson_rates_random(self):
209252
# Assert
210253
self.assertEqual(type(actual), type(expected), "Returned types not matching")
211254
self.assertTupleEqual(actual.shape, expected.shape, "Shapes not matching")
212-
np.testing.assert_almost_equal(actual, expected, decimal=8, err_msg="Fitted rates not matching")
255+
np.testing.assert_almost_equal(actual, expected, decimal=8, err_msg="Fitted rates not matching")
256+
257+
258+
259+
def test_fit_alpha(self):
260+
"""Test estimation of convergence factor alpha.
261+
262+
This example uses an actual example from EIOPA. Deviations must be less than 0.001.
263+
Source: https://eiopa.europa.eu/Publications/Standards/EIOPA_RFR_20190531.zip
264+
EIOPA_RFR_20190531_Term_Structures.xlsx; Tab: RFR_spot_no_VA; Switzerland
265+
"""
266+
267+
# Input
268+
r = np.array([-0.00803, -0.00814, -0.00778, -0.00725, -0.00652,
269+
-0.00565, -0.0048, -0.00391, -0.00313, -0.00214,
270+
-0.0014, -0.00067, -0.00008, 0.00051, 0.00108,
271+
0.00157, 0.00197, 0.00228, 0.0025, 0.00264,
272+
0.00271, 0.00274, 0.0028, 0.00291, 0.00309]).reshape((-1, 1))
273+
t = np.array([float(y + 1) for y in range(len(r))]).reshape((-1, 1)) # 1.0, 2.0, ..., 25.0
274+
ufr = 0.029
275+
276+
# Expected Output
277+
alpha_expected = 0.128562
278+
279+
# Actual Output
280+
alpha_actual = sw.fit_convergence_parameter(rates_obs=r, t_obs=t, ufr=ufr)
281+
282+
# Assert - Precision of 4 decimal points equals deviatino of less than 1bps
283+
self.assertEqual(type(alpha_actual), type(alpha_expected), "Returned types not matching")
284+
self.assertAlmostEqual(alpha_actual, alpha_expected, msg="Alpha not matching", delta=0.001)

0 commit comments

Comments
 (0)