| 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 | |
| 40 | using namespace QuantLib; |
| 41 | using namespace boost::unit_test_framework; |
| 42 | |
| 43 | namespace 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 | |
| 110 | void 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 | |
| 238 | void 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 | |
| 315 | void 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 | |
| 376 | test_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 | |