Bootstrapping discount factors from swap rates¶
Problem¶
From the par rates of interest rate swaps with annual payments, recover the discount factors for each maturity: first by solving a linear system (exercise 58), then with a reusable function, a recursive formula, and the inverse step from discount factors back to swap rates (exercise 59).
Method¶
A par swap with rate \(z_n\) and maturity \(n\) has zero value, so for each maturity:
Stacking the equations for \(n = 1, \dots, N\) gives a lower-triangular system \(A\,v = \mathbf{1}\), with the swap rates below the diagonal and \(1 + z_n\) on it. The same condition, solved one maturity at a time, gives the recursion and its inverse:
Code¶
clear all, clc
% Data Input
z = [0.021, 0.025, 0.032, 0.043]; % swap rates
z = z'; % we need a column vector
% Point 1
% 1. square matrix where each row holds the corresponding swap rate
M = repmat(z, 1, length(z)); % repeat z once vertically and 4 times horizontally
% 2. keep the lower triangular part (rates on and below the diagonal)
A = tril(M); % sets to zero everything above the main diagonal
% 3. add 1 on the main diagonal only
A = A + eye(length(z)); % eye() is the identity matrix: adding it puts a 1 on the diagonal
% Point 2
v = A \ ones(length(z), 1) % backslash: solves the linear system A*v = 1
check = A * v % verification: should return a vector of ones
% v are the discount factors for the 1, 2, 3 and 4 year maturities
% Point 2 alternative
v = inv(A) * ones(4, 1)
check = A * v
clear, clc
%% 1
% see the function F_bootstrap
%% 2
z = [0.0185; 0.0223; 0.0297; 0.0313];
v = F_bootstrap(z);
vv = zeros(length(z), 1);
for k = 1:length(z)
vv(k) = (1 - z(k)*sum(vv(1:k-1))) / (1 + z(k));
end
vv
%% 3
zz = zeros(length(v), 1);
for k = 1:length(v)
zz(k) = (1 - v(k)) / sum(v(1:k));
end
zz
import numpy as np
# Data Input
z = np.array([0.021, 0.025, 0.032, 0.043]) # swap rates
# %% Point 1
# 1. square matrix where each row holds the corresponding swap rate
M = np.tile(z.reshape(-1, 1), (1, len(z))) # MATLAB: repmat(z, 1, length(z)) with z a column
# reshape(-1, 1) turns z into a column
# 2. keep the lower triangular part
A = np.tril(M)
# 3. add 1 on the main diagonal
A = A + np.eye(len(z))
# %% Point 2
v = np.linalg.solve(A, np.ones(len(z))) # MATLAB: v = A \ ones(length(z), 1)
print("v =", v)
check = A @ v # verification: should return a vector of ones
print("check =", check)
# %% Point 2 alternative
v = np.linalg.inv(A) @ np.ones(4) # MATLAB: inv(A) * ones(4, 1)
print("v =", v)
check = A @ v
print("check =", check)
import numpy as np
from F_bootstrap import F_bootstrap
# %% 1
# see the function F_bootstrap
# %% 2
z = np.array([0.0185, 0.0223, 0.0297, 0.0313])
v = F_bootstrap(z)
vv = np.zeros(len(z))
for k in range(len(z)): # MATLAB: for k = 1:length(z)
vv[k] = (1 - z[k] * np.sum(vv[:k])) / (1 + z[k]) # MATLAB: sum(vv(1:k-1)) -> Python: vv[:k]
print("vv =", vv)
# %% 3
zz = np.zeros(len(v))
for k in range(len(v)):
zz[k] = (1 - v[k]) / np.sum(v[:k+1]) # MATLAB: sum(v(1:k)) -> Python: v[:k+1]
print("zz =", zz)
Results¶
| Maturity | Swap rate (ex. 58) | Discount factor | Swap rate (ex. 59) | Discount factor |
|---|---|---|---|---|
| 1 | 2.10% | 0.9794 | 1.85% | 0.9818 |
| 2 | 2.50% | 0.9517 | 2.23% | 0.9568 |
| 3 | 3.20% | 0.9091 | 2.97% | 0.9152 |
| 4 | 4.30% | 0.8417 | 3.13% | 0.8830 |
In exercise 58, \(A\,v\) returns a vector of ones. In exercise 59 the recursion gives the same discount factors as the linear system, and the inverse formula returns the original swap rates.
Takeaways¶
- Bootstrapping is a triangular linear system: each maturity only needs the shorter ones.
A \ bsolves the system directly and is the numerically preferred way;inv(A) * bgives the same result here but computes a full inverse that is not needed.- Being able to go from rates to discount factors and back is the check that both formulas are right.