| 1 | /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
| 2 | |
| 3 | /* |
| 4 | Copyright (C) 2008, 2009 StatPro Italia srl |
| 5 | |
| 6 | This file is part of QuantLib, a free-software/open-source library |
| 7 | for financial quantitative analysts and developers - http://quantlib.org/ |
| 8 | |
| 9 | QuantLib is free software: you can redistribute it and/or modify it |
| 10 | under the terms of the QuantLib license. You should have received a |
| 11 | copy of the license along with this program; if not, please email |
| 12 | <quantlib-dev@lists.sf.net>. The license is also available online at |
| 13 | <http://quantlib.org/license.shtml>. |
| 14 | |
| 15 | This program is distributed in the hope that it will be useful, but WITHOUT |
| 16 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 17 | FOR A PARTICULAR PURPOSE. See the license for more details. |
| 18 | */ |
| 19 | |
| 20 | #include "creditdefaultswap.hpp" |
| 21 | #include "utilities.hpp" |
| 22 | #include <ql/cashflows/iborcoupon.hpp> |
| 23 | #include <ql/instruments/creditdefaultswap.hpp> |
| 24 | #include <ql/instruments/makecds.hpp> |
| 25 | #include <ql/pricingengines/credit/midpointcdsengine.hpp> |
| 26 | #include <ql/pricingengines/credit/integralcdsengine.hpp> |
| 27 | #include <ql/pricingengines/credit/isdacdsengine.hpp> |
| 28 | #include <ql/termstructures/credit/flathazardrate.hpp> |
| 29 | #include <ql/termstructures/credit/interpolatedhazardratecurve.hpp> |
| 30 | #include <ql/termstructures/yield/flatforward.hpp> |
| 31 | #include <ql/termstructures/yield/discountcurve.hpp> |
| 32 | #include <ql/termstructures/yield/piecewiseyieldcurve.hpp> |
| 33 | #include <ql/termstructures/yield/ratehelpers.hpp> |
| 34 | #include <ql/quotes/simplequote.hpp> |
| 35 | #include <ql/math/interpolations/backwardflatinterpolation.hpp> |
| 36 | #include <ql/time/calendars/target.hpp> |
| 37 | #include <ql/time/calendars/unitedstates.hpp> |
| 38 | #include <ql/time/calendars/weekendsonly.hpp> |
| 39 | #include <ql/currencies/america.hpp> |
| 40 | #include <ql/currencies/europe.hpp> |
| 41 | #include <ql/time/daycounters/actual360.hpp> |
| 42 | #include <ql/time/daycounters/thirty360.hpp> |
| 43 | #include <ql/optional.hpp> |
| 44 | #include <map> |
| 45 | #include <iomanip> |
| 46 | #include <iostream> |
| 47 | |
| 48 | using namespace QuantLib; |
| 49 | using namespace boost::unit_test_framework; |
| 50 | using std::map; |
| 51 | |
| 52 | void CreditDefaultSwapTest::testCachedValue() { |
| 53 | |
| 54 | BOOST_TEST_MESSAGE("Testing credit-default swap against cached values..." ); |
| 55 | |
| 56 | // Initialize curves |
| 57 | Settings::instance().evaluationDate() = Date(9,June,2006); |
| 58 | Date today = Settings::instance().evaluationDate(); |
| 59 | Calendar calendar = TARGET(); |
| 60 | |
| 61 | Handle<Quote> hazardRate = Handle<Quote>( |
| 62 | ext::shared_ptr<Quote>(new SimpleQuote(0.01234))); |
| 63 | RelinkableHandle<DefaultProbabilityTermStructure> probabilityCurve; |
| 64 | probabilityCurve.linkTo( |
| 65 | h: ext::shared_ptr<DefaultProbabilityTermStructure>( |
| 66 | new FlatHazardRate(0, calendar, hazardRate, Actual360()))); |
| 67 | |
| 68 | RelinkableHandle<YieldTermStructure> discountCurve; |
| 69 | |
| 70 | discountCurve.linkTo(h: ext::shared_ptr<YieldTermStructure>( |
| 71 | new FlatForward(today,0.06,Actual360()))); |
| 72 | |
| 73 | // Build the schedule |
| 74 | Date issueDate = calendar.advance(today, n: -1, unit: Years); |
| 75 | Date maturity = calendar.advance(issueDate, n: 10, unit: Years); |
| 76 | Frequency frequency = Semiannual; |
| 77 | BusinessDayConvention convention = ModifiedFollowing; |
| 78 | |
| 79 | Schedule schedule(issueDate, maturity, Period(frequency), calendar, |
| 80 | convention, convention, DateGeneration::Forward, false); |
| 81 | |
| 82 | // Build the CDS |
| 83 | Rate fixedRate = 0.0120; |
| 84 | DayCounter dayCount = Actual360(); |
| 85 | Real notional = 10000.0; |
| 86 | Real recoveryRate = 0.4; |
| 87 | |
| 88 | CreditDefaultSwap cds(Protection::Seller, notional, fixedRate, |
| 89 | schedule, convention, dayCount, true, true); |
| 90 | cds.setPricingEngine(ext::shared_ptr<PricingEngine>( |
| 91 | new MidPointCdsEngine(probabilityCurve,recoveryRate,discountCurve))); |
| 92 | |
| 93 | Real npv = 295.0153398; |
| 94 | Rate fairRate = 0.007517539081; |
| 95 | |
| 96 | Real calculatedNpv = cds.NPV(); |
| 97 | Rate calculatedFairRate = cds.fairSpread(); |
| 98 | Real tolerance = 1.0e-7; |
| 99 | |
| 100 | if (std::fabs(x: calculatedNpv - npv) > tolerance) |
| 101 | BOOST_ERROR( |
| 102 | "Failed to reproduce NPV with mid-point engine\n" |
| 103 | << std::setprecision(10) |
| 104 | << " calculated NPV: " << calculatedNpv << "\n" |
| 105 | << " expected NPV: " << npv); |
| 106 | |
| 107 | if (std::fabs(x: calculatedFairRate - fairRate) > tolerance) |
| 108 | BOOST_ERROR( |
| 109 | "Failed to reproduce fair rate with mid-point engine\n" |
| 110 | << std::setprecision(10) |
| 111 | << " calculated fair rate: " << calculatedFairRate << "\n" |
| 112 | << " expected fair rate: " << fairRate); |
| 113 | |
| 114 | cds.setPricingEngine(ext::shared_ptr<PricingEngine>( |
| 115 | new IntegralCdsEngine(1*Days,probabilityCurve, |
| 116 | recoveryRate,discountCurve))); |
| 117 | |
| 118 | calculatedNpv = cds.NPV(); |
| 119 | calculatedFairRate = cds.fairSpread(); |
| 120 | tolerance = 1.0e-5; |
| 121 | |
| 122 | if (std::fabs(x: calculatedNpv - npv) > notional*tolerance*10) |
| 123 | BOOST_ERROR( |
| 124 | "Failed to reproduce NPV with integral engine " |
| 125 | "(step = 1 day)\n" |
| 126 | << std::setprecision(10) |
| 127 | << " calculated NPV: " << calculatedNpv << "\n" |
| 128 | << " expected NPV: " << npv); |
| 129 | |
| 130 | if (std::fabs(x: calculatedFairRate - fairRate) > tolerance) |
| 131 | BOOST_ERROR( |
| 132 | "Failed to reproduce fair rate with integral engine " |
| 133 | "(step = 1 day)\n" |
| 134 | << std::setprecision(10) |
| 135 | << " calculated fair rate: " << calculatedFairRate << "\n" |
| 136 | << " expected fair rate: " << fairRate); |
| 137 | |
| 138 | cds.setPricingEngine(ext::shared_ptr<PricingEngine>( |
| 139 | new IntegralCdsEngine(1*Weeks,probabilityCurve, |
| 140 | recoveryRate,discountCurve))); |
| 141 | |
| 142 | calculatedNpv = cds.NPV(); |
| 143 | calculatedFairRate = cds.fairSpread(); |
| 144 | tolerance = 1.0e-5; |
| 145 | |
| 146 | if (std::fabs(x: calculatedNpv - npv) > notional*tolerance*10) |
| 147 | BOOST_ERROR( |
| 148 | "Failed to reproduce NPV with integral engine " |
| 149 | "(step = 1 week)\n" |
| 150 | << std::setprecision(10) |
| 151 | << " calculated NPV: " << calculatedNpv << "\n" |
| 152 | << " expected NPV: " << npv); |
| 153 | |
| 154 | if (std::fabs(x: calculatedFairRate - fairRate) > tolerance) |
| 155 | BOOST_ERROR( |
| 156 | "Failed to reproduce fair rate with integral engine " |
| 157 | "(step = 1 week)\n" |
| 158 | << std::setprecision(10) |
| 159 | << " calculated fair rate: " << calculatedFairRate << "\n" |
| 160 | << " expected fair rate: " << fairRate); |
| 161 | } |
| 162 | |
| 163 | |
| 164 | void CreditDefaultSwapTest::testCachedMarketValue() { |
| 165 | |
| 166 | BOOST_TEST_MESSAGE( |
| 167 | "Testing credit-default swap against cached market values..." ); |
| 168 | |
| 169 | Settings::instance().evaluationDate() = Date(9,June,2006); |
| 170 | Date evalDate = Settings::instance().evaluationDate(); |
| 171 | Calendar calendar = UnitedStates(UnitedStates::GovernmentBond); |
| 172 | |
| 173 | std::vector<Date> discountDates = { |
| 174 | evalDate, |
| 175 | calendar.advance(evalDate, n: 1, unit: Weeks, convention: ModifiedFollowing), |
| 176 | calendar.advance(evalDate, n: 1, unit: Months, convention: ModifiedFollowing), |
| 177 | calendar.advance(evalDate, n: 2, unit: Months, convention: ModifiedFollowing), |
| 178 | calendar.advance(evalDate, n: 3, unit: Months, convention: ModifiedFollowing), |
| 179 | calendar.advance(evalDate, n: 6, unit: Months, convention: ModifiedFollowing), |
| 180 | calendar.advance(evalDate,n: 12, unit: Months, convention: ModifiedFollowing), |
| 181 | calendar.advance(evalDate, n: 2, unit: Years, convention: ModifiedFollowing), |
| 182 | calendar.advance(evalDate, n: 3, unit: Years, convention: ModifiedFollowing), |
| 183 | calendar.advance(evalDate, n: 4, unit: Years, convention: ModifiedFollowing), |
| 184 | calendar.advance(evalDate, n: 5, unit: Years, convention: ModifiedFollowing), |
| 185 | calendar.advance(evalDate, n: 6, unit: Years, convention: ModifiedFollowing), |
| 186 | calendar.advance(evalDate, n: 7, unit: Years, convention: ModifiedFollowing), |
| 187 | calendar.advance(evalDate, n: 8, unit: Years, convention: ModifiedFollowing), |
| 188 | calendar.advance(evalDate, n: 9, unit: Years, convention: ModifiedFollowing), |
| 189 | calendar.advance(evalDate,n: 10, unit: Years, convention: ModifiedFollowing), |
| 190 | calendar.advance(evalDate,n: 15, unit: Years, convention: ModifiedFollowing) |
| 191 | }; |
| 192 | |
| 193 | std::vector<DiscountFactor> dfs = { |
| 194 | 1.0, |
| 195 | 0.9990151375768731, |
| 196 | 0.99570502636871183, |
| 197 | 0.99118260474528685, |
| 198 | 0.98661167950906203, |
| 199 | 0.9732592953359388, |
| 200 | 0.94724424481038083, |
| 201 | 0.89844996737120875, |
| 202 | 0.85216647839921411, |
| 203 | 0.80775477692556874, |
| 204 | 0.76517289234200347, |
| 205 | 0.72401019553182933, |
| 206 | 0.68503909569219212, |
| 207 | 0.64797499814013748, |
| 208 | 0.61263171936255534, |
| 209 | 0.5791942350748791, |
| 210 | 0.43518868769953606 |
| 211 | }; |
| 212 | |
| 213 | const DayCounter& curveDayCounter=Actual360(); |
| 214 | |
| 215 | RelinkableHandle<YieldTermStructure> discountCurve; |
| 216 | discountCurve.linkTo( |
| 217 | h: ext::shared_ptr<YieldTermStructure>( |
| 218 | new DiscountCurve(discountDates, dfs, curveDayCounter))); |
| 219 | |
| 220 | DayCounter dayCounter = Thirty360(Thirty360::BondBasis); |
| 221 | std::vector<Date> dates = { |
| 222 | evalDate, |
| 223 | calendar.advance(evalDate, n: 6, unit: Months, convention: ModifiedFollowing), |
| 224 | calendar.advance(evalDate, n: 1, unit: Years, convention: ModifiedFollowing), |
| 225 | calendar.advance(evalDate, n: 2, unit: Years, convention: ModifiedFollowing), |
| 226 | calendar.advance(evalDate, n: 3, unit: Years, convention: ModifiedFollowing), |
| 227 | calendar.advance(evalDate, n: 4, unit: Years, convention: ModifiedFollowing), |
| 228 | calendar.advance(evalDate, n: 5, unit: Years, convention: ModifiedFollowing), |
| 229 | calendar.advance(evalDate, n: 7, unit: Years, convention: ModifiedFollowing), |
| 230 | calendar.advance(evalDate,n: 10, unit: Years, convention: ModifiedFollowing) |
| 231 | }; |
| 232 | |
| 233 | std::vector<Probability> defaultProbabilities = { |
| 234 | 0.0000, |
| 235 | 0.0047, |
| 236 | 0.0093, |
| 237 | 0.0286, |
| 238 | 0.0619, |
| 239 | 0.0953, |
| 240 | 0.1508, |
| 241 | 0.2288, |
| 242 | 0.3666 |
| 243 | }; |
| 244 | |
| 245 | std::vector<Real> hazardRates; |
| 246 | hazardRates.push_back(x: 0.0); |
| 247 | for (Size i=1; i<dates.size(); ++i) { |
| 248 | Time t1 = dayCounter.yearFraction(d1: dates[0], d2: dates[i-1]); |
| 249 | Time t2 = dayCounter.yearFraction(d1: dates[0], d2: dates[i]); |
| 250 | Probability S1 = 1.0 - defaultProbabilities[i-1]; |
| 251 | Probability S2 = 1.0 - defaultProbabilities[i]; |
| 252 | hazardRates.push_back(x: std::log(x: S1/S2)/(t2-t1)); |
| 253 | } |
| 254 | |
| 255 | RelinkableHandle<DefaultProbabilityTermStructure> piecewiseFlatHazardRate; |
| 256 | piecewiseFlatHazardRate.linkTo( |
| 257 | h: ext::shared_ptr<DefaultProbabilityTermStructure>( |
| 258 | new InterpolatedHazardRateCurve<BackwardFlat>(dates, |
| 259 | hazardRates, |
| 260 | Thirty360(Thirty360::BondBasis)))); |
| 261 | |
| 262 | // Testing credit default swap |
| 263 | |
| 264 | // Build the schedule |
| 265 | Date issueDate(20, March, 2006); |
| 266 | Date maturity(20, June, 2013); |
| 267 | Frequency cdsFrequency = Semiannual; |
| 268 | BusinessDayConvention cdsConvention = ModifiedFollowing; |
| 269 | |
| 270 | Schedule schedule(issueDate, maturity, Period(cdsFrequency), calendar, |
| 271 | cdsConvention, cdsConvention, |
| 272 | DateGeneration::Forward, false); |
| 273 | |
| 274 | // Build the CDS |
| 275 | Real recoveryRate = 0.25; |
| 276 | Rate fixedRate=0.0224; |
| 277 | DayCounter dayCount=Actual360(); |
| 278 | Real cdsNotional=100.0; |
| 279 | |
| 280 | CreditDefaultSwap cds(Protection::Seller, cdsNotional, fixedRate, |
| 281 | schedule, cdsConvention, dayCount, true, true); |
| 282 | cds.setPricingEngine(ext::shared_ptr<PricingEngine>( |
| 283 | new MidPointCdsEngine(piecewiseFlatHazardRate, |
| 284 | recoveryRate,discountCurve))); |
| 285 | |
| 286 | Real calculatedNpv = cds.NPV(); |
| 287 | Real calculatedFairRate = cds.fairSpread(); |
| 288 | |
| 289 | double npv = -1.364048777; // from Bloomberg we have 98.15598868 - 100.00; |
| 290 | double fairRate = 0.0248429452; // from Bloomberg we have 0.0258378; |
| 291 | |
| 292 | Real tolerance = 1e-9; |
| 293 | |
| 294 | if (std::fabs(x: npv - calculatedNpv) > tolerance) |
| 295 | BOOST_ERROR( |
| 296 | "Failed to reproduce the npv for the given credit-default swap\n" |
| 297 | << std::setprecision(10) |
| 298 | << " computed NPV: " << calculatedNpv << "\n" |
| 299 | << " Given NPV: " << npv); |
| 300 | |
| 301 | if (std::fabs(x: fairRate - calculatedFairRate) > tolerance) |
| 302 | BOOST_ERROR( |
| 303 | "Failed to reproduce the fair rate for the given credit-default swap\n" |
| 304 | << std::setprecision(10) |
| 305 | << " computed fair rate: " << calculatedFairRate << "\n" |
| 306 | << " Given fair rate: " << fairRate); |
| 307 | } |
| 308 | |
| 309 | |
| 310 | void CreditDefaultSwapTest::testImpliedHazardRate() { |
| 311 | |
| 312 | BOOST_TEST_MESSAGE("Testing implied hazard-rate for credit-default swaps..." ); |
| 313 | |
| 314 | // Initialize curves |
| 315 | Calendar calendar = TARGET(); |
| 316 | Date today = calendar.adjust(Date::todaysDate()); |
| 317 | Settings::instance().evaluationDate() = today; |
| 318 | |
| 319 | Rate h1 = 0.30, h2 = 0.40; |
| 320 | DayCounter dayCounter = Actual365Fixed(); |
| 321 | |
| 322 | std::vector<Date> dates(3); |
| 323 | std::vector<Real> hazardRates(3); |
| 324 | dates[0] = today; |
| 325 | hazardRates[0] = h1; |
| 326 | |
| 327 | dates[1] = today + 5*Years; |
| 328 | hazardRates[1] = h1; |
| 329 | |
| 330 | dates[2] = today + 10*Years; |
| 331 | hazardRates[2] = h2; |
| 332 | |
| 333 | RelinkableHandle<DefaultProbabilityTermStructure> probabilityCurve; |
| 334 | probabilityCurve.linkTo(h: ext::shared_ptr<DefaultProbabilityTermStructure>( |
| 335 | new InterpolatedHazardRateCurve<BackwardFlat>(dates, |
| 336 | hazardRates, |
| 337 | dayCounter))); |
| 338 | |
| 339 | RelinkableHandle<YieldTermStructure> discountCurve; |
| 340 | discountCurve.linkTo(h: ext::shared_ptr<YieldTermStructure>( |
| 341 | new FlatForward(today,0.03,Actual360()))); |
| 342 | |
| 343 | |
| 344 | Frequency frequency = Semiannual; |
| 345 | BusinessDayConvention convention = ModifiedFollowing; |
| 346 | |
| 347 | Date issueDate = calendar.advance(today, n: -6, unit: Months); |
| 348 | Rate fixedRate = 0.0120; |
| 349 | DayCounter cdsDayCount = Actual360(); |
| 350 | Real notional = 10000.0; |
| 351 | Real recoveryRate = 0.4; |
| 352 | |
| 353 | Rate latestRate = Null<Rate>(); |
| 354 | for (Integer n=6; n<=10; ++n) { |
| 355 | |
| 356 | Date maturity = calendar.advance(issueDate, n, unit: Years); |
| 357 | Schedule schedule(issueDate, maturity, Period(frequency), calendar, |
| 358 | convention, convention, |
| 359 | DateGeneration::Forward, false); |
| 360 | |
| 361 | CreditDefaultSwap cds(Protection::Seller, notional, fixedRate, |
| 362 | schedule, convention, cdsDayCount, |
| 363 | true, true); |
| 364 | cds.setPricingEngine(ext::shared_ptr<PricingEngine>( |
| 365 | new MidPointCdsEngine(probabilityCurve, |
| 366 | recoveryRate, discountCurve))); |
| 367 | |
| 368 | Real NPV = cds.NPV(); |
| 369 | Rate flatRate = cds.impliedHazardRate(targetNPV: NPV, discountCurve, |
| 370 | dayCounter, |
| 371 | recoveryRate); |
| 372 | |
| 373 | if (flatRate < h1 || flatRate > h2) { |
| 374 | BOOST_ERROR("implied hazard rate outside expected range\n" |
| 375 | << " maturity: " << n << " years\n" |
| 376 | << " expected minimum: " << h1 << "\n" |
| 377 | << " expected maximum: " << h2 << "\n" |
| 378 | << " implied rate: " << flatRate); |
| 379 | } |
| 380 | |
| 381 | if (n > 6 && flatRate < latestRate) { |
| 382 | BOOST_ERROR("implied hazard rate decreasing with swap maturity\n" |
| 383 | << " maturity: " << n << " years\n" |
| 384 | << " previous rate: " << latestRate << "\n" |
| 385 | << " implied rate: " << flatRate); |
| 386 | } |
| 387 | |
| 388 | latestRate = flatRate; |
| 389 | |
| 390 | RelinkableHandle<DefaultProbabilityTermStructure> probability; |
| 391 | probability.linkTo(h: ext::shared_ptr<DefaultProbabilityTermStructure>( |
| 392 | new FlatHazardRate( |
| 393 | today, |
| 394 | Handle<Quote>(ext::shared_ptr<Quote>(new SimpleQuote(flatRate))), |
| 395 | dayCounter))); |
| 396 | |
| 397 | CreditDefaultSwap cds2(Protection::Seller, notional, fixedRate, |
| 398 | schedule, convention, cdsDayCount, |
| 399 | true, true); |
| 400 | cds2.setPricingEngine(ext::shared_ptr<PricingEngine>( |
| 401 | new MidPointCdsEngine(probability,recoveryRate, |
| 402 | discountCurve))); |
| 403 | |
| 404 | Real NPV2 = cds2.NPV(); |
| 405 | Real tolerance = 1.0; |
| 406 | if (std::fabs(x: NPV-NPV2) > tolerance) { |
| 407 | BOOST_ERROR("failed to reproduce NPV with implied rate\n" |
| 408 | << " expected: " << NPV << "\n" |
| 409 | << " calculated: " << NPV2); |
| 410 | } |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | |
| 415 | void CreditDefaultSwapTest::testFairSpread() { |
| 416 | |
| 417 | BOOST_TEST_MESSAGE( |
| 418 | "Testing fair-spread calculation for credit-default swaps..." ); |
| 419 | |
| 420 | // Initialize curves |
| 421 | Calendar calendar = TARGET(); |
| 422 | Date today = calendar.adjust(Date::todaysDate()); |
| 423 | Settings::instance().evaluationDate() = today; |
| 424 | |
| 425 | Handle<Quote> hazardRate = Handle<Quote>( |
| 426 | ext::shared_ptr<Quote>(new SimpleQuote(0.01234))); |
| 427 | RelinkableHandle<DefaultProbabilityTermStructure> probabilityCurve; |
| 428 | probabilityCurve.linkTo( |
| 429 | h: ext::shared_ptr<DefaultProbabilityTermStructure>( |
| 430 | new FlatHazardRate(0, calendar, hazardRate, Actual360()))); |
| 431 | |
| 432 | RelinkableHandle<YieldTermStructure> discountCurve; |
| 433 | discountCurve.linkTo(h: ext::shared_ptr<YieldTermStructure>( |
| 434 | new FlatForward(today,0.06,Actual360()))); |
| 435 | |
| 436 | // Build the schedule |
| 437 | Date issueDate = calendar.advance(today, n: -1, unit: Years); |
| 438 | Date maturity = calendar.advance(issueDate, n: 10, unit: Years); |
| 439 | BusinessDayConvention convention = Following; |
| 440 | |
| 441 | Schedule schedule = |
| 442 | MakeSchedule().from(effectiveDate: issueDate) |
| 443 | .to(terminationDate: maturity) |
| 444 | .withFrequency(Quarterly) |
| 445 | .withCalendar(calendar) |
| 446 | .withTerminationDateConvention(convention) |
| 447 | .withRule(DateGeneration::TwentiethIMM); |
| 448 | |
| 449 | // Build the CDS |
| 450 | Rate fixedRate = 0.001; |
| 451 | DayCounter dayCount = Actual360(); |
| 452 | Real notional = 10000.0; |
| 453 | Real recoveryRate = 0.4; |
| 454 | |
| 455 | ext::shared_ptr<PricingEngine> engine( |
| 456 | new MidPointCdsEngine(probabilityCurve,recoveryRate,discountCurve)); |
| 457 | |
| 458 | CreditDefaultSwap cds(Protection::Seller, notional, fixedRate, |
| 459 | schedule, convention, dayCount, true, true); |
| 460 | cds.setPricingEngine(engine); |
| 461 | |
| 462 | Rate fairRate = cds.fairSpread(); |
| 463 | |
| 464 | CreditDefaultSwap fairCds(Protection::Seller, notional, fairRate, |
| 465 | schedule, convention, dayCount, true, true); |
| 466 | fairCds.setPricingEngine(engine); |
| 467 | |
| 468 | Real fairNPV = fairCds.NPV(); |
| 469 | Real tolerance = 1e-9; |
| 470 | |
| 471 | if (std::fabs(x: fairNPV) > tolerance) |
| 472 | BOOST_ERROR( |
| 473 | "Failed to reproduce null NPV with calculated fair spread\n" |
| 474 | << " calculated spread: " << io::rate(fairRate) << "\n" |
| 475 | << " calculated NPV: " << fairNPV); |
| 476 | } |
| 477 | |
| 478 | void CreditDefaultSwapTest::testFairUpfront() { |
| 479 | |
| 480 | BOOST_TEST_MESSAGE( |
| 481 | "Testing fair-upfront calculation for credit-default swaps..." ); |
| 482 | |
| 483 | // Initialize curves |
| 484 | Calendar calendar = TARGET(); |
| 485 | Date today = calendar.adjust(Date::todaysDate()); |
| 486 | Settings::instance().evaluationDate() = today; |
| 487 | |
| 488 | Handle<Quote> hazardRate = Handle<Quote>( |
| 489 | ext::shared_ptr<Quote>(new SimpleQuote(0.01234))); |
| 490 | RelinkableHandle<DefaultProbabilityTermStructure> probabilityCurve; |
| 491 | probabilityCurve.linkTo( |
| 492 | h: ext::shared_ptr<DefaultProbabilityTermStructure>( |
| 493 | new FlatHazardRate(0, calendar, hazardRate, Actual360()))); |
| 494 | |
| 495 | RelinkableHandle<YieldTermStructure> discountCurve; |
| 496 | discountCurve.linkTo(h: ext::shared_ptr<YieldTermStructure>( |
| 497 | new FlatForward(today,0.06,Actual360()))); |
| 498 | |
| 499 | // Build the schedule |
| 500 | Date issueDate = today; |
| 501 | Date maturity = calendar.advance(issueDate, n: 10, unit: Years); |
| 502 | BusinessDayConvention convention = Following; |
| 503 | |
| 504 | Schedule schedule = |
| 505 | MakeSchedule().from(effectiveDate: issueDate) |
| 506 | .to(terminationDate: maturity) |
| 507 | .withFrequency(Quarterly) |
| 508 | .withCalendar(calendar) |
| 509 | .withTerminationDateConvention(convention) |
| 510 | .withRule(DateGeneration::TwentiethIMM); |
| 511 | |
| 512 | // Build the CDS |
| 513 | Rate fixedRate = 0.05; |
| 514 | Rate upfront = 0.001; |
| 515 | DayCounter dayCount = Actual360(); |
| 516 | Real notional = 10000.0; |
| 517 | Real recoveryRate = 0.4; |
| 518 | |
| 519 | ext::shared_ptr<PricingEngine> engine( |
| 520 | new MidPointCdsEngine(probabilityCurve, recoveryRate, |
| 521 | discountCurve, true)); |
| 522 | |
| 523 | CreditDefaultSwap cds(Protection::Seller, notional, upfront, fixedRate, |
| 524 | schedule, convention, dayCount, true, true); |
| 525 | cds.setPricingEngine(engine); |
| 526 | |
| 527 | Rate fairUpfront = cds.fairUpfront(); |
| 528 | |
| 529 | CreditDefaultSwap fairCds(Protection::Seller, notional, |
| 530 | fairUpfront, fixedRate, |
| 531 | schedule, convention, dayCount, true, true); |
| 532 | fairCds.setPricingEngine(engine); |
| 533 | |
| 534 | Real fairNPV = fairCds.NPV(); |
| 535 | Real tolerance = 1e-9; |
| 536 | |
| 537 | if (std::fabs(x: fairNPV) > tolerance) |
| 538 | BOOST_ERROR( |
| 539 | "Failed to reproduce null NPV with calculated fair upfront\n" |
| 540 | << " calculated upfront: " << io::rate(fairUpfront) << "\n" |
| 541 | << " calculated NPV: " << fairNPV); |
| 542 | |
| 543 | // same with null upfront to begin with |
| 544 | upfront = 0.0; |
| 545 | CreditDefaultSwap cds2(Protection::Seller, notional, upfront, fixedRate, |
| 546 | schedule, convention, dayCount, true, true); |
| 547 | cds2.setPricingEngine(engine); |
| 548 | |
| 549 | fairUpfront = cds2.fairUpfront(); |
| 550 | |
| 551 | CreditDefaultSwap fairCds2(Protection::Seller, notional, |
| 552 | fairUpfront, fixedRate, |
| 553 | schedule, convention, dayCount, true, true); |
| 554 | fairCds2.setPricingEngine(engine); |
| 555 | |
| 556 | fairNPV = fairCds2.NPV(); |
| 557 | |
| 558 | if (std::fabs(x: fairNPV) > tolerance) |
| 559 | BOOST_ERROR( |
| 560 | "Failed to reproduce null NPV with calculated fair upfront\n" |
| 561 | << " calculated upfront: " << io::rate(fairUpfront) << "\n" |
| 562 | << " calculated NPV: " << fairNPV); |
| 563 | } |
| 564 | |
| 565 | void CreditDefaultSwapTest::testIsdaEngine() { |
| 566 | |
| 567 | BOOST_TEST_MESSAGE( |
| 568 | "Testing ISDA engine calculations for credit-default swaps..." ); |
| 569 | |
| 570 | bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); |
| 571 | |
| 572 | Date tradeDate(21, May, 2009); |
| 573 | Settings::instance().evaluationDate() = tradeDate; |
| 574 | |
| 575 | |
| 576 | //build an ISDA compliant yield curve |
| 577 | //data comes from Markit published rates |
| 578 | std::vector<ext::shared_ptr<RateHelper> > isdaRateHelpers; |
| 579 | int dep_tenors[] = {1, 2, 3, 6, 9, 12}; |
| 580 | double dep_quotes[] = {0.003081, |
| 581 | 0.005525, |
| 582 | 0.007163, |
| 583 | 0.012413, |
| 584 | 0.014, |
| 585 | 0.015488}; |
| 586 | |
| 587 | for(size_t i = 0; i < sizeof(dep_tenors) / sizeof(int); i++) { |
| 588 | isdaRateHelpers.push_back(x: ext::make_shared<DepositRateHelper>( |
| 589 | args&: dep_quotes[i], args: dep_tenors[i] * Months, args: 2, |
| 590 | args: WeekendsOnly(), args: ModifiedFollowing, |
| 591 | args: false, args: Actual360() |
| 592 | ) |
| 593 | ); |
| 594 | } |
| 595 | int swap_tenors[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 25, 30}; |
| 596 | double swap_quotes[] = {0.011907, |
| 597 | 0.01699, |
| 598 | 0.021198, |
| 599 | 0.02444, |
| 600 | 0.026937, |
| 601 | 0.028967, |
| 602 | 0.030504, |
| 603 | 0.031719, |
| 604 | 0.03279, |
| 605 | 0.034535, |
| 606 | 0.036217, |
| 607 | 0.036981, |
| 608 | 0.037246, |
| 609 | 0.037605}; |
| 610 | |
| 611 | ext::shared_ptr<IborIndex> isda_ibor = ext::make_shared<IborIndex>( |
| 612 | args: "IsdaIbor" , args: 3 * Months, args: 2, args: USDCurrency(), args: WeekendsOnly(), |
| 613 | args: ModifiedFollowing, args: false, args: Actual360()); |
| 614 | for(size_t i = 0; i < sizeof(swap_tenors) / sizeof(int); i++) { |
| 615 | isdaRateHelpers.push_back(x: ext::make_shared<SwapRateHelper>( |
| 616 | args&: swap_quotes[i], args: swap_tenors[i] * Years, |
| 617 | args: WeekendsOnly(), |
| 618 | args: Semiannual, |
| 619 | args: ModifiedFollowing, args: Thirty360(Thirty360::BondBasis), args&: isda_ibor |
| 620 | ) |
| 621 | ); |
| 622 | } |
| 623 | |
| 624 | RelinkableHandle<YieldTermStructure> discountCurve; |
| 625 | discountCurve.linkTo( |
| 626 | h: ext::make_shared<PiecewiseYieldCurve<Discount, LogLinear> >( |
| 627 | args: 0, args: WeekendsOnly(), args&: isdaRateHelpers, args: Actual365Fixed()) |
| 628 | ); |
| 629 | |
| 630 | |
| 631 | RelinkableHandle<DefaultProbabilityTermStructure> probabilityCurve; |
| 632 | Date termDates[] = {Date(20, June, 2010), |
| 633 | Date(20, June, 2011), |
| 634 | Date(20, June, 2012), |
| 635 | Date(20, June, 2016), |
| 636 | Date(20, June, 2019)}; |
| 637 | Rate spreads[] = {0.001, 0.1}; |
| 638 | Rate recoveries[] = {0.2, 0.4}; |
| 639 | |
| 640 | double markitValues[] = { |
| 641 | -97798.29358, //0.001 |
| 642 | -97776.11889, //0.001 |
| 643 | 914971.5977, //0.1 |
| 644 | 894985.6298, //0.1 |
| 645 | -186921.3594, //0.001 |
| 646 | -186839.8148, //0.001 |
| 647 | 1646623.672, //0.1 |
| 648 | 1579803.626, //0.1 |
| 649 | -274298.9203, |
| 650 | -274122.4725, |
| 651 | 2279730.93, |
| 652 | 2147972.527, |
| 653 | -592420.2297, |
| 654 | -591571.2294, |
| 655 | 3993550.206, |
| 656 | 3545843.418, |
| 657 | -797501.1422, |
| 658 | -795915.9787, |
| 659 | 4702034.688, |
| 660 | 4042340.999 |
| 661 | }; |
| 662 | |
| 663 | /* When using indexes coupons, the risk-free curve is a bit off. |
| 664 | We might skip the tests altogether and rely on running them |
| 665 | with indexed coupons disabled, but leaving them can be useful anyway. */ |
| 666 | Real tolerance = usingAtParCoupons ? 1.0e-6 : 1.0e-3; |
| 667 | |
| 668 | size_t l = 0; |
| 669 | |
| 670 | for (auto termDate : termDates) { |
| 671 | for (Real spread : spreads) { |
| 672 | for (Real& recovery : recoveries) { |
| 673 | |
| 674 | ext::shared_ptr<CreditDefaultSwap> quotedTrade = |
| 675 | MakeCreditDefaultSwap(termDate, spread).withNominal(10000000.); |
| 676 | |
| 677 | Rate h = quotedTrade->impliedHazardRate(targetNPV: 0., discountCurve, dayCounter: Actual365Fixed(), |
| 678 | recoveryRate: recovery, accuracy: 1e-10, model: CreditDefaultSwap::ISDA); |
| 679 | |
| 680 | probabilityCurve.linkTo( |
| 681 | h: ext::make_shared<FlatHazardRate>(args: 0, args: WeekendsOnly(), args&: h, args: Actual365Fixed())); |
| 682 | |
| 683 | ext::shared_ptr<IsdaCdsEngine> engine = ext::make_shared<IsdaCdsEngine>( |
| 684 | args&: probabilityCurve, args&: recovery, args&: discountCurve, args: ext::nullopt, args: IsdaCdsEngine::Taylor, |
| 685 | args: IsdaCdsEngine::HalfDayBias, args: IsdaCdsEngine::Piecewise); |
| 686 | |
| 687 | ext::shared_ptr<CreditDefaultSwap> conventionalTrade = |
| 688 | MakeCreditDefaultSwap(termDate, 0.01) |
| 689 | .withNominal(10000000.) |
| 690 | .withPricingEngine(engine); |
| 691 | |
| 692 | QL_CHECK_CLOSE(conventionalTrade->notional() * conventionalTrade->fairUpfront(), |
| 693 | markitValues[l], tolerance); |
| 694 | |
| 695 | // Now testing that with the calculated fair-upfront, both Buyer and Seller sides |
| 696 | // price close to zero |
| 697 | ext::shared_ptr<CreditDefaultSwap> conventionalTradeBuy = |
| 698 | MakeCreditDefaultSwap(termDate, 0.01) |
| 699 | .withNominal(10000000.) |
| 700 | .withUpfrontRate(conventionalTrade->fairUpfront()) |
| 701 | .withSide(Protection::Buyer) |
| 702 | .withPricingEngine(engine); |
| 703 | |
| 704 | QL_CHECK_SMALL(conventionalTradeBuy->NPV(), tolerance); |
| 705 | |
| 706 | ext::shared_ptr<CreditDefaultSwap> conventionalTradeSell = |
| 707 | MakeCreditDefaultSwap(termDate, 0.01) |
| 708 | .withNominal(10000000.) |
| 709 | .withUpfrontRate(conventionalTrade->fairUpfront()) |
| 710 | .withSide(Protection::Seller) |
| 711 | .withPricingEngine(engine); |
| 712 | |
| 713 | QL_CHECK_SMALL(conventionalTradeSell->NPV(), tolerance); |
| 714 | |
| 715 | l++; |
| 716 | } |
| 717 | } |
| 718 | } |
| 719 | } |
| 720 | |
| 721 | void CreditDefaultSwapTest::testAccrualRebateAmounts() { |
| 722 | |
| 723 | BOOST_TEST_MESSAGE("Testing accrual rebate amounts on credit default swaps..." ); |
| 724 | |
| 725 | // The accrual values are taken from various test results on the ISDA CDS model website |
| 726 | // https://www.cdsmodel.com/cdsmodel/documentation.html. |
| 727 | |
| 728 | // Inputs |
| 729 | Real notional = 10000000; |
| 730 | Real spread = 0.0100; |
| 731 | Date maturity(20, Jun, 2014); |
| 732 | |
| 733 | // key is trade date and value is expected accrual |
| 734 | typedef map<Date, Real> InputData; |
| 735 | InputData inputs = { |
| 736 | {Date(18, Mar, 2009), 24166.67}, |
| 737 | {Date(19, Mar, 2009), 0.00}, |
| 738 | {Date(20, Mar, 2009), 277.78}, |
| 739 | {Date(23, Mar, 2009), 1111.11}, |
| 740 | {Date(19, Jun, 2009), 25555.56}, |
| 741 | {Date(20, Jun, 2009), 25833.33}, |
| 742 | {Date(21, Jun, 2009), 0.00}, |
| 743 | {Date(22, Jun, 2009), 277.78}, |
| 744 | {Date(18, Jun, 2014), 25277.78}, |
| 745 | {Date(19, Jun, 2014), 25555.56} |
| 746 | }; |
| 747 | |
| 748 | for (auto& input: inputs) { |
| 749 | Settings::instance().evaluationDate() = input.first; |
| 750 | CreditDefaultSwap cds = MakeCreditDefaultSwap(maturity, spread) |
| 751 | .withNominal(notional); |
| 752 | QL_CHECK_SMALL(input.second - cds.accrualRebate()->amount(), 0.01); |
| 753 | } |
| 754 | } |
| 755 | |
| 756 | void CreditDefaultSwapTest::testIsdaCalculatorReconcileSingleQuote () |
| 757 | { |
| 758 | BOOST_TEST_MESSAGE( |
| 759 | "Testing ISDA engine calculations for a single credit-default swap record (reconciliation)..." ); |
| 760 | |
| 761 | Date tradeDate(26, July, 2021); |
| 762 | Settings::instance().evaluationDate() = tradeDate; |
| 763 | |
| 764 | //build an ISDA compliant yield curve |
| 765 | //data comes from Markit published rates |
| 766 | std::vector<ext::shared_ptr<RateHelper> > isdaRateHelpers; |
| 767 | int dep_tenors[] = {1, 3, 6, 12}; |
| 768 | double dep_quotes[] = {-0.0056,-0.005440,-0.005190,-0.004930}; |
| 769 | |
| 770 | for(size_t i = 0; i < sizeof(dep_tenors) / sizeof(int); i++) { |
| 771 | isdaRateHelpers.push_back(x: ext::make_shared<DepositRateHelper>( |
| 772 | args&: dep_quotes[i], args: dep_tenors[i] * Months, args: 2, |
| 773 | args: WeekendsOnly(), args: ModifiedFollowing, |
| 774 | args: false, args: Actual360() |
| 775 | ) |
| 776 | ); |
| 777 | } |
| 778 | int swap_tenors[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 30}; |
| 779 | double swap_quotes[] = {-0.004820, |
| 780 | -0.004420, |
| 781 | -0.003990, |
| 782 | -0.003520, |
| 783 | -0.002970, |
| 784 | -0.002370, |
| 785 | -0.001760, |
| 786 | -0.001140, |
| 787 | -0.000540, |
| 788 | 0.000570, |
| 789 | 0.001880, |
| 790 | 0.002940, |
| 791 | 0.002820}; |
| 792 | |
| 793 | ext::shared_ptr<IborIndex> isda_ibor = ext::make_shared<IborIndex>( |
| 794 | args: "IsdaIbor" , args: 6 * Months, args: 2, args: EURCurrency(), args: WeekendsOnly(), |
| 795 | args: ModifiedFollowing, args: false, args: Actual360()); |
| 796 | for(size_t i = 0; i < sizeof(swap_tenors) / sizeof(int); i++) { |
| 797 | isdaRateHelpers.push_back(x: ext::make_shared<SwapRateHelper>( |
| 798 | args&: swap_quotes[i], args: swap_tenors[i] * Years, |
| 799 | args: WeekendsOnly(), |
| 800 | args: Annual, |
| 801 | args: ModifiedFollowing, args: Thirty360(Thirty360::BondBasis), args&: isda_ibor |
| 802 | ) |
| 803 | ); |
| 804 | } |
| 805 | |
| 806 | RelinkableHandle<YieldTermStructure> discountCurve; |
| 807 | discountCurve.linkTo( |
| 808 | h: ext::make_shared<PiecewiseYieldCurve<Discount, LogLinear> >( |
| 809 | args: 0, args: WeekendsOnly(), args&: isdaRateHelpers, args: Actual365Fixed()) |
| 810 | ); |
| 811 | |
| 812 | RelinkableHandle<DefaultProbabilityTermStructure> probabilityCurve; |
| 813 | Date instrumentMaturity = Date(20, June, 2026); |
| 814 | Rate coupon = 0.01, conventionalSpread = 0.006713, recovery = 0.4; |
| 815 | double nominal = 1e6, markitValue = -16070.7, expected_accrual = 1000, tolerance = 1.0e-3; |
| 816 | |
| 817 | ext::shared_ptr<CreditDefaultSwap> quotedTrade = |
| 818 | MakeCreditDefaultSwap(instrumentMaturity, conventionalSpread).withNominal(nominal); |
| 819 | |
| 820 | Rate h = quotedTrade->impliedHazardRate(targetNPV: 0., discountCurve, dayCounter: Actual365Fixed(), |
| 821 | recoveryRate: recovery, accuracy: 1e-10, model: CreditDefaultSwap::ISDA); |
| 822 | |
| 823 | probabilityCurve.linkTo( |
| 824 | h: ext::make_shared<FlatHazardRate>(args: 0, args: WeekendsOnly(), args&: h, args: Actual365Fixed())); |
| 825 | |
| 826 | ext::shared_ptr<IsdaCdsEngine> engine = ext::make_shared<IsdaCdsEngine>( |
| 827 | args&: probabilityCurve, args&: recovery, args&: discountCurve, args: ext::nullopt, args: IsdaCdsEngine::Taylor, |
| 828 | args: IsdaCdsEngine::HalfDayBias, args: IsdaCdsEngine::Piecewise); |
| 829 | |
| 830 | ext::shared_ptr<CreditDefaultSwap> conventionalTrade = |
| 831 | MakeCreditDefaultSwap(instrumentMaturity, coupon) |
| 832 | .withNominal(nominal) |
| 833 | .withPricingEngine(engine); |
| 834 | |
| 835 | |
| 836 | Real npv = conventionalTrade->NPV(); |
| 837 | Real calculated_upfront = conventionalTrade->notional() * conventionalTrade->fairUpfront(); |
| 838 | Real df = calculated_upfront / npv; // to take into account of the discount to cash settlement |
| 839 | Real derived_accrual = |
| 840 | df * (npv - |
| 841 | conventionalTrade->defaultLegNPV() - |
| 842 | conventionalTrade->couponLegNPV()); |
| 843 | |
| 844 | Real calculated_accrual = conventionalTrade->accrualRebate()->amount(); |
| 845 | |
| 846 | auto settlement_date = conventionalTrade->accrualRebate()->date(); |
| 847 | |
| 848 | QL_CHECK_CLOSE(npv, markitValue, tolerance); |
| 849 | |
| 850 | QL_CHECK_CLOSE(calculated_upfront, df * markitValue, tolerance); |
| 851 | |
| 852 | QL_CHECK_CLOSE(derived_accrual, expected_accrual, tolerance); |
| 853 | |
| 854 | QL_CHECK_CLOSE(calculated_accrual, expected_accrual, tolerance); |
| 855 | |
| 856 | BOOST_CHECK_EQUAL(settlement_date, WeekendsOnly().advance(tradeDate,3, TimeUnit::Days)); |
| 857 | |
| 858 | } |
| 859 | |
| 860 | void CreditDefaultSwapTest::testIsdaCalculatorReconcileSingleWithIssueDateInThePast () |
| 861 | { |
| 862 | BOOST_TEST_MESSAGE( |
| 863 | "Testing ISDA engine calculations for a single credit-default swap with issue date in the past..." ); |
| 864 | |
| 865 | Date valueDate(26, July, 2021); |
| 866 | Settings::instance().evaluationDate() = valueDate; |
| 867 | |
| 868 | //this is not IMM date but the settlement date is in the past so the accrual rebate |
| 869 | //should not be part of the NPV |
| 870 | Date tradeDate(20, July, 2019); |
| 871 | |
| 872 | //build an ISDA compliant yield curve |
| 873 | //data comes from Markit published rates |
| 874 | std::vector<ext::shared_ptr<RateHelper> > isdaRateHelpers; |
| 875 | int dep_tenors[] = {1, 3, 6, 12}; |
| 876 | double dep_quotes[] = {-0.0056,-0.005440,-0.005190,-0.004930}; |
| 877 | |
| 878 | for(size_t i = 0; i < sizeof(dep_tenors) / sizeof(int); i++) { |
| 879 | isdaRateHelpers.push_back(x: ext::make_shared<DepositRateHelper>( |
| 880 | args&: dep_quotes[i], args: dep_tenors[i] * Months, args: 2, |
| 881 | args: WeekendsOnly(), args: ModifiedFollowing, |
| 882 | args: false, args: Actual360() |
| 883 | ) |
| 884 | ); |
| 885 | } |
| 886 | int swap_tenors[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 30}; |
| 887 | double swap_quotes[] = {-0.004820, |
| 888 | -0.004420, |
| 889 | -0.003990, |
| 890 | -0.003520, |
| 891 | -0.002970, |
| 892 | -0.002370, |
| 893 | -0.001760, |
| 894 | -0.001140, |
| 895 | -0.000540, |
| 896 | 0.000570, |
| 897 | 0.001880, |
| 898 | 0.002940, |
| 899 | 0.002820}; |
| 900 | |
| 901 | ext::shared_ptr<IborIndex> isda_ibor = ext::make_shared<IborIndex>( |
| 902 | args: "IsdaIbor" , args: 6 * Months, args: 2, args: EURCurrency(), args: WeekendsOnly(), |
| 903 | args: ModifiedFollowing, args: false, args: Actual360()); |
| 904 | for(size_t i = 0; i < sizeof(swap_tenors) / sizeof(int); i++) { |
| 905 | isdaRateHelpers.push_back(x: ext::make_shared<SwapRateHelper>( |
| 906 | args&: swap_quotes[i], args: swap_tenors[i] * Years, |
| 907 | args: WeekendsOnly(), |
| 908 | args: Annual, |
| 909 | args: ModifiedFollowing, args: Thirty360(Thirty360::BondBasis), args&: isda_ibor |
| 910 | ) |
| 911 | ); |
| 912 | } |
| 913 | |
| 914 | RelinkableHandle<YieldTermStructure> discountCurve; |
| 915 | discountCurve.linkTo( |
| 916 | h: ext::make_shared<PiecewiseYieldCurve<Discount, LogLinear> >( |
| 917 | args: 0, args: WeekendsOnly(), args&: isdaRateHelpers, args: Actual365Fixed()) |
| 918 | ); |
| 919 | |
| 920 | RelinkableHandle<DefaultProbabilityTermStructure> probabilityCurve; |
| 921 | Date instrumentMaturity = Date(20, June, 2026); |
| 922 | Rate coupon = 0.01, conventionalSpread = 0.006713, recovery = 0.4; |
| 923 | |
| 924 | //because there is no accrual involved, the markit value is decreased as compared to the |
| 925 | //previous test (old_markit_value - old_accrual or -16070.7 - 1000) |
| 926 | double nominal = 1e6, markitValue = -17070.77, expected_accrual = 0, tolerance = 1.0e-3; |
| 927 | |
| 928 | ext::shared_ptr<CreditDefaultSwap> quotedTrade = |
| 929 | MakeCreditDefaultSwap(instrumentMaturity, conventionalSpread) |
| 930 | .withNominal(nominal); |
| 931 | |
| 932 | Rate h = quotedTrade->impliedHazardRate(targetNPV: 0., discountCurve, dayCounter: Actual365Fixed(), |
| 933 | recoveryRate: recovery, accuracy: 1e-10, model: CreditDefaultSwap::ISDA); |
| 934 | |
| 935 | probabilityCurve.linkTo( |
| 936 | h: ext::make_shared<FlatHazardRate>(args: 0, args: WeekendsOnly(), args&: h, args: Actual365Fixed())); |
| 937 | |
| 938 | ext::shared_ptr<IsdaCdsEngine> engine = ext::make_shared<IsdaCdsEngine>( |
| 939 | args&: probabilityCurve, args&: recovery, args&: discountCurve, args: ext::nullopt, args: IsdaCdsEngine::Taylor, |
| 940 | args: IsdaCdsEngine::HalfDayBias, args: IsdaCdsEngine::Piecewise); |
| 941 | |
| 942 | ext::shared_ptr<CreditDefaultSwap> conventionalTrade = |
| 943 | MakeCreditDefaultSwap(instrumentMaturity, coupon) |
| 944 | .withNominal(nominal) |
| 945 | .withPricingEngine(engine) |
| 946 | .withTradeDate(tradeDate); |
| 947 | |
| 948 | |
| 949 | Real npv = conventionalTrade->NPV(); |
| 950 | Real calculated_accrual = npv - |
| 951 | conventionalTrade->defaultLegNPV() - |
| 952 | conventionalTrade->couponLegNPV(); |
| 953 | |
| 954 | QL_CHECK_CLOSE(npv, markitValue, tolerance); |
| 955 | |
| 956 | QL_CHECK_CLOSE(calculated_accrual, expected_accrual, tolerance); |
| 957 | } |
| 958 | |
| 959 | test_suite* CreditDefaultSwapTest::suite() { |
| 960 | auto* suite = BOOST_TEST_SUITE("Credit-default swap tests" ); |
| 961 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testCachedValue)); |
| 962 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testCachedMarketValue)); |
| 963 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testImpliedHazardRate)); |
| 964 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testFairSpread)); |
| 965 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testFairUpfront)); |
| 966 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testIsdaEngine)); |
| 967 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testAccrualRebateAmounts)); |
| 968 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testIsdaCalculatorReconcileSingleQuote)); |
| 969 | suite->add(QUANTLIB_TEST_CASE(&CreditDefaultSwapTest::testIsdaCalculatorReconcileSingleWithIssueDateInThePast)); |
| 970 | return suite; |
| 971 | } |
| 972 | |