Skip to content
All library documents

Choosing Normal or Shifted Black Volatility for Hull–White Calibration

Article Quant Q&A · Author: Michielap

Summary

This document concerns calibrating a one-factor Hull–White short-rate model to a yield curve and swaption volatilities when rates can be negative. The accepted response says QuantLib’s swaption helper must be configured for the market’s volatility convention: Black, shifted Black, or normal. For markets where negative forwards make ordinary Black volatility unsuitable, shifted Black or normal volatility may be used.

The response flags that the example’s 20% shift is likely inappropriate and gives 2% as an example of a shift quoted for EUR rates by one data provider. It recommends using the shift that matches the volatility data and comparing model prices with market prices. The author’s update says the data contains normal volatilities, but the document does not resolve the later root-not-bracketed error or provide a verified calibration. Its numerical shift example is market-specific, not a universal setting.

Key ideas

  • The volatility type supplied to the swaption helper must match the convention of the market data.
  • Normal or shifted Black volatility can accommodate markets where negative forwards limit the use of ordinary Black volatility.
  • A shifted Black volatility input requires a shift that corresponds to the quoted data.
  • Comparing model prices with market prices can help assess the calibration fit.
  • The document does not resolve the reported root-not-bracketed error.

Tags

Full text
# QuantLib - Calibrating Hull White one-factor on negative interest rates


# QuantLib - Calibrating Hull White one-factor on negative interest rates












I have been working with the QuantLib Python package for some days now. Currently, I am working on calibrating a Hull White one-factor model for short rates. I am calibrating the model on the yield-curve and on swaption volatilities.

Given the current market circumstances, a large part of the yield curve is negative. This causes some issues with calibrating the Hull White one-factor model since the lognormal distribution cannot be negative. After some searching I found the possibility of specifying a shift. In the code below my SwaptionHelper initialization is shown including the shift.

Please note I am currently using a yield curve, however, when using a flatforward of -0.00478 I get similar results.

However, using the shift results in unrealistic output for a (a=0.0000389, sigma=0.0222).

I have been searching a lot for an answer, but cannot find decent information for situations with a negative rate. If anyone would be able to explain the usage of a shift or do a quick check on my code to see if anything is wrong would be so helpful.

```
helper = ql.SwaptionHelper(
        ql.Period(int(maturity), ql.Years),
        ql.Period(int(tenor), ql.Years),
        volatility,
        index,
        fixedLegTenor,
        fixedLegDayCounter,
        floatingLegDayCounter,
        term_structure,
        ql.BlackCalibrationHelper.RelativePriceError,
        ql.nullDouble(),
        1.0,
        ql.ShiftedLognormal,
        0.05 #shift to make rates non-negative
    )
```

Full code:

```
import csv
from QuantLib.QuantLib import SwaptionHelper
import matplotlib.pyplot as plt
import pandas as pd
import QuantLib as ql

def load_csv_input():
    zero_curve = pd.read_csv('ZeroCurve.csv', delimiter=';')
    dates = []
    rates = []

    for i in range(0,len(zero_curve)):
        rates.append(float(zero_curve['Rate'][i]))
        dates.append(ql.Date(int(zero_curve['Mat_Day'][i]),int(zero_curve['Mat_Month'][i]),int(zero_curve['Mat_Year'][i])))

    return dates, rates

# Read the swaption volatilities from the csv file.
swaption_vols = pd.read_csv('SwaptionVol.csv', delimiter=';', index_col=0)
dates, rates = load_csv_input()

curve = ql.ZeroCurve(dates, rates, ql.Actual365Fixed())
term_structure = ql.YieldTermStructureHandle(curve)

model = ql.HullWhite(term_structure)
#engine = ql.TreeSwaptionEngine(model, 25)
engine = ql.JamshidianSwaptionEngine(model)
#engine = ql.G2SwaptionEngine(model, 10, 400)

index = ql.Euribor1Y(term_structure)
fixedLegTenor = ql.Period('1Y')
fixedLegDayCounter = ql.Actual360()
floatingLegDayCounter = ql.Actual360()

swaptions = []
ql.Settings.instance().evaluationDate = ql.Date(1, 10, 2020)
for maturity in swaption_vols.index:
    for tenor in swaption_vols.columns:
        volatility = ql.QuoteHandle(ql.SimpleQuote(swaption_vols.at[maturity,tenor]))
        helper = ql.SwaptionHelper(
            ql.Period(int(maturity), ql.Years),
            ql.Period(int(tenor), ql.Years),
            volatility,
            index,
            fixedLegTenor,
            fixedLegDayCounter,
            floatingLegDayCounter,
            term_structure,
            ql.BlackCalibrationHelper.RelativePriceError,
            ql.nullDouble(),
            1.0,
            ql.ShiftedLognormal,
            0.2 #shift to make rates non-negative
        )
        helper.setPricingEngine(engine)
        swaptions.append(helper)

optimization_method = ql.LevenbergMarquardt(1.0e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(500000, 1000, 1e-6, 1e-8, 1e-8)
model.calibrate(swaptions, optimization_method, end_criteria)
params = model.params()
print(params)
```

Update

I have changed the volatility type from lognormal to normal, since the data contains normal volatilities. So this should be able to work with the negative values.

Now I am running into an issue where the calibration only works if I use a small selection of volatilities. My complete volatility matrix is 10x10 (1, 2, 3, 4, 5, 10, 15, 20, 25, and 30Y maturity and tenors). However, if I use more than the first 5x5 I get a "root not bracketed" error.

## Answer by David Duarte (score 4, accepted)

https://quant.stackexchange.com/a/59273

When building a SwaptionHelper, you have to tell QuantLib what kind of volatility you are inputting. There are three options: Black Vol, Shifted Black Vol and Normal Vol.

Since you don't have black vol for most of the swaption surface (EUR) because of the negative forwards, you can either use shifted Black Vol or Normal Vol.

In the example you are using shifted black vol but you have a shift of 20%! Your shifted vols should have a respective shift value but I doubt it is 20%. ICAP for example quotes shifted black vols with a shift of 2% for EUR rates.

Without doing any code, I would guess if you correct your shift to the appropriate value you'll get better results. You can compare the model values with the market values for your helpers.

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.