Table of Contents

Short-rate models

This page follows QuantLib's BermudanSwaption example: short-rate models calibrated to five co-terminal swaptions, then a Bermudan swaption priced on them. Its values match that example's output, but for those marked † and ‡, which two of nanoql's documented fixes of QuantLib move. The sample, ShortRateModels.cs, carries QuantLib's notice. The example's fourth model, Black-Karasinski, is left out: only the tree prices its swaptions, so nothing would check its values.

The market

A flat curve, Euribor 6M on it, and the swaptions 1Y into 5Y, 2Y into 4Y, … 5Y into 1Y, each with its market vol:

Calendar calendar = new TARGET();
var settlement = new Date(19, Month.February, 2002);
YieldTermStructure curve = new FlatForward(settlement, 0.04875825, new Actual365Fixed());
var index = new Euribor6M(curve);

// The co-terminal swaptions: 1Y into 5Y, 2Y into 4Y, ... 5Y into 1Y.
double[] marketVols = [0.1148, 0.1108, 0.1070, 0.1021, 0.1000];
var helpers = new List<SwaptionHelper>();
var times = new List<double>();
for (int i = 0; i < 5; ++i)
{
    var helper = new SwaptionHelper(new Period(i + 1, TimeUnit.Years), new Period(5 - i, TimeUnit.Years),
        marketVols[i], index, index.Tenor, index.DayCounter, index.DayCounter, curve, context);
    helpers.Add(helper);
    times.AddRange(helper.Times(context));
}

var grid = new TimeGrid([.. times], 30);

Calibration

G2 on its analytic formula, Hull-White on Jamshidian's, and Hull-White again on a tree, each by Levenberg-Marquardt:

var optimizer = new LevenbergMarquardt();
var endCriteria = new EndCriteria(400, 100, 1.0e-8, 1.0e-8, 1.0e-8);
(TModel Model, List<SwaptionHelper> Helpers) Calibrate<TModel>(
    TModel model, Func<Swaption, EvaluationContext, TModel, InstrumentResults> engine)
    where TModel : ICalibratedModel
{
    List<SwaptionHelper> priced = [.. helpers.Select(h => h.SetPricingEngine(engine))];
    return (model.Calibrate(priced.Cast<CalibrationHelper>(), optimizer, endCriteria, context).Model, priced);
}

var g2 = Calibrate(new G2(curve), (s, c, m) => G2SwaptionEngine.Calculate(s, c, m, 6.0, 16));
var hullWhite = Calibrate(new HullWhite(curve), (s, c, m) => JamshidianSwaptionEngine.Calculate(s, c, m));
var numerical = Calibrate(new HullWhite(curve), (s, c, m) => TreeSwaptionEngine.Calculate(s, c, m, grid));

Each swaption's value on the calibrated model, as a Black vol, beside its market vol:

Model Swaption Model vol Market vol
G2 † 1Y into 5Y 10.63458% 11.48000%
G2 † 2Y into 4Y 10.64501% 11.08000%
G2 † 3Y into 3Y 10.62873% 10.70000%
G2 † 4Y into 2Y 10.63652% 10.21000%
G2 † 5Y into 1Y 10.65230% 10.00000%
Hull-White 1Y into 5Y 10.62037% 11.48000%
Hull-White 2Y into 4Y 10.62959% 11.08000%
Hull-White 3Y into 3Y 10.63414% 10.70000%
Hull-White 4Y into 2Y 10.64428% 10.21000%
Hull-White 5Y into 1Y 10.66131% 10.00000%
Hull-White, numerical ‡ 1Y into 5Y 10.31183% 11.48000%
Hull-White, numerical ‡ 2Y into 4Y 10.54617% 11.08000%
Hull-White, numerical ‡ 3Y into 3Y 10.66911% 10.70000%
Hull-White, numerical ‡ 4Y into 2Y 10.74017% 10.21000%
Hull-White, numerical ‡ 5Y into 1Y 10.79723% 10.00000%
Model Calibrated parameters
G2 † a 0.046794, σ 0.0088295, b 0.046794, η 0.0088295, ρ -0.77927
Hull-White a 0.046416, σ 0.0058693
Hull-White, numerical ‡ a 0.055232, σ 0.0061063

A Bermudan swaption

A payer swaption into the five-year swap, exercisable on each fixed coupon's start, at the money, out of it (the strike 1.2 times) and in it (0.8 times), on each model's tree and by finite differences:

Date start = calendar.Advance(settlement, 1, TimeUnit.Years, BusinessDayConvention.ModifiedFollowing);
Date maturity = calendar.Advance(start, 5, TimeUnit.Years, BusinessDayConvention.ModifiedFollowing);
var fixedSchedule = new Schedule(start, maturity, new Period(Frequency.Annual), calendar,
    BusinessDayConvention.Unadjusted, BusinessDayConvention.Unadjusted, DateGenerationRule.Forward, false, context);
var floatSchedule = new Schedule(start, maturity, new Period(Frequency.Semiannual), calendar,
    BusinessDayConvention.ModifiedFollowing, BusinessDayConvention.ModifiedFollowing, DateGenerationRule.Forward,
    false, context);
VanillaSwap Swap(double rate) => new(SwapType.Payer, 1000.0, fixedSchedule, rate,
    new Thirty360(Thirty360Convention.European), floatSchedule, index, 0.0, index.DayCounter, context);

double atm = DiscountingSwapEngine.Calculate(Swap(0.03), context, curve).FairRate!.Value;
var exercise = new BermudanExercise([.. Swap(atm).FixedLeg.Select(cf => ((Coupon)cf).AccrualStartDate)]);
var bermudans = new List<Bermudan>();
foreach (var (label, strike, g2Steps) in new[] { ("ATM", atm, 50), ("OTM", atm * 1.2, 300), ("ITM", atm * 0.8, 50) })
{
    var swaption = new Swaption(Swap(strike), exercise);
    bermudans.Add(new Bermudan(label, strike,
        TreeSwaptionEngine.Calculate(swaption, context, g2.Model, g2Steps).NPV!.Value,
        FdG2SwaptionEngine.Calculate(swaption, context, g2.Model).NPV!.Value,
        TreeSwaptionEngine.Calculate(swaption, context, hullWhite.Model, 50).NPV!.Value,
        FdHullWhiteSwaptionEngine.Calculate(swaption, context, hullWhite.Model).NPV!.Value,
        TreeSwaptionEngine.Calculate(swaption, context, numerical.Model, 50).NPV!.Value,
        FdHullWhiteSwaptionEngine.Calculate(swaption, context, numerical.Model).NPV!.Value));
}

Strike G2, tree †‡ G2, FD † Hull-White, tree ‡ Hull-White, FD Hull-White numerical, tree ‡ Hull-White numerical, FD ‡
ATM, 5.00000% 12.82 12.832 12.904 12.91 13.158 13.157
OTM, 6.00000% 2.4639 2.4553 2.4921 2.4596 2.615 2.5829
ITM, 4.00000% 42.258 42.206 42.253 42.215 42.364 42.311

The marks

  • † G2's analytic swaption formula takes each fixed coupon on the leg's own day counter, where QuantLib's takes it on the curve's (the divergence). G2's calibration, and every value on it, moves. QuantLib, given nanoql's calibrated parameters and the coupons restated on the curve's day counter, gives the same model values, and its finite-difference engine the same prices.
  • ‡ The tree values a swap on its own dates, where QuantLib's snaps a date within a week of an exercise date to it (the divergence). Every tree value moves, and so does the calibration on the tree. Each tree price is within 0.1 on the notional of 1,000 of the finite-difference price on the same model, and QuantLib's finite-difference engine, given nanoql's numerical Hull-White, gives its prices.