EIOPA RISK FREE CURVE MONTHLY PUBLICATION RECALCULATION#

The risk free curve is one of the principal inputs into an economic scenario generator. This notebook recalculates the risk free curve using the parameters that are claimed to be used. The European Insurance and Occupational Pensions Authority (EIOPA) publishes their own yield curve prediction. To do this they use the Smith & Wilson algorithm.

Goal#

The goal of this test is to replicate the EIOPA yield curve This test will use the methodology that EIOPA claims it is using and the calibration vector that they publish. If the test is passed, the user can be more confident, that EIOPA risk free rate (RFR) curve was generated using the described methodology/calibration and that the process was implemented correctly.

Note on Smith & Wilson algorithm#

To replicate the calculations, this example uses a modificatied Smith&Wilson implementation (The original implementation is avalible on GitHub: - Python - Matlab - JavaScript

Data requirements#

In this example, we look at the EIOPA risk free rate publication for August 2022. The publication can be found on the EIOPA RFR website.

The observed maturities M_Obs and the calibrated vector Qb can be found in the Excel sheet EIOPA_RFR_20220831_Qb_SW.xlsx.

For this example, the curve without the volatility adjustment (VA) is used. It can be found in the sheet SW_Qb_no_VA. This example is focused on the EUR curve, but this example can be easily modified for any other curve.

The target maturities (T_Obs), the additional parameters (UFR and alpha), and the given curve can be found in the Excel EIOPA_RFR_20220831_Term_Structures.xlsx, sheet RFR_spot_no_VA.

Success criteria#

In this example, the following success criteria is proposed: - Maximum difference between the given curve and the recalculated curve is less than 100 bps. - Average difference between the curves is less than 10 bps.

[1]:
test_statistics_max_diff_in_bps = 0.1
test_statistics_average_diff_in_bps = 0.05

The success function is called at the end of the test to confirm if the success criteria have been meet.

[2]:
def SuccessTest(TestStatistics, threshold_max, threshold_mean):
    if max(TestStatistics)<threshold_max:
        print("Test passed")
    else:
        print("Test failed")

    if np.mean(TestStatistics)<threshold_mean:
        print("Test passed")
    else:
        print("Test failed")

This implementation uses three well established Python packages widely used in the financial industry. Pandas (https://pandas.pydata.org/docs/), Numpy (https://numpy.org/doc/), and Matplotlib (https://matplotlib.org/stable/index.html)

[3]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

Step 1: Data#

Insertion of data provided by EIOPA.

[4]:
# Maturity of observations:
M_Obs = np.transpose(np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]))

# Ultimate froward rate ufr represents the rate to which the rate curve will converge as time increases:
ufr = 0.0345

# Convergence speed parameter alpha controls the speed at which the curve converges towards the ufr from the last liquid point:
alpha = 0.123101

# For which maturities do we want the SW algorithm to calculate the rates. In this case, for every year up to 65:
M_Target = np.transpose(np.arange(1,150))

# Qb calibration vector published by EIOPA for the August 2022 calibration:
Qb = np.transpose(np.array([16.6492808327834,
                            -15.5532139436678,
                            6.35667251451134,
                            -1.23854722782483,
                            0.365103848953126,
                            -1.0571571437455,
                            1.33917386115124,
                            -0.278129268962339,
                            -2.90540200100003,
                            10.0852060296744,
                            -13.5164497129641,
                            7.48340006599309,
                            -0.030860450530635,
                            -0.02983127165842,
                            -2.20860220321924,
                            0.022505350081095,
                            0.021754809164905,
                            0.021029298371102,
                            0.020327982959016,
                            0.888352798117858]))

Step 2: Smith & Wilson calculation functions#

In this step, the independent version of the Smith&Wilson algorithm is implemented. To do this, two functions are taken from the repository and modified to accept the product of Q*b instead of the calibration vector b.

[5]:
def SWExtrapolate(M_Target, M_Obs, Qb, ufr, alpha):
    # SWEXTRAPOLATE Interpolate or/and extrapolate rates for targeted maturities using a Smith-Wilson algorithm.
    # out = SWExtrapolate(M_Target, M_Obs, Qb, ufr, alpha) calculates the rates for maturities specified in M_Target using the calibration vector b.
    #
    # Arguments:
    #    M_Target = k x 1 ndarray. Each element represents a bond maturity of interest. Ex. M_Target = [[1], [2], [3], [5]]
    #    M_Obs =    n x 1 ndarray. Observed bond maturities used for calibrating the calibration vector b. Ex. M_Obs = [[1], [3]]
    #    Qb =       n x 1 ndarray. Calibration vector calculated on observed bonds.
    #    ufr =      1 x 1 floating number. Representing the ultimate forward rate.
    #       Ex. ufr = 0.042
    #    alpha =    1 x 1 floating number. Representing the convergence speed parameter alpha. Ex. alpha = 0.05
    #    rates
    #
    # Returns:
    #    k x 1 ndarray. Represents the targeted rates for a zero-coupon bond. Each rate belongs to a targeted zero-coupon bond with a maturity from T_Target. Ex. r = [0.0024; 0.0029; 0.0034; 0.0039]
    #
    # For more information see https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/12092019-technical_documentation.pdf

    def SWHeart(u, v, alpha):
        # SWHEART Calculate the heart of the Wilson function.
        # H = SWHeart(u, v, alpha) calculates the matrix H (Heart of the Wilson
        # function) for maturities specified by vectors u and v. The formula is
        # taken from the EIOPA technical specifications paragraph 132.
        #
        # Arguments:
        #    u =     n_1 x 1 vector of maturities. Ex. u = [1; 3]
        #    v =     n_2 x 1 vector of maturities. Ex. v = [1; 2; 3; 5]
        #    alpha = 1 x 1 floating number representing the convergence speed parameter alpha. Ex. alpha = 0.05
        #
        # Returns:
        #    n_1 x n_2 matrix representing the Heart of the Wilson function for selected maturities and parameter alpha. H is calculated as in the paragraph 132 of the EIOPA documentation.
        #
        # For more information see https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/12092019-technical_documentation.pdf

        u_Mat = np.tile(u, [v.size, 1]).transpose()
        v_Mat = np.tile(v, [u.size, 1])
        return 0.5 * (alpha * (u_Mat + v_Mat) + np.exp(-alpha * (u_Mat + v_Mat)) - alpha * np.absolute(u_Mat-v_Mat) - np.exp(-alpha * np.absolute(u_Mat-v_Mat))); # Heart of the Wilson function from paragraph 132

    H = SWHeart(M_Target, M_Obs, alpha) # Heart of the Wilson function from paragraph 132
    p = np.exp(-np.log(1+ufr)* M_Target) + np.diag(np.exp(-np.log(1+ufr) * M_Target)) @ H @ Qb # Discount pricing function for targeted maturities from paragraph 147
    return p ** (-1/ M_Target) -1 # Convert obtained prices to rates and return prices

Step 3: Generation of the final output curve#

The observed maturities, target maturities, and the model parameters provided by EIOPA are used to generate the target curve.

[6]:
r_Target = SWExtrapolate(M_Target,M_Obs, Qb, ufr, alpha)
r_Target = pd.DataFrame(r_Target,columns=['Recalculated rates'])

r_Target.head()
[6]:
Recalculated rates
0 0.017450
1 0.020845
2 0.021150
3 0.021422
4 0.021729

Step 4: Comparson test#

Comparison of the output curve with the final curve provided by EIOPA. The test is passed if the success criteria is reached.

The provided curve is availabe in the Excel sheet: EIOPA_RFR_20220831_Term_Structures in the sheet RFR_spot_no_VA.

[7]:
EUR_curve = np.transpose(np.array([0.01745, 0.02085, 0.02115, 0.02142, 0.02173, 0.02201, 0.02227, 0.02261, 0.02295, 0.02333, 0.02382, 0.0239, 0.024, 0.02411, 0.02408, 0.02384, 0.02347, 0.02308, 0.02274, 0.02249, 0.02235, 0.02231, 0.02235, 0.02244, 0.02258, 0.02274, 0.02293, 0.02313, 0.02334, 0.02356, 0.02378, 0.02401, 0.02423, 0.02445, 0.02467, 0.02488, 0.02509, 0.02529, 0.02549, 0.02568, 0.02587, 0.02605, 0.02622, 0.02639, 0.02656, 0.02672, 0.02687, 0.02702, 0.02716, 0.0273, 0.02743, 0.02756, 0.02769, 0.02781, 0.02793, 0.02804, 0.02815, 0.02826, 0.02836, 0.02846, 0.02856, 0.02865, 0.02874, 0.02883, 0.02892, 0.029,0.02908, 0.02916, 0.02924, 0.02931, 0.02939, 0.02946, 0.02953, 0.02959, 0.02966, 0.02972, 0.02978, 0.02984, 0.0299, 0.02996, 0.03001, 0.03007, 0.03012, 0.03017, 0.03022, 0.03027, 0.03032, 0.03037, 0.03042, 0.03046, 0.03051, 0.03055, 0.03059, 0.03063, 0.03067, 0.03071, 0.03075, 0.03079, 0.03083, 0.03086, 0.0309, 0.03094, 0.03097, 0.031, 0.03104, 0.03107, 0.0311, 0.03113, 0.03116, 0.03119, 0.03122, 0.03125, 0.03128, 0.03131, 0.03134, 0.03137, 0.03139, 0.03142, 0.03144, 0.03147, 0.03149, 0.03152, 0.03154, 0.03157, 0.03159, 0.03161, 0.03164, 0.03166, 0.03168, 0.0317, 0.03172, 0.03174, 0.03177, 0.03179, 0.03181, 0.03183, 0.03185, 0.03186, 0.03188, 0.0319, 0.03192, 0.03194, 0.03196, 0.03197, 0.03199, 0.03201, 0.03203, 0.03204, 0.03206]))
EUR_curve = pd.DataFrame(EUR_curve,columns=['Given rates'])
EUR_curve.head()
[7]:
Given rates
0 0.01745
1 0.02085
2 0.02115
3 0.02142
4 0.02173

This implementation looks at two kinds of test statistics. The average deviation and the maximum deviation.

The average deviation is deffined as:

\[S_{AVERAGE} = \frac{1}{T} \sum_{t = 0}^T \left|r_{EIOPA}(t) - r_{EST}(t) \right|\]

The maximum deviation is deffined as:

\[S_{MAX} = \max_t \left| r_{EIOPA}(t) - r_{EST}(t) \right|\]

Where T is the maximum maturity available.

The average difference test is successful if:

\[S_{AVERAGE} < 0.05 bps\]

The maximum difference test is successful if:

\[S_{MAX} < 0.1 bps\]
[8]:
test_statistics_bdp = pd.DataFrame(abs(r_Target.values-EUR_curve.values)*10000, columns=["Abs diff in bps"])
test_statistics_bdp.head()
[8]:
Abs diff in bps
0 2.445960e-10
1 4.921938e-02
2 3.519053e-03
3 1.871457e-02
4 7.976257e-03
[9]:
x_data_label = range(2022,2022+r_Target.shape[0],1)
fig, (ax1, ax2) = plt.subplots(2,1)
ax1.plot(x_data_label, r_Target.values, color='tab:blue')
ax1.plot(x_data_label, EUR_curve.values, color='tab:orange')

ax1.set_ylabel("Yield")
ax1.set_title('Recalculated & given curves')

ax2.plot(x_data_label, test_statistics_bdp)
ax2.axhline(y = test_statistics_max_diff_in_bps, color = 'r', linestyle = '-')

ax2.set_xlabel("time")
ax2.set_ylabel("Difference (in bps)")
ax2.set_title('Absolute difference in yield curve')
fig.tight_layout(h_pad=2)
plt.show()
../../_images/libraries_economic_curves_EIOPA_RISK_FREE_CURVE_MONTHLY_PUBLICATION_RECALCULATION_22_0.png
[10]:
SuccessTest(test_statistics_bdp.values, test_statistics_max_diff_in_bps, test_statistics_average_diff_in_bps)
Test passed
Test passed

Step 5: Conclusion#

The EIOPA curve generated of the August 2022 submission has passed the success criteria. Based on the tests preformed, it is very likely that the curve was generated using the the Smith & Wilson algorithm with the calibration vector that was provided in the EIOPA_RFR_20220831_Qb_SW.xlsx sheet.