Coding towards CFA (19) – Curve Fitting with QuantLib
Linxiao Ma
Financial Data Architect | Hands-on Engineer | PhD | CFA Candidate | Distributed Database Expert | DolphinDB UK Rep. | Tech Blogger | Insane Coder
*More articles can be found from my blog site - https://dataninjago.com
In the previous blog post, I implemented the bootstrapping spot curve in both Python and DolphinDB. However, to boost productivity and improve reusability, it’s more efficient to leverage an established quantitative library. QuantLib is a widely-used open-source library for fixed income and derivatives pricing, offering a variety of curve fitting methods and built-in bootstrapping functionality. In this blog post, I will walk through the process of implementing curve fitting using QuantLib.
Here, we use the same use case as the one in the previous blog post, i.e., fitting the spot curve from a set of given par rates.
First, we specify the evaluation date and define the attributes of the par bonds, which will be used to define the underlying instruments.
QuantLib offers a wide range of curve fitting methods and models. More details can be found here. For this demo, I will test the following methods and models.
For each fitting method, we need to define a set of QuantLib helper instances, each representing the underlying bond at a specified maturity as an instrument in the bootstrapping process. In this demo, I will create a FixedRateBondHelper instance for each par bond at a particular maturity.
领英推荐
With the list of FixedRateBondHelper instances representing the par bonds, we can create the curve instance using the curve fitting method classes defined earlier. The detailed code flow can be found in the full version of the code attached below.
Output:
Full version of the code implemented for this blog post can be found here.
Full Code – QuantLib
import QuantLib as ql
import matplotlib.pyplot as plt
# Function to fit spot curve from a par curve using QuantLib
def fit_spot_curve_from_par(maturities, pars, bond_settings, fit_method, fit_model=None):
# Fetch the settings of the bond instruments used for curve fitting
calendar = bond_settings['calendar']
day_counter = bond_settings['day_counter']
settlement_days = bond_settings['settlement_days']
payment_freq = bond_settings['payment_freq']
settlement_date = calendar.advance(today, ql.Period(settlement_days, ql.Days))
# Loop through the provided maturities and create bond helpers for each
rate_helpers = []
for i in range(len(maturities)):
#Calculate maturity date by advancing settlement date by the maturity period
maturity_date = calendar.advance(settlement_date,
ql.Period(maturities[i], ql.Years))
#Define the bond cashflow schedule
schedule = ql.Schedule(
settlement_date,
maturity_date,
ql.Period(payment_freq),
calendar,
ql.ModifiedFollowing,
ql.ModifiedFollowing,
ql.DateGeneration.Backward,
False)
#Create the fixed rate bond helper for the maturity date
helper = ql.FixedRateBondHelper(
ql.QuoteHandle(ql.SimpleQuote(100)),
settlement_days,
100.0,
schedule,
[pars[i]/100],
day_counter
)
rate_helpers.append(helper)
if fit_model is None:
# fit the curve using the fit method specified through the fit_method parameter
curve = fit_method(today, rate_helpers, day_counter)
else:
# The fit_model is only provided when using FittedBondDiscountCurve
curve = fit_method(settlement_days, calendar, rate_helpers, day_counter, fit_model)
return curve
# a wrapper function for calling the fit_sport_curve_from_par function to fit the curve,
# and return the spot rates of the specified tenors.
def fit_curve(maturities, pars, bond_settings, fit_method, tenors, fit_model=None):
spot_curve = fit_spot_curve_from_par(
maturities,
pars,
bond_settings,
fit_method,
fit_model
)
calendar = bond_settings['calendar']
day_counter = bond_settings['day_counter']
settlement_days = bond_settings['settlement_days']
settlement_date = calendar.advance(today, ql.Period(settlement_days, ql.Days))
# For each specified tenor, calculate the spot rate using the fitted curve
return [spot_curve.zeroRate(calendar.advance(settlement_date, ql.Period(i, ql.Years)),
day_counter, ql.Continuous
).rate() for i in tenors
]
# Utility function for drawing line chart
def draw_chart(title, x_data, x_title, y1, y1_title, y2_data=None, y2_title=None, x_invert=False):
fig, ax1 = plt.subplots(figsize=(8, 4))
for y in y1:
ax1.plot(x_data, y[0], label=y[1], color=y[2], linewidth=2)
ax1.set_xlabel(x_title, fontsize=12)
ax1.set_ylabel(y1_title, color='b', fontsize=12)
ax1.tick_params(axis='y', labelcolor='b')
ax1.grid(True)
if y2_data is not None:
ax2 = ax1.twinx()
ax2.plot(x_data, y2_data, label=y2_title, color='red', linewidth=2)
ax2.set_ylabel(y2_title, color='red', fontsize=12)
ax2.tick_params(axis='y', labelcolor='red')
if x_invert:
ax=plt.gca()
ax.invert_xaxis()
plt.legend()
plt.title(title, fontsize=16)
fig.tight_layout()
plt.show()
# Define the dummy market par rates for demostration purpose
maturities = [1, 2, 3, 5, 7, 10, 20, 30]
pars = [4.26, 4.30, 4.36, 4.44, 4.52, 4.59, 4.85, 4.78]
# Set evaluation date for QuantLib
today = ql.Date(23, 12, 2024)
ql.Settings.instance().evaluationDate = today
# Define the underlying par bond settings
bond_settings = {
'calendar': ql.UnitedStates(ql.UnitedStates.NYSE),
'day_counter': ql.Actual360(),
'settlement_days': 2,
'payment_freq': ql.Semiannual
}
# Define a list of tenors for visualising the fitted curve
x = list(range(1, 31))
# Fit the curve using various methods
logLinearDiscount_y = fit_curve(maturities, pars, bond_settings,
ql.PiecewiseLogLinearDiscount, x)
logCubicDiscount_y = fit_curve(maturities, pars, bond_settings,
ql.PiecewiseLogCubicDiscount, x)
linearZero_y = fit_curve(maturities, pars, bond_settings,
ql.PiecewiseLinearZero, x)
NelsonSiegel_y = fit_curve(maturities, pars, bond_settings,
ql.FittedBondDiscountCurve, x, ql.NelsonSiegelFitting())
Svensson_y = fit_curve(maturities, pars, bond_settings,
ql.FittedBondDiscountCurve, x, ql.SvenssonFitting())
ExponentialSplines_y = fit_curve(maturities, pars, bond_settings,
ql.FittedBondDiscountCurve, x, ql.ExponentialSplinesFitting())
# Plot the fitted curves
draw_chart("", x_data=x, x_title="Tenor",
y1=[
(logLinearDiscount_y, "logLinearDiscount", "orange"),
(logCubicDiscount_y, "ExponentialSplines", "green"),
(linearZero_y, "linearZero", "red"),
(NelsonSiegel_y, "NelsonSiegel", "purple"),
(Svensson_y, "Svensson", "cyan"),
(ExponentialSplines_y, "ExponentialSplines", "brown"),
], y1_title="Rate")