1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3/*
4 Copyright (C) 2005, 2007 StatPro Italia srl
5 Copyright (C) 2016 Klaus Spanderen
6 Copyright (C) 2021, 2022 Ralf Konrad Eckel
7
8 This file is part of QuantLib, a free-software/open-source library
9 for financial quantitative analysts and developers - http://quantlib.org/
10
11 QuantLib is free software: you can redistribute it and/or modify it
12 under the terms of the QuantLib license. You should have received a
13 copy of the license along with this program; if not, please email
14 <quantlib-dev@lists.sf.net>. The license is also available online at
15 <http://quantlib.org/license.shtml>.
16
17 This program is distributed in the hope that it will be useful, but WITHOUT
18 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19 FOR A PARTICULAR PURPOSE. See the license for more details.
20*/
21
22#include "bermudanswaption.hpp"
23#include "utilities.hpp"
24#include <ql/cashflows/coupon.hpp>
25#include <ql/cashflows/iborcoupon.hpp>
26#include <ql/indexes/ibor/euribor.hpp>
27#include <ql/instruments/makevanillaswap.hpp>
28#include <ql/instruments/swaption.hpp>
29#include <ql/models/shortrate/onefactormodels/hullwhite.hpp>
30#include <ql/models/shortrate/twofactormodels/g2.hpp>
31#include <ql/pricingengines/swap/discountingswapengine.hpp>
32#include <ql/pricingengines/swaption/fdg2swaptionengine.hpp>
33#include <ql/pricingengines/swaption/fdhullwhiteswaptionengine.hpp>
34#include <ql/pricingengines/swaption/treeswaptionengine.hpp>
35#include <ql/termstructures/yield/flatforward.hpp>
36#include <ql/time/daycounters/thirty360.hpp>
37#include <ql/time/schedule.hpp>
38
39
40using namespace QuantLib;
41using namespace boost::unit_test_framework;
42
43namespace bermudan_swaption_test {
44
45 struct CommonVars {
46 // global data
47 Date today, settlement;
48 Calendar calendar;
49
50 // underlying swap parameters
51 Integer startYears, length;
52 Swap::Type type;
53 Real nominal;
54 BusinessDayConvention fixedConvention, floatingConvention;
55 Frequency fixedFrequency, floatingFrequency;
56 DayCounter fixedDayCount;
57 ext::shared_ptr<IborIndex> index;
58 Natural settlementDays;
59
60 RelinkableHandle<YieldTermStructure> termStructure;
61
62 // setup
63 CommonVars() {
64 startYears = 1;
65 length = 5;
66 type = Swap::Payer;
67 nominal = 1000.0;
68 settlementDays = 2;
69 fixedConvention = Unadjusted;
70 floatingConvention = ModifiedFollowing;
71 fixedFrequency = Annual;
72 floatingFrequency = Semiannual;
73 fixedDayCount = Thirty360(Thirty360::BondBasis);
74 index = ext::shared_ptr<IborIndex>(new Euribor6M(termStructure));
75 calendar = index->fixingCalendar();
76 today = calendar.adjust(Date::todaysDate());
77 settlement = calendar.advance(today,n: settlementDays,unit: Days);
78 }
79
80 // utilities
81 ext::shared_ptr<VanillaSwap> makeSwap(Rate fixedRate) const {
82 Date start = calendar.advance(settlement, n: startYears, unit: Years);
83 Date maturity = calendar.advance(start, n: length, unit: Years);
84 Schedule fixedSchedule(start, maturity,
85 Period(fixedFrequency),
86 calendar,
87 fixedConvention,
88 fixedConvention,
89 DateGeneration::Forward, false);
90 Schedule floatSchedule(start, maturity,
91 Period(floatingFrequency),
92 calendar,
93 floatingConvention,
94 floatingConvention,
95 DateGeneration::Forward, false);
96 ext::shared_ptr<VanillaSwap> swap(
97 new VanillaSwap(type, nominal,
98 fixedSchedule, fixedRate, fixedDayCount,
99 floatSchedule, index, 0.0,
100 index->dayCounter()));
101 swap->setPricingEngine(ext::shared_ptr<PricingEngine>(
102 new DiscountingSwapEngine(termStructure)));
103 return swap;
104 }
105 };
106
107}
108
109
110void BermudanSwaptionTest::testCachedValues() {
111
112 BOOST_TEST_MESSAGE(
113 "Testing Bermudan swaption with HW model against cached values...");
114
115 using namespace bermudan_swaption_test;
116
117 bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons();
118
119 CommonVars vars;
120
121 vars.today = Date(15, February, 2002);
122
123 Settings::instance().evaluationDate() = vars.today;
124
125 vars.settlement = Date(19, February, 2002);
126 // flat yield term structure impling 1x5 swap at 5%
127 vars.termStructure.linkTo(h: flatRate(today: vars.settlement,
128 forward: 0.04875825,
129 dc: Actual365Fixed()));
130
131 Rate atmRate = vars.makeSwap(fixedRate: 0.0)->fairRate();
132
133 ext::shared_ptr<VanillaSwap> itmSwap = vars.makeSwap(fixedRate: 0.8*atmRate);
134 ext::shared_ptr<VanillaSwap> atmSwap = vars.makeSwap(fixedRate: atmRate);
135 ext::shared_ptr<VanillaSwap> otmSwap = vars.makeSwap(fixedRate: 1.2*atmRate);
136
137 Real a = 0.048696, sigma = 0.0058904;
138 ext::shared_ptr<HullWhite> model(new HullWhite(vars.termStructure,
139 a, sigma));
140 std::vector<Date> exerciseDates;
141 const Leg& leg = atmSwap->fixedLeg();
142 for (const auto& i : leg) {
143 ext::shared_ptr<Coupon> coupon = ext::dynamic_pointer_cast<Coupon>(r: i);
144 exerciseDates.push_back(x: coupon->accrualStartDate());
145 }
146 ext::shared_ptr<Exercise> exercise(new BermudanExercise(exerciseDates));
147
148 ext::shared_ptr<PricingEngine> treeEngine(
149 new TreeSwaptionEngine(model, 50));
150 ext::shared_ptr<PricingEngine> fdmEngine(
151 new FdHullWhiteSwaptionEngine(model));
152
153 Real itmValue, atmValue, otmValue;
154 Real itmValueFdm, atmValueFdm, otmValueFdm;
155 if (!usingAtParCoupons) {
156 itmValue = 42.2402, atmValue = 12.9032, otmValue = 2.49758;
157 itmValueFdm = 42.2111, atmValueFdm = 12.8879, otmValueFdm = 2.44443;
158 } else {
159 itmValue = 42.2460, atmValue = 12.9069, otmValue = 2.4985;
160 itmValueFdm = 42.2091, atmValueFdm = 12.8864, otmValueFdm = 2.4437;
161 }
162
163 Real tolerance = 1.0e-4;
164
165 Swaption swaption(itmSwap, exercise);
166 swaption.setPricingEngine(treeEngine);
167 if (std::fabs(x: swaption.NPV()-itmValue) > tolerance)
168 BOOST_ERROR("failed to reproduce cached in-the-money swaption value:\n"
169 << "calculated: " << swaption.NPV() << "\n"
170 << "expected: " << itmValue);
171 swaption.setPricingEngine(fdmEngine);
172 if (std::fabs(x: swaption.NPV()-itmValueFdm) > tolerance)
173 BOOST_ERROR("failed to reproduce cached in-the-money swaption value:\n"
174 << "calculated: " << swaption.NPV() << "\n"
175 << "expected: " << itmValueFdm);
176
177 swaption = Swaption(atmSwap, exercise);
178 swaption.setPricingEngine(treeEngine);
179 if (std::fabs(x: swaption.NPV()-atmValue) > tolerance)
180 BOOST_ERROR("failed to reproduce cached at-the-money swaption value:\n"
181 << "calculated: " << swaption.NPV() << "\n"
182 << "expected: " << atmValue);
183
184 swaption.setPricingEngine(fdmEngine);
185 if (std::fabs(x: swaption.NPV()-atmValueFdm) > tolerance)
186 BOOST_ERROR("failed to reproduce cached at-the-money swaption value:\n"
187 << "calculated: " << swaption.NPV() << "\n"
188 << "expected: " << atmValueFdm);
189
190 swaption = Swaption(otmSwap, exercise);
191 swaption.setPricingEngine(treeEngine);
192 if (std::fabs(x: swaption.NPV()-otmValue) > tolerance)
193 BOOST_ERROR("failed to reproduce cached out-of-the-money "
194 << "swaption value:\n"
195 << "calculated: " << swaption.NPV() << "\n"
196 << "expected: " << otmValue);
197
198 swaption.setPricingEngine(fdmEngine);
199 if (std::fabs(x: swaption.NPV()-otmValueFdm) > tolerance)
200 BOOST_ERROR("failed to reproduce cached out-of-the-money "
201 << "swaption value:\n"
202 << "calculated: " << swaption.NPV() << "\n"
203 << "expected: " << otmValueFdm);
204
205
206 for (auto& exerciseDate : exerciseDates)
207 exerciseDate = vars.calendar.adjust(exerciseDate - 10);
208 exercise =
209 ext::shared_ptr<Exercise>(new BermudanExercise(exerciseDates));
210
211 if (!usingAtParCoupons) {
212 itmValue = 42.1791; atmValue = 12.7699; otmValue = 2.4368;
213 } else {
214 itmValue = 42.1849; atmValue = 12.7736; otmValue = 2.4379;
215 }
216
217 swaption = Swaption(itmSwap, exercise);
218 swaption.setPricingEngine(treeEngine);
219 if (std::fabs(x: swaption.NPV()-itmValue) > tolerance)
220 BOOST_ERROR("failed to reproduce cached in-the-money swaption value:\n"
221 << "calculated: " << swaption.NPV() << "\n"
222 << "expected: " << itmValue);
223 swaption = Swaption(atmSwap, exercise);
224 swaption.setPricingEngine(treeEngine);
225 if (std::fabs(x: swaption.NPV()-atmValue) > tolerance)
226 BOOST_ERROR("failed to reproduce cached at-the-money swaption value:\n"
227 << "calculated: " << swaption.NPV() << "\n"
228 << "expected: " << atmValue);
229 swaption = Swaption(otmSwap, exercise);
230 swaption.setPricingEngine(treeEngine);
231 if (std::fabs(x: swaption.NPV()-otmValue) > tolerance)
232 BOOST_ERROR("failed to reproduce cached out-of-the-money "
233 << "swaption value:\n"
234 << "calculated: " << swaption.NPV() << "\n"
235 << "expected: " << otmValue);
236}
237
238void BermudanSwaptionTest::testCachedG2Values() {
239 BOOST_TEST_MESSAGE(
240 "Testing Bermudan swaption with G2 model against cached values...");
241
242 using namespace bermudan_swaption_test;
243
244 bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons();
245
246 CommonVars vars;
247
248 vars.today = Date(15, September, 2016);
249 Settings::instance().evaluationDate() = vars.today;
250 vars.settlement = Date(19, September, 2016);
251
252 // flat yield term structure impling 1x5 swap at 5%
253 vars.termStructure.linkTo(h: flatRate(today: vars.settlement,
254 forward: 0.04875825,
255 dc: Actual365Fixed()));
256
257 const Rate atmRate = vars.makeSwap(fixedRate: 0.0)->fairRate();
258 std::vector<ext::shared_ptr<Swaption> > swaptions;
259 for (Real s=0.5; s<1.51; s+=0.25) {
260 const ext::shared_ptr<VanillaSwap> swap(vars.makeSwap(fixedRate: s*atmRate));
261
262 std::vector<Date> exerciseDates;
263 for (const auto& i : swap->fixedLeg()) {
264 exerciseDates.push_back(x: ext::dynamic_pointer_cast<Coupon>(r: i)->accrualStartDate());
265 }
266
267 swaptions.push_back(x: ext::make_shared<Swaption>(args: swap,
268 args: ext::make_shared<BermudanExercise>(args&: exerciseDates)));
269 }
270
271 const Real a=0.1, sigma=0.01, b=0.2, eta=0.013, rho=-0.5;
272
273 const ext::shared_ptr<G2> g2Model(ext::make_shared<G2>(
274 args&: vars.termStructure, args: a, args: sigma, args: b, args: eta, args: rho));
275 const ext::shared_ptr<PricingEngine> fdmEngine(
276 ext::make_shared<FdG2SwaptionEngine>(args: g2Model, args: 50, args: 75, args: 75, args: 0, args: 1e-3));
277 const ext::shared_ptr<PricingEngine> treeEngine(
278 ext::make_shared<TreeSwaptionEngine>(args: g2Model, args: 50));
279
280 Real expectedFdm[5], expectedTree[5];
281 if (!usingAtParCoupons) {
282 Real tmpExpectedFdm[] = { 103.231, 54.6519, 20.0475, 5.26941, 1.07097 };
283 Real tmpExpectedTree[] = { 103.245, 54.6685, 20.1656, 5.43999, 1.12702 };
284 std::copy(first: tmpExpectedFdm, last: tmpExpectedFdm + 5, result: expectedFdm);
285 std::copy(first: tmpExpectedTree, last: tmpExpectedTree + 5, result: expectedTree);
286 } else {
287 Real tmpExpectedFdm[] = { 103.227, 54.6502, 20.0469, 5.26924, 1.07093 };
288 Real tmpExpectedTree[] = { 103.248, 54.6726, 20.1685, 5.44118, 1.12737 };
289 std::copy(first: tmpExpectedFdm, last: tmpExpectedFdm + 5, result: expectedFdm);
290 std::copy(first: tmpExpectedTree, last: tmpExpectedTree + 5, result: expectedTree);
291 }
292
293 const Real tol = 0.005;
294 for (Size i=0; i < swaptions.size(); ++i) {
295 swaptions[i]->setPricingEngine(fdmEngine);
296 const Real calculatedFdm = swaptions[i]->NPV();
297
298 if (std::fabs(x: calculatedFdm - expectedFdm[i]) > tol) {
299 BOOST_ERROR("failed to reproduce cached G2 FDM swaption value:\n"
300 << "calculated: " << calculatedFdm << "\n"
301 << "expected: " << expectedFdm[i]);
302 }
303
304 swaptions[i]->setPricingEngine(treeEngine);
305 const Real calculatedTree = swaptions[i]->NPV();
306
307 if (std::fabs(x: calculatedTree - expectedTree[i]) > tol) {
308 BOOST_ERROR("failed to reproduce cached G2 Tree swaption value:\n"
309 << "calculated: " << calculatedTree << "\n"
310 << "expected: " << expectedTree[i]);
311 }
312 }
313}
314
315void BermudanSwaptionTest::testTreeEngineTimeSnapping() {
316 BOOST_TEST_MESSAGE("Testing snap of exercise dates for discretized swaption...");
317
318 Date today = Date(8, Jul, 2021);
319 Settings::instance().evaluationDate() = today;
320
321 RelinkableHandle<YieldTermStructure> termStructure;
322 termStructure.linkTo(h: ext::make_shared<FlatForward>(args&: today, args: 0.02, args: Actual365Fixed()));
323 auto index = ext::make_shared<Euribor3M>(args&: termStructure);
324
325 auto makeBermudanSwaption = [&index](Date callDate) {
326 auto effectiveDate = Date(15, May, 2025);
327 ext::shared_ptr<VanillaSwap> swap = MakeVanillaSwap(Period(10, Years), index, 0.05)
328 .withEffectiveDate(effectiveDate)
329 .withNominal(n: 10000.00)
330 .withType(type: Swap::Type::Payer);
331
332 std::vector<Date> exerciseDates{effectiveDate, callDate};
333 auto bermudanExercise = ext::make_shared<BermudanExercise>(args&: exerciseDates);
334 auto bermudanSwaption = ext::make_shared<Swaption>(args&: swap, args&: bermudanExercise);
335
336 return bermudanSwaption;
337 };
338
339 int intervalOfDaysToTest = 10;
340
341 for (int i = -intervalOfDaysToTest; i < intervalOfDaysToTest + 1; i++) {
342 static auto initialCallDate = Date(15, May, 2030);
343 static auto calendar = index->fixingCalendar();
344
345 auto callDate = initialCallDate + i * Days;
346 if (calendar.isBusinessDay(d: callDate)) {
347
348 auto bermudanSwaption = makeBermudanSwaption(callDate);
349
350 auto model = ext::make_shared<HullWhite>(args&: termStructure);
351
352 bermudanSwaption->setPricingEngine(ext::make_shared<FdHullWhiteSwaptionEngine>(args&: model));
353 auto npvFD = bermudanSwaption->NPV();
354
355 constexpr auto timesteps = 14 * 4 * 4;
356
357 bermudanSwaption->setPricingEngine(
358 ext::make_shared<TreeSwaptionEngine>(args&: model, args: timesteps));
359 auto npvTree = bermudanSwaption->NPV();
360
361 auto npvDiff = npvTree - npvFD;
362
363 static auto tolerance = 1.0;
364 if (std::abs(x: npvTree - npvFD) > tolerance) {
365 BOOST_ERROR(std::fixed << std::setprecision(2) << std::setw(5) << "At "
366 << io::iso_date(callDate)
367 << ": The difference between the npv of the FD and the tree "
368 "engine is expected to be smaller than "
369 << tolerance << " but was " << npvDiff << ". (FD: " << npvFD
370 << ", tree: " << npvTree << ")");
371 }
372 }
373 }
374}
375
376test_suite* BermudanSwaptionTest::suite(SpeedLevel speed) {
377 auto* suite = BOOST_TEST_SUITE("Bermudan swaption tests");
378
379 suite->add(QUANTLIB_TEST_CASE(&BermudanSwaptionTest::testCachedValues));
380 suite->add(QUANTLIB_TEST_CASE(&BermudanSwaptionTest::testTreeEngineTimeSnapping));
381
382 if (speed <= Fast) {
383 suite->add(QUANTLIB_TEST_CASE(&BermudanSwaptionTest::testCachedG2Values));
384 }
385
386 return suite;
387}
388

source code of quantlib/test-suite/bermudanswaption.cpp