Treasury Yield Curve¶
Background¶
Yield curve construction from treasury swap instruments.
Based on "Financial Engineering" course by L.A. Grzelak. The course is based on the book "Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes", by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak
Code¶
```python """ Yield curve construction from treasury swap instruments.
Based on "Financial Engineering" course by L.A. Grzelak. The course is based on the book "Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes", by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import enum import numpy as np from copy import deepcopy from scipy.interpolate import splrep, splev, interp1d
======================================================================¶
Functions / Classes¶
======================================================================¶
class OptionTypeSwap(enum.Enum): """Swap option type enumeration.""" RECEIVER = 1.0 PAYER = -1.0
def ir_swap(option_type, notional, strike, t, t_i, t_m, n, p0t): """ Compute interest rate swap value.
Parameters
----------
option_type : OptionTypeSwap
PAYER or RECEIVER
notional : float
Notional amount
strike : float
Strike rate
t : float
Current time
t_i : float
Swap start time
t_m : float
Swap end time
n : int
Number of payment dates
p0t : callable
Zero-coupon bond pricing function
Returns
-------
float
Swap value
"""
ti_grid = np.linspace(t_i, t_m, int(n))
tau = ti_grid[1] - ti_grid[0]
# Overwrite t_i if t > t_i
prev_ti = ti_grid[np.where(ti_grid < t)]
if np.size(prev_ti) > 0:
t_i = prev_ti[-1]
# Handle case when some payments are already done
ti_grid = ti_grid[np.where(ti_grid > t)]
temp = 0.0
for idx, ti in enumerate(ti_grid):
if ti > t_i:
temp = temp + tau * p0t(ti)
p_t_ti = p0t(t_i)
p_t_tm = p0t(t_m)
if option_type == OptionTypeSwap.PAYER:
swap = (p_t_ti - p_t_tm) - strike * temp
elif option_type == OptionTypeSwap.RECEIVER:
swap = strike * temp - (p_t_ti - p_t_tm)
return swap * notional
def p0t_model(t, ti, ri, method): """ Compute zero-coupon bond price using interpolation.
Parameters
----------
t : float or array
Time point(s)
ti : array
Interpolation nodes (times)
ri : array
Interpolation values (rates)
method : callable
Interpolation method
Returns
-------
float or array
Bond price P(0,t)
"""
r_interp = method(ti, ri)
r = r_interp(t)
return np.exp(-r * t)
def yield_curve(instruments, maturities, r0, method, tol): """ Compute yield curve from instrument prices.
Parameters
----------
instruments : list of callable
List of instrument pricing functions
maturities : array
Maturity points
r0 : array
Initial rate guess
method : callable
Interpolation method
tol : float
Convergence tolerance
Returns
-------
array
Optimal rates at maturities
"""
r0 = deepcopy(r0)
ri = multivariate_newton_raphson(r0, maturities, instruments, method,
tol=tol)
return ri
def multivariate_newton_raphson(ri, ti, instruments, method, tol): """ Multi-dimensional Newton-Raphson solver.
Parameters
----------
ri : array
Initial rate guess
ti : array
Time nodes
instruments : list of callable
Instrument pricing functions
method : callable
Interpolation method
tol : float
Convergence tolerance
Returns
-------
array
Converged rates
"""
err = 10e10
idx = 0
while err > tol:
idx = idx + 1
values = evaluate_instruments(ti, ri, instruments, method)
j = jacobian(ti, ri, instruments, method)
j_inv = np.linalg.inv(j)
err = -np.dot(j_inv, values)
ri[0:] = ri[0:] + err
err = np.linalg.norm(err)
print('index in the loop is', idx, ' Error is ', err)
return ri
def jacobian(ti, ri, instruments, method): """ Compute Jacobian matrix for Newton-Raphson.
Parameters
----------
ti : array
Time nodes
ri : array
Current rate estimate
instruments : list of callable
Instrument pricing functions
method : callable
Interpolation method
Returns
-------
ndarray
(n_instruments, n_instruments) Jacobian matrix
"""
eps = 1e-05
swap_num = len(ti)
j = np.zeros((swap_num, swap_num))
val = evaluate_instruments(ti, ri, instruments, method)
ri_up = deepcopy(ri)
for j_idx in range(0, len(ri)):
ri_up[j_idx] = ri[j_idx] + eps
val_up = evaluate_instruments(ti, ri_up, instruments, method)
ri_up[j_idx] = ri[j_idx]
dv = (val_up - val) / eps
j[:, j_idx] = dv[:]
return j
def evaluate_instruments(ti, ri, instruments, method): """ Evaluate all instruments at given rates.
Parameters
----------
ti : array
Time nodes
ri : array
Rates at nodes
instruments : list of callable
Instrument pricing functions
method : callable
Interpolation method
Returns
-------
array
Instrument values
"""
p0t_temp = lambda t: p0t_model(t, ti, ri, method)
val = np.zeros(len(instruments))
for i in range(0, len(instruments)):
val[i] = instruments[i](p0t_temp)
return val
def linear_interpolation(ti, ri): """ Linear interpolation function.
Parameters
----------
ti : array
Interpolation nodes
ri : array
Interpolation values
Returns
-------
callable
Interpolation function
"""
interpolator = lambda t: np.interp(t, ti, ri)
return interpolator
def main(): """Run yield curve construction.""" # ============= Parameters ============= tol = 1.0e-15 r0 = np.array([0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]) method = linear_interpolation
# Construct swaps for yield curve building
k = np.array([0.04 / 100.0, 0.16 / 100.0, 0.31 / 100.0, 0.81 / 100.0,
1.28 / 100.0, 1.62 / 100.0, 2.22 / 100.0, 2.30 / 100.0])
mat = np.array([1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0])
# ============= Build Instruments =============
swap1 = lambda p0t: ir_swap(OptionTypeSwap.PAYER, 1, k[0], 0.0, 0.0,
mat[0], 4 * mat[0], p0t)
swap2 = lambda p0t: ir_swap(OptionTypeSwap.PAYER, 1, k[1], 0.0, 0.0,
mat[1], 4 * mat[1], p0t)
swap3 = lambda p0t: ir_swap(OptionTypeSwap.PAYER, 1, k[2], 0.0, 0.0,
mat[2], 4 * mat[2], p0t)
swap4 = lambda p0t: ir_swap(OptionTypeSwap.PAYER, 1, k[3], 0.0, 0.0,
mat[3], 4 * mat[3], p0t)
swap5 = lambda p0t: ir_swap(OptionTypeSwap.PAYER, 1, k[4], 0.0, 0.0,
mat[4], 4 * mat[4], p0t)
swap6 = lambda p0t: ir_swap(OptionTypeSwap.PAYER, 1, k[5], 0.0, 0.0,
mat[5], 4 * mat[5], p0t)
swap7 = lambda p0t: ir_swap(OptionTypeSwap.PAYER, 1, k[6], 0.0, 0.0,
mat[6], 4 * mat[6], p0t)
swap8 = lambda p0t: ir_swap(OptionTypeSwap.PAYER, 1, k[7], 0.0, 0.0,
mat[7], 4 * mat[7], p0t)
instruments = [swap1, swap2, swap3, swap4, swap5, swap6, swap7, swap8]
# ============= Determine Optimal Spine Points =============
ri = yield_curve(instruments, mat, r0, method, tol)
print('\n Spine points are', ri, '\n')
# ============= Build Zero-Coupon Bond Curves =============
p0t_initial = lambda t: p0t_model(t, mat, r0, method)
p0t = lambda t: p0t_model(t, mat, ri, method)
# ============= Price Back the Swaps =============
swaps_model = np.zeros(len(instruments))
swaps_initial = np.zeros(len(instruments))
for i in range(0, len(instruments)):
swaps_initial[i] = instruments[i](p0t_initial)
swaps_model[i] = instruments[i](p0t)
print('Prices for Par Swaps (initial) = ', swaps_initial, '\n')
print('Prices for Par Swaps = ', swaps_model, '\n')
======================================================================¶
Main¶
======================================================================¶
if name == "main": main() ```
Exercises¶
Exercise 1. Describe the difference between a yield curve constructed from treasury instruments versus one constructed from swap instruments. Which is more commonly used for derivative pricing?
Solution to Exercise 1
Treasury yield curves are built from government bonds, which are considered default-free. They reflect the risk-free rate but may include liquidity premia and are affected by government supply/demand dynamics. Swap yield curves are built from interest rate swaps, which reflect interbank credit risk (historically LIBOR, now SOFR-based). Swap curves are smoother because swaps are standardized OTC contracts available at many maturities.
For derivative pricing, swap curves (specifically OIS curves for discounting) are more commonly used because derivatives are typically collateralized, and the collateral rate matches OIS. Treasury curves are used for government bond analytics and as benchmarks.
Exercise 2. The Newton-Raphson method requires an initial guess for the rates. What happens if the initial guess is far from the solution, and how does the code handle this?
Solution to Exercise 2
If the initial guess is far from the solution, Newton-Raphson may converge slowly, diverge, or converge to a wrong local minimum. The code uses \(r_0 = 0.01\) for all spine points, which is a reasonable initial guess for typical interest rate environments (\(0-5\%\)). The convergence is monitored via the norm of the update vector, and the loop continues until \(\|\text{err}\| < \text{tol}\). In practice, if the market swap rates are reasonable, this initial guess leads to convergence within \(5-10\) iterations.
Exercise 3. If par swap rates are \(\{2\%, 3\%, 4\%, 5\%\}\) at maturities \(\{1, 5, 10, 30\}\) years, the curve is upward-sloping. What does this imply about the market's expectation for future short-term rates?
Solution to Exercise 3
An upward-sloping yield curve implies that longer-term rates are higher than shorter-term rates. Under the expectations hypothesis, this means the market expects short-term rates to rise in the future:
However, this interpretation is complicated by the term premium: investors may demand higher yields for longer maturities due to increased interest rate risk, even if they expect rates to remain constant. The actual expectation component and the risk premium component cannot be separated from the yield curve alone.
Exercise 4.
Explain the role of the convergence tolerance parameter tol = 1e-15 and why such high precision is used.
Solution to Exercise 4
The tolerance \(10^{-15}\) requires the Newton-Raphson solution to be accurate to near machine precision (double-precision floating point has about 16 significant digits). This extreme precision is used because:
- Consistency: Even small errors in the yield curve can propagate and amplify in derivative pricing, especially for long-dated or exotic instruments.
- Greeks accuracy: Finite-difference Greeks involve subtracting nearby prices; if the curve is not solved precisely, the price differences are contaminated by curve-fitting noise rather than genuine sensitivities.
- Repricing: Par swaps should reprice to exactly zero; any residual value indicates a calibration error.