Fitting Nelson–Siegel–Svensson Curves to Zero Rates
Summary
The document describes an attempt to fit a Nelson–Siegel–Svensson yield curve in QuantLib from observed zero rates. The author converts the rates into zero-coupon bond prices, passes them through fixed-rate bond helpers, and fits a discount curve. The reported fit is poor on some dates, while manually changing an additional fitting parameter appears to improve it; the author seeks a stable approach that works across dates.
No accepted solution or systematic diagnosis is included, and the example does not establish why the fit fails. The material highlights a practical curve-fitting problem involving instrument construction, optimizer settings, and parameter initialization, but it does not provide comparative results or a validated fix. The proposed adjustment is trial-and-error, so it should not be treated as a general calibration rule.
Key ideas
- The example fits a Nelson–Siegel–Svensson discount curve to zero rates converted into zero-coupon bond prices.
- The author reports inconsistent fit quality across trade dates.
- Changing an additional fitting parameter by trial and error appears to improve some fits.
- The document asks for a robust diagnostic or calibration approach but provides no resolution.
- The example does not establish whether instrument construction or optimizer settings cause the poor fit.
Tags
Full text
# Nelson-Siegel-Svensson Yield Curve Estimation From Zero-rates Using QuantLib (Python)
# Nelson-Siegel-Svensson Yield Curve Estimation From Zero-rates Using QuantLib (Python)
I am using QuantLib in Python to estimate yield curves using the Nelson-Siegel-Svensson (NSS) model with zero-rates as input. Since the NSS model in QuantLib uses the discount function to estimate the parameters I simply use the zero-rates as bonds with no interest-rate. However, using the code I have noticed that for some trade dates the program perform quite poorly and I do not know why. I suspect is has something to do with the way I define the zero-bonds but I am not sure. Furthermore, I have noticed through trial-and-error that if I add some small number (a “simplexlambda”?) to the `ql.FittedBondDiscountCurve` program the estimation in some cases performs better. I do not know how to correctly choose a number so fit is better.
Is there any way I can change my code so it will more consistently estimate yield curves which fits the input (the zero-rates) better? I know I can use for example Cubic Spline however I would very much like to use the NSS model to estimate the yield curve.
My code is,
```
#zero-rates
zero_rates=[-0.005683497, -0.006091677, -0.006167227, -0.006020827, -0.005731884,
-0.003626564, -0.001838833, -0.000665441, 0.000556985]
#Time to maturity for the zero-rates
maturities =[1,2,3,4,5,10,15,20,30]
# Trade day
dato='2020-10-01'
# creating maturity dates
dates=[]
dato_split=re.split( '-',dato) # month # year
startDate = ql.Date(int(dato_split[2]), int(dato_split[1]), int(dato_split[0]))
print('startDate: ',startDate)
dates.append(startDate)
end_dates=[]
for years in maturities:
endDate = startDate + ql.Period(years, ql.Years)
end_dates.append(endDate)
dates.append(endDate)
###########################################
times = [ql.SimpleDayCounter().yearFraction(startDate, dt) for dt in dates]
#The price of the zero-coupon bonds are then,
counter = 1
zero_prices=[]
for zero_rate in zero_rates:
zero_price=np.exp(-zero_rate*times[counter])
counter+=1
zero_prices.append(zero_price)
###########################################
pgbs = pd.DataFrame(
{'maturity': dates[1:],
'px': zero_prices})
###########################################
calendar = ql.NullCalendar()
today = calendar.adjust(startDate)
ql.Settings.instance().evaluationDate = today
bondSettlementDays = 0
bondSettlementDate = calendar.advance(today,ql.Period(bondSettlementDays, ql.Days))
frequency = ql.NoFrequency # NO INTEREST PAYMENTS SINCE WE HAVE ZERO-COUPON BOND
dc = ql.SimpleDayCounter()
accrualConvention = ql.Unadjusted
convention = ql.Unadjusted #
###########################################
instruments = []
for idx, row in pgbs.iterrows():
maturity = row.maturity
schedule = ql.Schedule(
bondSettlementDate,
maturity,
ql.Period(frequency),
calendar,
accrualConvention,
accrualConvention,
ql.DateGeneration.Backward,
False)
dates2 = [dt for dt in schedule]
helper = ql.FixedRateBondHelper(
ql.QuoteHandle(ql.SimpleQuote(row.px)),
bondSettlementDays,
1, # face amount
schedule,
[0], #coupon rate => zero.
dc,
convention,
1)
instruments.append(helper)
params = [bondSettlementDate, instruments, dc]
###########################################
fittingMethods = {
'NelsonSiegelFitting': ql.NelsonSiegelFitting(),
'SvenssonFitting': ql.SvenssonFitting(),
}
accuracy=1e-10
numIter = 10000
guess = []
fittedBondCurveMethods = {
label: ql.FittedBondDiscountCurve(*params, method,accuracy,numIter,guess)
for label, method in fittingMethods.items()
}
# The NSS model
curve = fittedBondCurveMethods.get('SvenssonFitting')
# Estimated parameters
estimated_paramters=[parameter for parameter in curve.fitResults().solution()]
maturities =[1,2,3,4,5,10,15,20,30]
t = np.linspace(0.1,30)
rates_nss = [curve.zeroRate(n, ql.Annual).rate() for n in t]
plt.figure(figsize=(15,5)),
plt.title(date)
plt.axhline(y=0, color='black', linestyle='-',linewidth=1); #zeroline
plt.plot(t, rates_nss,label='NSS Estimated Curve');
plt.plot(maturities, results[1],marker='o',label='Input Zero-rates');
plt.legend();
```
Running the code should result in this, which clearly is a poor fit. If I add -0.3 in the `ql.FittedBondDiscountCurve` program, so `fittedBondCurveMethods = {label: ql.FittedBondDiscountCurve(*params, method,accuracy,numIter,guess,-0.3) for label, method in fittingMethods.items()} ` , I get a good fit. See the second screenshot.
I would very much like to find a permanent solution to the initial poor fit so I can use the code to estimate yield curves for many other trade dates and not having to adjust the "simplexlambda" each time.Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.