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 Copyright (C) 2009 Ferdinando Ametrano
6
7 This file is part of QuantLib, a free-software/open-source library
8 for financial quantitative analysts and developers - http://quantlib.org/
9
10 QuantLib is free software: you can redistribute it and/or modify it
11 under the terms of the QuantLib license. You should have received a
12 copy of the license along with this program; if not, please email
13 <quantlib-dev@lists.sf.net>. The license is also available online at
14 <http://quantlib.org/license.shtml>.
15
16 This program is distributed in the hope that it will be useful, but WITHOUT
17 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18 FOR A PARTICULAR PURPOSE. See the license for more details.
19*/
20
21#include "defaultprobabilitycurves.hpp"
22#include "utilities.hpp"
23#include <ql/instruments/creditdefaultswap.hpp>
24#include <ql/math/interpolations/backwardflatinterpolation.hpp>
25#include <ql/math/interpolations/linearinterpolation.hpp>
26#include <ql/math/interpolations/loginterpolation.hpp>
27#include <ql/pricingengines/credit/midpointcdsengine.hpp>
28#include <ql/quotes/simplequote.hpp>
29#include <ql/termstructures/credit/defaultprobabilityhelpers.hpp>
30#include <ql/termstructures/credit/flathazardrate.hpp>
31#include <ql/termstructures/credit/piecewisedefaultcurve.hpp>
32#include <ql/termstructures/yield/discountcurve.hpp>
33#include <ql/termstructures/yield/flatforward.hpp>
34#include <ql/time/calendars/target.hpp>
35#include <ql/time/calendars/weekendsonly.hpp>
36#include <ql/time/daycounters/actual360.hpp>
37#include <ql/time/daycounters/thirty360.hpp>
38#include <ql/utilities/dataformatters.hpp>
39#include <iomanip>
40#include <map>
41#include <string>
42#include <utility>
43#include <vector>
44
45using namespace QuantLib;
46using namespace boost::unit_test_framework;
47using std::map;
48using std::vector;
49using std::string;
50
51void DefaultProbabilityCurveTest::testDefaultProbability() {
52
53 BOOST_TEST_MESSAGE("Testing default-probability structure...");
54
55 Real hazardRate = 0.0100;
56 Handle<Quote> hazardRateQuote = Handle<Quote>(
57 ext::shared_ptr<Quote>(new SimpleQuote(hazardRate)));
58 DayCounter dayCounter = Actual360();
59 Calendar calendar = TARGET();
60 Size n = 20;
61
62 double tolerance = 1.0e-10;
63 Date today = Settings::instance().evaluationDate();
64 Date startDate = today;
65 Date endDate = startDate;
66
67 FlatHazardRate flatHazardRate(startDate, hazardRateQuote, dayCounter);
68
69 for(Size i=0; i<n; i++){
70 startDate = endDate;
71 endDate = calendar.advance(endDate, n: 1, unit: Years);
72
73 Probability pStart = flatHazardRate.defaultProbability(d: startDate);
74 Probability pEnd = flatHazardRate.defaultProbability(d: endDate);
75
76 Probability pBetweenComputed =
77 flatHazardRate.defaultProbability(startDate, endDate);
78
79 Probability pBetween = pEnd - pStart;
80
81 if (std::fabs(x: pBetween - pBetweenComputed) > tolerance)
82 BOOST_ERROR(
83 "Failed to reproduce probability(d1, d2) "
84 << "for default probability structure\n"
85 << std::setprecision(12)
86 << " calculated probability: " << pBetweenComputed << "\n"
87 << " expected probability: " << pBetween);
88
89 Time t2 = dayCounter.yearFraction(d1: today, d2: endDate);
90 Probability timeProbability = flatHazardRate.defaultProbability(t: t2);
91 Probability dateProbability =
92 flatHazardRate.defaultProbability(d: endDate);
93
94 if (std::fabs(x: timeProbability - dateProbability) > tolerance)
95 BOOST_ERROR(
96 "single-time probability and single-date probability do not match\n"
97 << std::setprecision(10)
98 << " time probability: " << timeProbability << "\n"
99 << " date probability: " << dateProbability);
100
101 Time t1 = dayCounter.yearFraction(d1: today, d2: startDate);
102 timeProbability = flatHazardRate.defaultProbability(t1, t2);
103 dateProbability = flatHazardRate.defaultProbability(startDate, endDate);
104
105 if (std::fabs(x: timeProbability - dateProbability) > tolerance)
106 BOOST_ERROR(
107 "double-time probability and double-date probability do not match\n"
108 << std::setprecision(10)
109 << " time probability: " << timeProbability << "\n"
110 << " date probability: " << dateProbability);
111 }
112}
113
114
115void DefaultProbabilityCurveTest::testFlatHazardRate() {
116
117 BOOST_TEST_MESSAGE("Testing flat hazard rate...");
118
119 Real hazardRate = 0.0100;
120 Handle<Quote> hazardRateQuote = Handle<Quote>(
121 ext::shared_ptr<Quote>(new SimpleQuote(hazardRate)));
122 DayCounter dayCounter = Actual360();
123 Calendar calendar = TARGET();
124 Size n = 20;
125
126 double tolerance = 1.0e-10;
127 Date today = Settings::instance().evaluationDate();
128 Date startDate = today;
129 Date endDate = startDate;
130
131 FlatHazardRate flatHazardRate(today, hazardRateQuote, dayCounter);
132
133 for(Size i=0; i<n; i++){
134 endDate = calendar.advance(endDate, n: 1, unit: Years);
135 Time t = dayCounter.yearFraction(d1: startDate, d2: endDate);
136 Probability probability = 1.0 - std::exp(x: -hazardRate * t);
137 Probability computedProbability = flatHazardRate.defaultProbability(t);
138
139 if (std::fabs(x: probability - computedProbability) > tolerance)
140 BOOST_ERROR(
141 "Failed to reproduce probability for flat hazard rate\n"
142 << std::setprecision(10)
143 << " calculated probability: " << computedProbability << "\n"
144 << " expected probability: " << probability);
145 }
146}
147
148
149namespace {
150
151 template <class T, class I>
152 void testBootstrapFromSpread() {
153
154 Calendar calendar = TARGET();
155
156 Date today = Settings::instance().evaluationDate();
157
158 Integer settlementDays = 1;
159
160 std::vector<Real> quote = {0.005, 0.006, 0.007, 0.009};
161 std::vector<Integer> n = {1, 2, 3, 5};
162
163 Frequency frequency = Quarterly;
164 BusinessDayConvention convention = Following;
165 DateGeneration::Rule rule = DateGeneration::TwentiethIMM;
166 DayCounter dayCounter = Thirty360(Thirty360::BondBasis);
167 Real recoveryRate = 0.4;
168
169 RelinkableHandle<YieldTermStructure> discountCurve;
170 discountCurve.linkTo(h: ext::shared_ptr<YieldTermStructure>(
171 new FlatForward(today,0.06,Actual360())));
172
173 std::vector<ext::shared_ptr<DefaultProbabilityHelper> > helpers;
174
175 for(Size i=0; i<n.size(); i++)
176 helpers.push_back(
177 x: ext::shared_ptr<DefaultProbabilityHelper>(
178 new SpreadCdsHelper(quote[i], Period(n[i], Years),
179 settlementDays, calendar,
180 frequency, convention, rule,
181 dayCounter, recoveryRate,
182 discountCurve)));
183
184 RelinkableHandle<DefaultProbabilityTermStructure> piecewiseCurve;
185 piecewiseCurve.linkTo(
186 h: ext::shared_ptr<DefaultProbabilityTermStructure>(
187 new PiecewiseDefaultCurve<T,I>(today, helpers,
188 Thirty360(Thirty360::BondBasis))));
189
190 Real notional = 1.0;
191 double tolerance = 1.0e-6;
192
193 {
194 SavedSettings restore;
195 // ensure apple-to-apple comparison
196 Settings::instance().includeTodaysCashFlows() = true;
197
198 for (Size i=0; i<n.size(); i++) {
199 Date protectionStart = today + settlementDays;
200 Date startDate = calendar.adjust(protectionStart, convention);
201 Date endDate = today + n[i]*Years;
202
203 Schedule schedule(startDate, endDate, Period(frequency), calendar,
204 convention, Unadjusted, rule, false);
205
206 CreditDefaultSwap cds(Protection::Buyer, notional, quote[i],
207 schedule, convention, dayCounter,
208 true, true, protectionStart);
209 cds.setPricingEngine(ext::shared_ptr<PricingEngine>(
210 new MidPointCdsEngine(piecewiseCurve, recoveryRate,
211 discountCurve)));
212
213 // test
214 Rate inputRate = quote[i];
215 Rate computedRate = cds.fairSpread();
216 if (std::fabs(x: inputRate - computedRate) > tolerance)
217 BOOST_ERROR(
218 "\nFailed to reproduce fair spread for " << n[i] <<
219 "Y credit-default swaps\n"
220 << std::setprecision(10)
221 << " computed rate: " << io::rate(computedRate) << "\n"
222 << " input rate: " << io::rate(inputRate));
223 }
224 }
225 }
226
227
228 template <class T, class I>
229 void testBootstrapFromUpfront() {
230
231 Calendar calendar = TARGET();
232
233 Date today = Settings::instance().evaluationDate();
234
235 Integer settlementDays = 1;
236
237 std::vector<Real> quote = {0.01, 0.02, 0.04, 0.06};
238 std::vector<Integer> n = {2, 3, 5, 7};
239
240 Rate fixedRate = 0.05;
241 Frequency frequency = Quarterly;
242 BusinessDayConvention convention = ModifiedFollowing;
243 DateGeneration::Rule rule = DateGeneration::CDS;
244 DayCounter dayCounter = Actual360();
245 Real recoveryRate = 0.4;
246 Integer upfrontSettlementDays = 3;
247
248 RelinkableHandle<YieldTermStructure> discountCurve;
249 discountCurve.linkTo(h: ext::shared_ptr<YieldTermStructure>(
250 new FlatForward(today,0.06,Actual360())));
251
252 std::vector<ext::shared_ptr<DefaultProbabilityHelper> > helpers;
253
254 for(Size i=0; i<n.size(); i++)
255 helpers.push_back(
256 x: ext::shared_ptr<DefaultProbabilityHelper>(
257 new UpfrontCdsHelper(quote[i], fixedRate,
258 Period(n[i], Years),
259 settlementDays, calendar,
260 frequency, convention, rule,
261 dayCounter, recoveryRate,
262 discountCurve,
263 upfrontSettlementDays,
264 true, true, Date(), Actual360(true))));
265
266 RelinkableHandle<DefaultProbabilityTermStructure> piecewiseCurve;
267 piecewiseCurve.linkTo(
268 h: ext::shared_ptr<DefaultProbabilityTermStructure>(
269 new PiecewiseDefaultCurve<T,I>(today, helpers,
270 Thirty360(Thirty360::BondBasis))));
271
272 Real notional = 1.0;
273 double tolerance = 1.0e-6;
274
275 {
276 SavedSettings backup;
277 // ensure apple-to-apple comparison
278 Settings::instance().includeTodaysCashFlows() = true;
279
280 for (Size i=0; i<n.size(); i++) {
281 Date protectionStart = today + settlementDays;
282 Date startDate = protectionStart;
283 Date endDate = cdsMaturity(tradeDate: today, tenor: n[i] * Years, rule);
284 Date upfrontDate = calendar.advance(today,
285 n: upfrontSettlementDays,
286 unit: Days,
287 convention);
288
289 Schedule schedule(startDate, endDate, Period(frequency), calendar,
290 convention, Unadjusted, rule, false);
291
292 CreditDefaultSwap cds(Protection::Buyer, notional,
293 quote[i], fixedRate,
294 schedule, convention, dayCounter,
295 true, true, protectionStart,
296 upfrontDate,
297 ext::shared_ptr<Claim>(),
298 Actual360(true),
299 true, today);
300 cds.setPricingEngine(ext::shared_ptr<PricingEngine>(
301 new MidPointCdsEngine(piecewiseCurve, recoveryRate,
302 discountCurve, true)));
303
304 // test
305 Rate inputUpfront = quote[i];
306 Rate computedUpfront = cds.fairUpfront();
307 if (std::fabs(x: inputUpfront - computedUpfront) > tolerance)
308 BOOST_ERROR(
309 "\nFailed to reproduce fair upfront for " << n[i] <<
310 "Y credit-default swaps\n"
311 << std::setprecision(10)
312 << " computed: " << io::rate(computedUpfront) << "\n"
313 << " expected: " << io::rate(inputUpfront));
314 }
315 }
316 }
317
318}
319
320void DefaultProbabilityCurveTest::testFlatHazardConsistency() {
321 BOOST_TEST_MESSAGE("Testing piecewise-flat hazard-rate consistency...");
322 testBootstrapFromSpread<HazardRate,BackwardFlat>();
323 testBootstrapFromUpfront<HazardRate,BackwardFlat>();
324}
325
326void DefaultProbabilityCurveTest::testFlatDensityConsistency() {
327 BOOST_TEST_MESSAGE("Testing piecewise-flat default-density consistency...");
328 testBootstrapFromSpread<DefaultDensity,BackwardFlat>();
329 testBootstrapFromUpfront<DefaultDensity,BackwardFlat>();
330}
331
332void DefaultProbabilityCurveTest::testLinearDensityConsistency() {
333 BOOST_TEST_MESSAGE("Testing piecewise-linear default-density consistency...");
334 testBootstrapFromSpread<DefaultDensity,Linear>();
335 testBootstrapFromUpfront<DefaultDensity,Linear>();
336}
337
338void DefaultProbabilityCurveTest::testLogLinearSurvivalConsistency() {
339 BOOST_TEST_MESSAGE("Testing log-linear survival-probability consistency...");
340 testBootstrapFromSpread<SurvivalProbability,LogLinear>();
341 testBootstrapFromUpfront<SurvivalProbability,LogLinear>();
342}
343
344void DefaultProbabilityCurveTest::testSingleInstrumentBootstrap() {
345 BOOST_TEST_MESSAGE("Testing single-instrument curve bootstrap...");
346
347 Calendar calendar = TARGET();
348
349 Date today = Settings::instance().evaluationDate();
350
351 Integer settlementDays = 0;
352
353 Real quote = 0.005;
354 Period tenor = 2*Years;
355
356 Frequency frequency = Quarterly;
357 BusinessDayConvention convention = Following;
358 DateGeneration::Rule rule = DateGeneration::TwentiethIMM;
359 DayCounter dayCounter = Thirty360(Thirty360::BondBasis);
360 Real recoveryRate = 0.4;
361
362 RelinkableHandle<YieldTermStructure> discountCurve;
363 discountCurve.linkTo(h: ext::shared_ptr<YieldTermStructure>(
364 new FlatForward(today,0.06,Actual360())));
365
366 std::vector<ext::shared_ptr<DefaultProbabilityHelper> > helpers(1);
367
368 helpers[0] = ext::shared_ptr<DefaultProbabilityHelper>(
369 new SpreadCdsHelper(quote, tenor,
370 settlementDays, calendar,
371 frequency, convention, rule,
372 dayCounter, recoveryRate,
373 discountCurve));
374
375 PiecewiseDefaultCurve<HazardRate,BackwardFlat> defaultCurve(today, helpers,
376 dayCounter);
377 defaultCurve.recalculate();
378}
379
380void DefaultProbabilityCurveTest::testUpfrontBootstrap() {
381 BOOST_TEST_MESSAGE("Testing bootstrap on upfront quotes...");
382
383 // Setting this to false would prevent the upfront from being used.
384 // By checking that the bootstrap works, we indirectly check that
385 // UpfrontCdsHelper::impliedQuote() overrides it.
386 Settings::instance().includeTodaysCashFlows() = false;
387
388 testBootstrapFromUpfront<HazardRate,BackwardFlat>();
389
390 // This checks that UpfrontCdsHelper::impliedQuote() didn't
391 // override the flag permanently; after the bootstrap, it should
392 // go back to its previous value.
393 ext::optional<bool> flag = Settings::instance().includeTodaysCashFlows();
394 if (flag != false)
395 BOOST_ERROR("Cash-flow settings improperly modified");
396}
397
398/* This test attempts to build a default curve from CDS spreads as of 1 Apr 2020. The spreads are real and from a
399 distressed reference entity with an inverted CDS spread curve. Using the default IterativeBootstrap with no
400 retries, the default curve building fails. Allowing retries, it expands the min survival probability bounds but
401 still fails. We set dontThrow to true in IterativeBootstrap to use a fall back curve.
402*/
403void DefaultProbabilityCurveTest::testIterativeBootstrapRetries() {
404
405 BOOST_TEST_MESSAGE("Testing iterative bootstrap with retries...");
406
407 Date asof(1, Apr, 2020);
408 Settings::instance().evaluationDate() = asof;
409 Actual365Fixed tsDayCounter;
410
411 // USD discount curve built out of FedFunds OIS swaps.
412 vector<Date> usdCurveDates = {
413 Date(1, Apr, 2020),
414 Date(2, Apr, 2020),
415 Date(14, Apr, 2020),
416 Date(21, Apr, 2020),
417 Date(28, Apr, 2020),
418 Date(6, May, 2020),
419 Date(5, Jun, 2020),
420 Date(7, Jul, 2020),
421 Date(5, Aug, 2020),
422 Date(8, Sep, 2020),
423 Date(7, Oct, 2020),
424 Date(5, Nov, 2020),
425 Date(7, Dec, 2020),
426 Date(6, Jan, 2021),
427 Date(5, Feb, 2021),
428 Date(5, Mar, 2021),
429 Date(7, Apr, 2021),
430 Date(4, Apr, 2022),
431 Date(3, Apr, 2023),
432 Date(3, Apr, 2024),
433 Date(3, Apr, 2025),
434 Date(5, Apr, 2027),
435 Date(3, Apr, 2030),
436 Date(3, Apr, 2035),
437 Date(3, Apr, 2040),
438 Date(4, Apr, 2050)
439 };
440
441 vector<DiscountFactor> usdCurveDfs = {
442 1.000000000,
443 0.999955835,
444 0.999931070,
445 0.999914629,
446 0.999902799,
447 0.999887990,
448 0.999825782,
449 0.999764392,
450 0.999709076,
451 0.999647785,
452 0.999594638,
453 0.999536198,
454 0.999483093,
455 0.999419291,
456 0.999379417,
457 0.999324981,
458 0.999262356,
459 0.999575101,
460 0.996135441,
461 0.995228348,
462 0.989366687,
463 0.979271200,
464 0.961150726,
465 0.926265361,
466 0.891640651,
467 0.839314063
468 };
469
470 Handle<YieldTermStructure> usdYts(ext::make_shared<InterpolatedDiscountCurve<LogLinear> >(
471 args&: usdCurveDates, args&: usdCurveDfs, args&: tsDayCounter));
472
473 // CDS spreads
474 map<Period, Rate> cdsSpreads = {
475 {6 * Months, 2.957980250},
476 {1 * Years, 3.076933100},
477 {2 * Years, 2.944524520},
478 {3 * Years, 2.844498960},
479 {4 * Years, 2.769234420},
480 {5 * Years, 2.713474100}
481 };
482 Real recoveryRate = 0.035;
483
484 // Conventions
485 Integer settlementDays = 1;
486 WeekendsOnly calendar;
487 Frequency frequency = Quarterly;
488 BusinessDayConvention paymentConvention = Following;
489 DateGeneration::Rule rule = DateGeneration::CDS2015;
490 Actual360 dayCounter;
491 Actual360 lastPeriodDayCounter(true);
492
493 // Create the CDS spread helpers.
494 vector<ext::shared_ptr<DefaultProbabilityHelper> > instruments;
495 for (map<Period, Rate>::const_iterator it = cdsSpreads.begin(); it != cdsSpreads.end(); ++it) {
496 instruments.push_back(x: ext::make_shared<SpreadCdsHelper>(
497 args: it->second, args: it->first, args&: settlementDays, args&: calendar,
498 args&: frequency, args&: paymentConvention, args&: rule, args&: dayCounter, args&: recoveryRate, args&: usdYts, args: true, args: true, args: Date(),
499 args&: lastPeriodDayCounter));
500 }
501
502 // Create the default curve with the default IterativeBootstrap.
503 typedef PiecewiseDefaultCurve<SurvivalProbability, LogLinear, IterativeBootstrap> SPCurve;
504 ext::shared_ptr<DefaultProbabilityTermStructure> dpts = ext::make_shared<SPCurve>(args&: asof, args&: instruments, args&: tsDayCounter);
505
506 // Check that the default curve throws by requesting a default probability.
507 Date testDate(21, Dec, 2020);
508 BOOST_CHECK_EXCEPTION(dpts->survivalProbability(testDate), Error,
509 ExpectedErrorMessage("1st iteration: failed at 1st alive instrument"));
510
511 // Create the default curve with an IterativeBootstrap allowing for 4 retries.
512 // Use a maxFactor value of 1.0 so that we still use the previous survival probability at each pillar. In other
513 // words, the survival probability cannot increase with time so best max at current pillar is the previous
514 // pillar's value - there is no point increasing it on a retry.
515 IterativeBootstrap<SPCurve> ib(Null<Real>(), Null<Real>(), Null<Real>(), 5, 1.0, 10.0);
516 dpts = ext::make_shared<SPCurve>(args&: asof, args&: instruments, args&: tsDayCounter, args&: ib);
517
518 // Check that the default curve still throws. It throws at the third pillar because the survival probability is
519 // too low at the second pillar.
520 BOOST_CHECK_EXCEPTION(dpts->survivalProbability(testDate), Error,
521 ExpectedErrorMessage("1st iteration: failed at 3rd alive instrument"));
522
523 // Create the default curve with an IterativeBootstrap that allows for 4 retries and does not throw.
524 IterativeBootstrap<SPCurve> ibNoThrow(Null<Real>(), Null<Real>(), Null<Real>(), 5, 1.0, 10.0, true, 2);
525 dpts = ext::make_shared<SPCurve>(args&: asof, args&: instruments, args&: tsDayCounter, args&: ibNoThrow);
526 BOOST_CHECK_NO_THROW(dpts->survivalProbability(testDate));
527}
528
529
530test_suite* DefaultProbabilityCurveTest::suite() {
531 auto* suite = BOOST_TEST_SUITE("Default-probability curve tests");
532 suite->add(QUANTLIB_TEST_CASE(
533 &DefaultProbabilityCurveTest::testDefaultProbability));
534 suite->add(QUANTLIB_TEST_CASE(
535 &DefaultProbabilityCurveTest::testFlatHazardRate));
536 suite->add(QUANTLIB_TEST_CASE(
537 &DefaultProbabilityCurveTest::testFlatHazardConsistency));
538 suite->add(QUANTLIB_TEST_CASE(
539 &DefaultProbabilityCurveTest::testFlatDensityConsistency));
540 suite->add(QUANTLIB_TEST_CASE(
541 &DefaultProbabilityCurveTest::testLinearDensityConsistency));
542 suite->add(QUANTLIB_TEST_CASE(
543 &DefaultProbabilityCurveTest::testLogLinearSurvivalConsistency));
544 suite->add(QUANTLIB_TEST_CASE(
545 &DefaultProbabilityCurveTest::testSingleInstrumentBootstrap));
546 suite->add(QUANTLIB_TEST_CASE(
547 &DefaultProbabilityCurveTest::testUpfrontBootstrap));
548 suite->add(QUANTLIB_TEST_CASE(
549 &DefaultProbabilityCurveTest::testIterativeBootstrapRetries));
550 return suite;
551}
552

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