From bac132fb96fe5b614bfea5931ab38713853d180a Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 13 Apr 2020 16:09:27 +0100 Subject: [PATCH 01/12] Extra IterativeBootstrap parameters that allow for expanding search bounds. --- ql/termstructures/iterativebootstrap.hpp | 69 +++++++++++++++++++----- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/ql/termstructures/iterativebootstrap.hpp b/ql/termstructures/iterativebootstrap.hpp index d63e41d9693..c0e0d936b33 100644 --- a/ql/termstructures/iterativebootstrap.hpp +++ b/ql/termstructures/iterativebootstrap.hpp @@ -42,15 +42,30 @@ namespace QuantLib { typedef typename Curve::traits_type Traits; typedef typename Curve::interpolator_type Interpolator; public: + /*! Constructor + \param accuracy Accuracy for the bootstrap stopping criterion. If it is set to + \c Null(), its value is taken from the termstructure's accuracy. + \param minValue Allow to override the initial minimum value coming from traits. + \param maxValue Allow to override the initial maximum value coming from traits. + \param maxAttempts Number of attempts on each iteration. A number greater than 1 implies retries. + \param maxFactor Factor for max value retry on each iteration if there is a failure. + \param minFactor Factor for min value retry on each iteration if there is a failure. + */ IterativeBootstrap(Real accuracy = Null(), Real minValue = Null(), - Real maxValue = Null()); + Real maxValue = Null(), + Size maxAttempts = 1, + Real maxFactor = 2.0, + Real minFactor = 2.0); void setup(Curve* ts); void calculate() const; private: void initialize() const; Real accuracy_; Real minValue_, maxValue_; + Size maxAttempts_; + Real maxFactor_; + Real minFactor_; Curve* ts_; Size n_; Brent firstSolver_; @@ -65,8 +80,10 @@ namespace QuantLib { // template definitions template - IterativeBootstrap::IterativeBootstrap(Real accuracy, Real minValue, Real maxValue) + IterativeBootstrap::IterativeBootstrap(Real accuracy, Real minValue, Real maxValue, + Size maxAttempts, Real maxFactor, Real minFactor) : accuracy_(accuracy), minValue_(minValue), maxValue_(maxValue), + maxAttempts_(maxAttempts), maxFactor_(maxFactor), minFactor_(minFactor), ts_(0), initialized_(false), validCurve_(false), loopRequired_(Interpolator::global) {} @@ -192,20 +209,35 @@ namespace QuantLib { for (Size iteration=0; ; ++iteration) { previousData_ = ts_->data_; + // Store min value and max value at each pillar so that we can expand search if necessary. + std::vector minValues(alive_, Null()); + std::vector maxValues(alive_, Null()); + std::vector attempts(alive_, 1); + for (Size i=1; i<=alive_; ++i) { // pillar loop // bracket root and calculate guess - Real min = minValue_ != Null() ? minValue_ : - Traits::minValueAfter(i, ts_, validData, firstAliveHelper_); - Real max = maxValue_ != Null() ? maxValue_ : - Traits::maxValueAfter(i, ts_, validData, firstAliveHelper_); - + if (minValues[i - 1] == Null()) { + minValues[i - 1] = minValue_ != Null() ? minValue_ : + Traits::minValueAfter(i, ts_, validData, firstAliveHelper_); + } else { + minValues[i - 1] = minValues[i - 1] < 0.0 ? + minFactor_ * minValues[i - 1] : minValues[i - 1] / minFactor_; + } + if (maxValues[i - 1] == Null()) { + maxValues[i - 1] = maxValue_ != Null() ? maxValue_ : + Traits::maxValueAfter(i, ts_, validData, firstAliveHelper_); + } else { + maxValues[i - 1] = maxValues[i - 1] > 0.0 ? + maxFactor_ * maxValues[i - 1] : maxValues[i - 1] / maxFactor_; + } Real guess = Traits::guess(i, ts_, validData, firstAliveHelper_); + // adjust guess if needed - if (guess>=max) - guess = max - (max-min)/5.0; - else if (guess<=min) - guess = min + (max-min)/5.0; + if (guess >= maxValues[i - 1]) + guess = maxValues[i - 1] - (maxValues[i - 1] - minValues[i - 1]) / 5.0; + else if (guess <= minValues[i - 1]) + guess = minValues[i - 1] + (maxValues[i - 1] - minValues[i - 1]) / 5.0; // extend interpolation if needed if (!validData) { @@ -227,9 +259,9 @@ namespace QuantLib { try { if (validData) - solver_.solve(*errors_[i], accuracy, guess, min, max); + solver_.solve(*errors_[i], accuracy, guess, minValues[i - 1], maxValues[i - 1]); else - firstSolver_.solve(*errors_[i], accuracy, guess, min, max); + firstSolver_.solve(*errors_[i], accuracy, guess, minValues[i - 1], maxValues[i - 1]); } catch (std::exception &e) { if (validCurve_) { // the previous curve state might have been a @@ -242,7 +274,16 @@ namespace QuantLib { calculate(); return; } - QL_FAIL(io::ordinal(iteration+1) << " iteration: failed " + + // If we have more attempts left on this iteration, try again. Note that the max and min + // bounds will be widened on the retry. + if (attempts[i - 1] < maxAttempts_) { + attempts[i - 1]++; + i--; + continue; + } + + QL_FAIL(io::ordinal(iteration + 1) << " iteration: failed " "at " << io::ordinal(i) << " alive instrument, " "pillar " << errors_[i]->helper()->pillarDate() << ", maturity " << errors_[i]->helper()->maturityDate() << From e5bf4ee8bb9311026c673bbc85d4e174e32777e2 Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 13 Apr 2020 16:10:35 +0100 Subject: [PATCH 02/12] Unit test - piecewise yield curve with expanding bounds. --- test-suite/piecewiseyieldcurve.cpp | 132 +++++++++++++++++++++++++++++ test-suite/piecewiseyieldcurve.hpp | 2 + 2 files changed, 134 insertions(+) diff --git a/test-suite/piecewiseyieldcurve.cpp b/test-suite/piecewiseyieldcurve.cpp index 6e71e8abd1c..d487f25acbd 100644 --- a/test-suite/piecewiseyieldcurve.cpp +++ b/test-suite/piecewiseyieldcurve.cpp @@ -52,9 +52,18 @@ #include #include #include +#include +#include +#include +#include using namespace QuantLib; using namespace boost::unit_test_framework; +using boost::assign::list_of; +using boost::assign::map_list_of; +using std::map; +using std::vector; +using std::string; namespace piecewise_yield_curve_test { @@ -630,6 +639,21 @@ namespace piecewise_yield_curve_test { } } + // Used to check that the exception message contains the expected message string, expMsg. + struct ExpErrorPred { + + ExpErrorPred(const string& msg) : expMsg(msg) {} + + bool operator()(const Error& ex) { + string errMsg(ex.what()); + BOOST_TEST_MESSAGE("Error expected to contain: '" << expMsg << "'."); + BOOST_TEST_MESSAGE("Actual error is: '" << errMsg << "'."); + return errMsg.find(expMsg) != string::npos; + } + + string expMsg; + }; + } @@ -1364,6 +1388,112 @@ void PiecewiseYieldCurveTest::testGlobalBootstrap() { } } +/* This test attempts to build an ARS collateralised in USD curve as of 25 Sep 2019. Using the default + IterativeBootstrap with no retries, the yield curve building fails. Allowing retries, it expands the min and max + bounds and passes. +*/ +void PiecewiseYieldCurveTest::testIterativeBootstrapRetries() { + + BOOST_TEST_MESSAGE("Testing iterative boostrap with retries..."); + + SavedSettings backup; + + Date asof(25, Sep, 2019); + Settings::instance().evaluationDate() = asof; + Actual365Fixed tsDayCounter; + + // USD discount curve built out of FedFunds OIS swaps. + vector usdCurveDates = list_of + (Date(25, Sep, 2019)) + (Date(26, Sep, 2019)) + (Date(8, Oct, 2019)) + (Date(16, Oct, 2019)) + (Date(22, Oct, 2019)) + (Date(30, Oct, 2019)) + (Date(2, Dec, 2019)) + (Date(31, Dec, 2019)) + (Date(29, Jan, 2020)) + (Date(2, Mar, 2020)) + (Date(31, Mar, 2020)) + (Date(29, Apr, 2020)) + (Date(29, May, 2020)) + (Date(1, Jul, 2020)) + (Date(29, Jul, 2020)) + (Date(31, Aug, 2020)) + (Date(30, Sep, 2020)); + + vector usdCurveDfs = list_of + (1.000000000) + (0.999940837) + (0.999309357) + (0.998894646) + (0.998574816) + (0.998162528) + (0.996552511) + (0.995197584) + (0.993915264) + (0.992530008) + (0.991329696) + (0.990179606) + (0.989005698) + (0.987751691) + (0.986703371) + (0.985495036) + (0.984413446); + + Handle usdYts(ext::make_shared>( + usdCurveDates, usdCurveDfs, tsDayCounter)); + + // USD/ARS forward points + Handle arsSpot(ext::make_shared(56.881)); + map arsFwdPoints = map_list_of + (1 * Months, 8.5157) + (2 * Months, 12.7180) + (3 * Months, 17.8310) + (6 * Months, 30.3680) + (9 * Months, 45.5520) + (1 * Years, 60.7370); + + // Create the FX swap rate helpers for the ARS in USD curve. + vector> instruments; + for (map::const_iterator it = arsFwdPoints.begin(); it != arsFwdPoints.end(); it++) { + Handle arsFwd(ext::make_shared(it->second)); + instruments.push_back(ext::make_shared(arsFwd, arsSpot, it->first, 2, + UnitedStates(), Following, false, true, usdYts)); + } + + // Create the ARS in USD curve with the default IterativeBootstrap. + typedef PiecewiseYieldCurve LLDFCurve; + ext::shared_ptr arsYts = ext::make_shared(asof, instruments, tsDayCounter); + + // USD/ARS spot date. The date on which we check the ARS discount curve. + Date spotDate(27, Sep, 2019); + + // Check that the ARS in USD curve throws by requesting a discount factor. + using piecewise_yield_curve_test::ExpErrorPred; + BOOST_CHECK_EXCEPTION(arsYts->discount(spotDate), Error, + ExpErrorPred("1st iteration: failed at 1st alive instrument")); + + // Create the ARS in USD curve with an IterativeBootstrap allowing for 4 retries. + IterativeBootstrap ib(Null(), Null(), Null(), 5); + arsYts = ext::make_shared(asof, instruments, tsDayCounter, ib); + + // Check that the ARS in USD curve builds and populate the spot ARS discount factor. + DiscountFactor spotDfArs; + BOOST_REQUIRE_NO_THROW(spotDfArs = arsYts->discount(spotDate)); + + // Additional dates and discount factors used in the final check i.e. that calculated 1Y FX forward equals input. + Date oneYearFwdDate(28, Sep, 2020); + DiscountFactor spotDfUsd = usdYts->discount(spotDate); + DiscountFactor oneYearDfUsd = usdYts->discount(oneYearFwdDate); + + // Given that the ARS in USD curve builds, check that the 1Y USD/ARS forward rate is as expected. + DiscountFactor oneYearDfArs = arsYts->discount(oneYearFwdDate); + Real calcFwd = (spotDfArs * arsSpot->value() / oneYearDfArs) / (spotDfUsd / oneYearDfUsd); + Real expFwd = arsSpot->value() + arsFwdPoints.at(1 * Years); + BOOST_CHECK_SMALL(calcFwd - expFwd, 1e-10); +} + test_suite* PiecewiseYieldCurveTest::suite() { test_suite* suite = BOOST_TEST_SUITE("Piecewise yield curve tests"); @@ -1419,5 +1549,7 @@ test_suite* PiecewiseYieldCurveTest::suite() { suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testGlobalBootstrap)); #endif + suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testIterativeBootstrapRetries)); + return suite; } diff --git a/test-suite/piecewiseyieldcurve.hpp b/test-suite/piecewiseyieldcurve.hpp index 411417e0b2a..46b66eb70a1 100644 --- a/test-suite/piecewiseyieldcurve.hpp +++ b/test-suite/piecewiseyieldcurve.hpp @@ -59,6 +59,8 @@ class PiecewiseYieldCurveTest { static void testGlobalBootstrap(); + static void testIterativeBootstrapRetries(); + static boost::unit_test_framework::test_suite* suite(); }; From bbe53355158e3f717d10d846cd81f1bfe4e337bf Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 13 Apr 2020 16:49:31 +0100 Subject: [PATCH 03/12] Allow IterativeBootstrap to not throw and return a fall back curve. --- ql/termstructures/iterativebootstrap.hpp | 89 ++++++++++++++++++++---- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/ql/termstructures/iterativebootstrap.hpp b/ql/termstructures/iterativebootstrap.hpp index c0e0d936b33..0686e98e2e8 100644 --- a/ql/termstructures/iterativebootstrap.hpp +++ b/ql/termstructures/iterativebootstrap.hpp @@ -36,6 +36,43 @@ namespace QuantLib { +namespace detail { + + /*! If \c dontThrow is \c true in IterativeBootstrap and on a given pillar the bootstrap fails when + searching for a helper root between \c xMin and \c xMax, we use this function to return the value that + gives the minimum absolute helper error in the interval between \c xMin and \c xMax inclusive. + */ + template + Real dontThrowFallback(const BootstrapError& error, + Real xMin, Real xMax, Size steps) { + + QL_REQUIRE(xMin < xMax, "Expected xMin to be less than xMax"); + + // Set the initial value of the result to xMin and store the absolute bootstrap error at xMin + Real result = xMin; + Real absError = std::abs(error(xMin)); + Real minError = absError; + + // Step out to xMax + Real stepSize = (xMax - xMin) / steps; + for (Size i = 0; i < steps; i++) { + + // Get absolute bootstrap error at updated x value + xMin += stepSize; + absError = std::abs(error(xMin)); + + // If this absolute bootstrap error is less than the minimum, update result and minError + if (absError < minError) { + result = xMin; + minError = absError; + } + } + + return result; + } + +} + //! Universal piecewise-term-structure boostrapper. template class IterativeBootstrap { @@ -50,13 +87,19 @@ namespace QuantLib { \param maxAttempts Number of attempts on each iteration. A number greater than 1 implies retries. \param maxFactor Factor for max value retry on each iteration if there is a failure. \param minFactor Factor for min value retry on each iteration if there is a failure. + \param dontThrow If set to \c true, the bootstrap doesn't throw and returns a fall back + result. + \param dontThrowSteps If \p dontThrow is \c true, this gives the number of steps to use when searching + for a fallback curve pillar value that gives the minimum bootstrap helper error. */ IterativeBootstrap(Real accuracy = Null(), Real minValue = Null(), Real maxValue = Null(), Size maxAttempts = 1, Real maxFactor = 2.0, - Real minFactor = 2.0); + Real minFactor = 2.0, + bool dontThrow = false, + Size dontThrowSteps = 10); void setup(Curve* ts); void calculate() const; private: @@ -66,6 +109,8 @@ namespace QuantLib { Size maxAttempts_; Real maxFactor_; Real minFactor_; + bool dontThrow_; + Size dontThrowSteps_; Curve* ts_; Size n_; Brent firstSolver_; @@ -81,10 +126,10 @@ namespace QuantLib { template IterativeBootstrap::IterativeBootstrap(Real accuracy, Real minValue, Real maxValue, - Size maxAttempts, Real maxFactor, Real minFactor) + Size maxAttempts, Real maxFactor, Real minFactor, bool dontThrow, Size dontThrowSteps) : accuracy_(accuracy), minValue_(minValue), maxValue_(maxValue), - maxAttempts_(maxAttempts), maxFactor_(maxFactor), minFactor_(minFactor), - ts_(0), initialized_(false), validCurve_(false), + maxAttempts_(maxAttempts), maxFactor_(maxFactor), minFactor_(minFactor), dontThrow_(dontThrow), + dontThrowSteps_(dontThrowSteps), ts_(0), initialized_(false), validCurve_(false), loopRequired_(Interpolator::global) {} template @@ -283,12 +328,22 @@ namespace QuantLib { continue; } - QL_FAIL(io::ordinal(iteration + 1) << " iteration: failed " - "at " << io::ordinal(i) << " alive instrument, " - "pillar " << errors_[i]->helper()->pillarDate() << - ", maturity " << errors_[i]->helper()->maturityDate() << - ", reference date " << ts_->dates_[0] << - ": " << e.what()); + if (dontThrow_) { + // Use the fallback value + ts_->data_[i] = detail::dontThrowFallback(*errors_[i], minValues[i - 1], + maxValues[i - 1], dontThrowSteps_); + + // Remember to update the interpolation. If we don't and we are on the last "i", we will still + // have the last attempted value in the solver being used in ts_->interpolation_. + ts_->interpolation_.update(); + } else { + QL_FAIL(io::ordinal(iteration + 1) << " iteration: failed " + "at " << io::ordinal(i) << " alive instrument, " + "pillar " << errors_[i]->helper()->pillarDate() << + ", maturity " << errors_[i]->helper()->maturityDate() << + ", reference date " << ts_->dates_[0] << + ": " << e.what()); + } } } @@ -302,10 +357,16 @@ namespace QuantLib { if (change<=accuracy) // convergence reached break; - QL_REQUIRE(iteration Date: Mon, 13 Apr 2020 16:51:00 +0100 Subject: [PATCH 04/12] Unit test - fall back for distressed CDS curve that doesn't bootstrap. --- test-suite/defaultprobabilitycurves.cpp | 166 ++++++++++++++++++++++++ test-suite/defaultprobabilitycurves.hpp | 1 + 2 files changed, 167 insertions(+) diff --git a/test-suite/defaultprobabilitycurves.cpp b/test-suite/defaultprobabilitycurves.cpp index 25202c98823..41431fbfb0e 100644 --- a/test-suite/defaultprobabilitycurves.cpp +++ b/test-suite/defaultprobabilitycurves.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -31,13 +32,23 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include +#include using namespace QuantLib; using namespace boost::unit_test_framework; +using boost::assign::list_of; +using boost::assign::map_list_of; +using std::map; +using std::vector; +using std::string; void DefaultProbabilityCurveTest::testDefaultProbability() { @@ -320,6 +331,21 @@ namespace { } } + // Used to check that the exception message contains the expected message string, expMsg. + struct ExpErrorPred { + + ExpErrorPred(const string& msg) : expMsg(msg) {} + + bool operator()(const Error& ex) { + string errMsg(ex.what()); + BOOST_TEST_MESSAGE("Error expected to contain: '" << expMsg << "'."); + BOOST_TEST_MESSAGE("Actual error is: '" << errMsg << "'."); + return errMsg.find(expMsg) != string::npos; + } + + string expMsg; + }; + } void DefaultProbabilityCurveTest::testFlatHazardConsistency() { @@ -397,6 +423,144 @@ void DefaultProbabilityCurveTest::testUpfrontBootstrap() { BOOST_ERROR("Cash-flow settings improperly modified"); } +/* This test attempts to build a default curve from CDS spreads as of 1 Apr 2020. The spreads are real and from a + distressed reference entity with an inverted CDS spread curve. Using the default IterativeBootstrap with no + retries, the default curve building fails. Allowing retries, it expands the min survival probability bounds but + still fails. We set dontThrow to true in IterativeBootstrap to use a fall back curve. +*/ +void DefaultProbabilityCurveTest::testIterativeBootstrapRetries() { + + BOOST_TEST_MESSAGE("Testing iterative boostrap with retries..."); + + SavedSettings backup; + + Date asof(1, Apr, 2020); + Settings::instance().evaluationDate() = asof; + Actual365Fixed tsDayCounter; + + // USD discount curve built out of FedFunds OIS swaps. + vector usdCurveDates = list_of + (Date(1, Apr, 2020)) + (Date(2, Apr, 2020)) + (Date(14, Apr, 2020)) + (Date(21, Apr, 2020)) + (Date(28, Apr, 2020)) + (Date(6, May, 2020)) + (Date(5, Jun, 2020)) + (Date(7, Jul, 2020)) + (Date(5, Aug, 2020)) + (Date(8, Sep, 2020)) + (Date(7, Oct, 2020)) + (Date(5, Nov, 2020)) + (Date(7, Dec, 2020)) + (Date(6, Jan, 2021)) + (Date(5, Feb, 2021)) + (Date(5, Mar, 2021)) + (Date(7, Apr, 2021)) + (Date(4, Apr, 2022)) + (Date(3, Apr, 2023)) + (Date(3, Apr, 2024)) + (Date(3, Apr, 2025)) + (Date(5, Apr, 2027)) + (Date(3, Apr, 2030)) + (Date(3, Apr, 2035)) + (Date(3, Apr, 2040)) + (Date(4, Apr, 2050)); + + vector usdCurveDfs = list_of + (1.000000000) + (0.999955835) + (0.999931070) + (0.999914629) + (0.999902799) + (0.999887990) + (0.999825782) + (0.999764392) + (0.999709076) + (0.999647785) + (0.999594638) + (0.999536198) + (0.999483093) + (0.999419291) + (0.999379417) + (0.999324981) + (0.999262356) + (0.999575101) + (0.996135441) + (0.995228348) + (0.989366687) + (0.979271200) + (0.961150726) + (0.926265361) + (0.891640651) + (0.839314063); + + Handle usdYts(ext::make_shared>( + usdCurveDates, usdCurveDfs, tsDayCounter)); + + // CDS spreads + map cdsSpreads = map_list_of + (6 * Months, 2.957980250) + (1 * Years, 3.076933100) + (2 * Years, 2.944524520) + (3 * Years, 2.844498960) + (4 * Years, 2.769234420) + (5 * Years, 2.713474100); + Real recoveryRate = 0.035; + + // Conventions + Integer settlementDays = 1; + WeekendsOnly calendar; + Frequency frequency = Quarterly; + BusinessDayConvention paymentConvention = Following; + DateGeneration::Rule rule = DateGeneration::CDS2015; + Actual360 dayCounter; + Actual360 lastPeriodDayCounter(true); + + // Create the CDS spread helpers. + vector> instruments; + for (map::const_iterator it = cdsSpreads.begin(); it != cdsSpreads.end(); it++) { + instruments.push_back(ext::make_shared(it->second, it->first, settlementDays, calendar, + frequency, paymentConvention, rule, dayCounter, recoveryRate, usdYts, true, true, Date(), + lastPeriodDayCounter)); + } + + // Create the default curve with the default IterativeBootstrap. + typedef PiecewiseDefaultCurve SPCurve; + ext::shared_ptr dpts = ext::make_shared(asof, instruments, tsDayCounter); + + // Check that the default curve throws by requesting a default probability. + Date testDate(21, Dec, 2020); + BOOST_CHECK_EXCEPTION(dpts->survivalProbability(testDate), Error, + ExpErrorPred("1st iteration: failed at 1st alive instrument")); + + // Create the default curve with an IterativeBootstrap allowing for 4 retries. + // Use a maxFactor value of 1.0 so that we still use the previous survival probability at each pillar. In other + // words, the survival probability cannot increase with time so best max at current pillar is the previous + // pillar's value - there is no point increasing it on a retry. + IterativeBootstrap ib(Null(), Null(), Null(), 5, 1.0, 10.0); + dpts = ext::make_shared(asof, instruments, tsDayCounter, ib); + + // Check that the default curve still throws. It throws at the third pillar because the survival probability is + // too low at the second pillar. + BOOST_CHECK_EXCEPTION(dpts->survivalProbability(testDate), Error, + ExpErrorPred("1st iteration: failed at 3rd alive instrument")); + + // Create the default curve with an IterativeBootstrap that allows for 4 retries and does not throw. + IterativeBootstrap ibNoThrow(Null(), Null(), Null(), 5, 1.0, 10.0, true, 3); + dpts = ext::make_shared(asof, instruments, tsDayCounter, ibNoThrow); + BOOST_CHECK_NO_THROW(dpts->survivalProbability(testDate)); + + for (const auto inst : instruments) { + Date latestDate = inst->latestDate(); + Date pillarDate = inst->pillarDate(); + Date latestRelevantDate = inst->latestRelevantDate(); + Real sp = dpts->survivalProbability(pillarDate); + BOOST_TEST_MESSAGE(io::iso_date(latestDate) << "," << io::iso_date(pillarDate) << "," << + io::iso_date(latestRelevantDate) << "," << std::fixed << std::setprecision(12) << sp); + } +} + test_suite* DefaultProbabilityCurveTest::suite() { test_suite* suite = BOOST_TEST_SUITE("Default-probability curve tests"); @@ -416,5 +580,7 @@ test_suite* DefaultProbabilityCurveTest::suite() { &DefaultProbabilityCurveTest::testSingleInstrumentBootstrap)); suite->add(QUANTLIB_TEST_CASE( &DefaultProbabilityCurveTest::testUpfrontBootstrap)); + suite->add(QUANTLIB_TEST_CASE( + &DefaultProbabilityCurveTest::testIterativeBootstrapRetries)); return suite; } diff --git a/test-suite/defaultprobabilitycurves.hpp b/test-suite/defaultprobabilitycurves.hpp index 35965223fb3..d2b3e41935f 100644 --- a/test-suite/defaultprobabilitycurves.hpp +++ b/test-suite/defaultprobabilitycurves.hpp @@ -36,6 +36,7 @@ class DefaultProbabilityCurveTest { static void testLogLinearSurvivalConsistency(); static void testSingleInstrumentBootstrap(); static void testUpfrontBootstrap(); + static void testIterativeBootstrapRetries(); static boost::unit_test_framework::test_suite* suite(); }; From 067c3ab2b4a89824ea264f3d42ef2a4fe7545e2c Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 13 Apr 2020 17:00:11 +0100 Subject: [PATCH 05/12] Remove the extra message logs. --- test-suite/defaultprobabilitycurves.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test-suite/defaultprobabilitycurves.cpp b/test-suite/defaultprobabilitycurves.cpp index 41431fbfb0e..016304b5b60 100644 --- a/test-suite/defaultprobabilitycurves.cpp +++ b/test-suite/defaultprobabilitycurves.cpp @@ -550,15 +550,6 @@ void DefaultProbabilityCurveTest::testIterativeBootstrapRetries() { IterativeBootstrap ibNoThrow(Null(), Null(), Null(), 5, 1.0, 10.0, true, 3); dpts = ext::make_shared(asof, instruments, tsDayCounter, ibNoThrow); BOOST_CHECK_NO_THROW(dpts->survivalProbability(testDate)); - - for (const auto inst : instruments) { - Date latestDate = inst->latestDate(); - Date pillarDate = inst->pillarDate(); - Date latestRelevantDate = inst->latestRelevantDate(); - Real sp = dpts->survivalProbability(pillarDate); - BOOST_TEST_MESSAGE(io::iso_date(latestDate) << "," << io::iso_date(pillarDate) << "," << - io::iso_date(latestRelevantDate) << "," << std::fixed << std::setprecision(12) << sp); - } } From e635021b2cccf19aa6dd1c13562b3f24b8af9c38 Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 13 Apr 2020 20:13:08 +0100 Subject: [PATCH 06/12] Add a check that the factors are at least 1.0. --- ql/termstructures/iterativebootstrap.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ql/termstructures/iterativebootstrap.hpp b/ql/termstructures/iterativebootstrap.hpp index 0686e98e2e8..1eaa66f59f5 100644 --- a/ql/termstructures/iterativebootstrap.hpp +++ b/ql/termstructures/iterativebootstrap.hpp @@ -130,7 +130,10 @@ namespace detail { : accuracy_(accuracy), minValue_(minValue), maxValue_(maxValue), maxAttempts_(maxAttempts), maxFactor_(maxFactor), minFactor_(minFactor), dontThrow_(dontThrow), dontThrowSteps_(dontThrowSteps), ts_(0), initialized_(false), validCurve_(false), - loopRequired_(Interpolator::global) {} + loopRequired_(Interpolator::global) { + QL_REQUIRE(maxFactor_ >= 1.0, "Expected that maxFactor would be at least 1.0 but got " << maxFactor_); + QL_REQUIRE(minFactor_ >= 1.0, "Expected that minFactor would be at least 1.0 but got " << minFactor_); + } template void IterativeBootstrap::setup(Curve* ts) { From d11a6502be6a166470e2536045789583e8cf77ec Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 13 Apr 2020 20:28:17 +0100 Subject: [PATCH 07/12] Only need two steps here. --- test-suite/defaultprobabilitycurves.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite/defaultprobabilitycurves.cpp b/test-suite/defaultprobabilitycurves.cpp index 016304b5b60..388a48495a5 100644 --- a/test-suite/defaultprobabilitycurves.cpp +++ b/test-suite/defaultprobabilitycurves.cpp @@ -547,7 +547,7 @@ void DefaultProbabilityCurveTest::testIterativeBootstrapRetries() { ExpErrorPred("1st iteration: failed at 3rd alive instrument")); // Create the default curve with an IterativeBootstrap that allows for 4 retries and does not throw. - IterativeBootstrap ibNoThrow(Null(), Null(), Null(), 5, 1.0, 10.0, true, 3); + IterativeBootstrap ibNoThrow(Null(), Null(), Null(), 5, 1.0, 10.0, true, 2); dpts = ext::make_shared(asof, instruments, tsDayCounter, ibNoThrow); BOOST_CHECK_NO_THROW(dpts->survivalProbability(testDate)); } From 28b12f3de076b12fb0f568f9f8f06beaceefb7fd Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Mon, 27 Apr 2020 11:00:54 +0200 Subject: [PATCH 08/12] Fix some build errors on Travis. --- test-suite/defaultprobabilitycurves.cpp | 15 ++++++++------- test-suite/piecewiseyieldcurve.cpp | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/test-suite/defaultprobabilitycurves.cpp b/test-suite/defaultprobabilitycurves.cpp index 388a48495a5..d5d9736072d 100644 --- a/test-suite/defaultprobabilitycurves.cpp +++ b/test-suite/defaultprobabilitycurves.cpp @@ -334,7 +334,7 @@ namespace { // Used to check that the exception message contains the expected message string, expMsg. struct ExpErrorPred { - ExpErrorPred(const string& msg) : expMsg(msg) {} + explicit ExpErrorPred(const string& msg) : expMsg(msg) {} bool operator()(const Error& ex) { string errMsg(ex.what()); @@ -495,7 +495,7 @@ void DefaultProbabilityCurveTest::testIterativeBootstrapRetries() { (0.891640651) (0.839314063); - Handle usdYts(ext::make_shared>( + Handle usdYts(ext::make_shared >( usdCurveDates, usdCurveDfs, tsDayCounter)); // CDS spreads @@ -518,11 +518,12 @@ void DefaultProbabilityCurveTest::testIterativeBootstrapRetries() { Actual360 lastPeriodDayCounter(true); // Create the CDS spread helpers. - vector> instruments; - for (map::const_iterator it = cdsSpreads.begin(); it != cdsSpreads.end(); it++) { - instruments.push_back(ext::make_shared(it->second, it->first, settlementDays, calendar, - frequency, paymentConvention, rule, dayCounter, recoveryRate, usdYts, true, true, Date(), - lastPeriodDayCounter)); + vector > instruments; + for (map::const_iterator it = cdsSpreads.begin(); it != cdsSpreads.end(); ++it) { + instruments.push_back(ext::shared_ptr( + new SpreadCdsHelper(it->second, it->first, settlementDays, calendar, + frequency, paymentConvention, rule, dayCounter, recoveryRate, usdYts, true, true, Date(), + lastPeriodDayCounter))); } // Create the default curve with the default IterativeBootstrap. diff --git a/test-suite/piecewiseyieldcurve.cpp b/test-suite/piecewiseyieldcurve.cpp index ffc0d1e262e..49305f0e8ed 100644 --- a/test-suite/piecewiseyieldcurve.cpp +++ b/test-suite/piecewiseyieldcurve.cpp @@ -642,7 +642,7 @@ namespace piecewise_yield_curve_test { // Used to check that the exception message contains the expected message string, expMsg. struct ExpErrorPred { - ExpErrorPred(const string& msg) : expMsg(msg) {} + explicit ExpErrorPred(const string& msg) : expMsg(msg) {} bool operator()(const Error& ex) { string errMsg(ex.what()); @@ -1441,7 +1441,7 @@ void PiecewiseYieldCurveTest::testIterativeBootstrapRetries() { (0.985495036) (0.984413446); - Handle usdYts(ext::make_shared>( + Handle usdYts(ext::make_shared >( usdCurveDates, usdCurveDfs, tsDayCounter)); // USD/ARS forward points @@ -1455,8 +1455,8 @@ void PiecewiseYieldCurveTest::testIterativeBootstrapRetries() { (1 * Years, 60.7370); // Create the FX swap rate helpers for the ARS in USD curve. - vector> instruments; - for (map::const_iterator it = arsFwdPoints.begin(); it != arsFwdPoints.end(); it++) { + vector > instruments; + for (map::const_iterator it = arsFwdPoints.begin(); it != arsFwdPoints.end(); ++it) { Handle arsFwd(ext::make_shared(it->second)); instruments.push_back(ext::make_shared(arsFwd, arsSpot, it->first, 2, UnitedStates(), Following, false, true, usdYts)); From c629f98598c93b27ab8dc8a8bac5a6e47cd0495d Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Mon, 27 Apr 2020 18:53:40 +0200 Subject: [PATCH 09/12] Avoid uninitialized-variable warning. --- test-suite/piecewiseyieldcurve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite/piecewiseyieldcurve.cpp b/test-suite/piecewiseyieldcurve.cpp index 49305f0e8ed..97b3b2398f3 100644 --- a/test-suite/piecewiseyieldcurve.cpp +++ b/test-suite/piecewiseyieldcurve.cpp @@ -1479,7 +1479,7 @@ void PiecewiseYieldCurveTest::testIterativeBootstrapRetries() { arsYts = ext::make_shared(asof, instruments, tsDayCounter, ib); // Check that the ARS in USD curve builds and populate the spot ARS discount factor. - DiscountFactor spotDfArs; + DiscountFactor spotDfArs = 1.0; BOOST_REQUIRE_NO_THROW(spotDfArs = arsYts->discount(spotDate)); // Additional dates and discount factors used in the final check i.e. that calculated 1Y FX forward equals input. From 0049c8db28922c5d1437b41f86df71b95dabf2fb Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Mon, 27 Apr 2020 18:59:13 +0200 Subject: [PATCH 10/12] Avoid message when test is successful. --- test-suite/defaultprobabilitycurves.cpp | 10 +++++++--- test-suite/piecewiseyieldcurve.cpp | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/test-suite/defaultprobabilitycurves.cpp b/test-suite/defaultprobabilitycurves.cpp index d5d9736072d..6dd2204637a 100644 --- a/test-suite/defaultprobabilitycurves.cpp +++ b/test-suite/defaultprobabilitycurves.cpp @@ -338,9 +338,13 @@ namespace { bool operator()(const Error& ex) { string errMsg(ex.what()); - BOOST_TEST_MESSAGE("Error expected to contain: '" << expMsg << "'."); - BOOST_TEST_MESSAGE("Actual error is: '" << errMsg << "'."); - return errMsg.find(expMsg) != string::npos; + if (errMsg.find(expMsg) == string::npos) { + BOOST_TEST_MESSAGE("Error expected to contain: '" << expMsg << "'."); + BOOST_TEST_MESSAGE("Actual error is: '" << errMsg << "'."); + return false; + } else { + return true; + } } string expMsg; diff --git a/test-suite/piecewiseyieldcurve.cpp b/test-suite/piecewiseyieldcurve.cpp index 97b3b2398f3..75915e69cf7 100644 --- a/test-suite/piecewiseyieldcurve.cpp +++ b/test-suite/piecewiseyieldcurve.cpp @@ -646,9 +646,13 @@ namespace piecewise_yield_curve_test { bool operator()(const Error& ex) { string errMsg(ex.what()); - BOOST_TEST_MESSAGE("Error expected to contain: '" << expMsg << "'."); - BOOST_TEST_MESSAGE("Actual error is: '" << errMsg << "'."); - return errMsg.find(expMsg) != string::npos; + if (errMsg.find(expMsg) == string::npos) { + BOOST_TEST_MESSAGE("Error expected to contain: '" << expMsg << "'."); + BOOST_TEST_MESSAGE("Actual error is: '" << errMsg << "'."); + return false; + } else { + return true; + } } string expMsg; From d61ad011276253f4a9265d71dea11f24ea8a5c1c Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Tue, 28 Apr 2020 16:03:09 +0200 Subject: [PATCH 11/12] Try to improve readability. --- ql/termstructures/iterativebootstrap.hpp | 59 +++++++++++++----------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/ql/termstructures/iterativebootstrap.hpp b/ql/termstructures/iterativebootstrap.hpp index 1eaa66f59f5..a56d02354c4 100644 --- a/ql/termstructures/iterativebootstrap.hpp +++ b/ql/termstructures/iterativebootstrap.hpp @@ -39,7 +39,7 @@ namespace QuantLib { namespace detail { /*! If \c dontThrow is \c true in IterativeBootstrap and on a given pillar the bootstrap fails when - searching for a helper root between \c xMin and \c xMax, we use this function to return the value that + searching for a helper root between \c xMin and \c xMax, we use this function to return the value that gives the minimum absolute helper error in the interval between \c xMin and \c xMax inclusive. */ template @@ -258,34 +258,38 @@ namespace detail { previousData_ = ts_->data_; // Store min value and max value at each pillar so that we can expand search if necessary. - std::vector minValues(alive_, Null()); - std::vector maxValues(alive_, Null()); - std::vector attempts(alive_, 1); + std::vector minValues(alive_+1, Null()); + std::vector maxValues(alive_+1, Null()); + std::vector attempts(alive_+1, 1); for (Size i=1; i<=alive_; ++i) { // pillar loop + // shorter aliases for readability and to avoid duplication + Real& min = minValues[i]; + Real& max = maxValues[i]; + // bracket root and calculate guess - if (minValues[i - 1] == Null()) { - minValues[i - 1] = minValue_ != Null() ? minValue_ : - Traits::minValueAfter(i, ts_, validData, firstAliveHelper_); - } else { - minValues[i - 1] = minValues[i - 1] < 0.0 ? - minFactor_ * minValues[i - 1] : minValues[i - 1] / minFactor_; - } - if (maxValues[i - 1] == Null()) { - maxValues[i - 1] = maxValue_ != Null() ? maxValue_ : - Traits::maxValueAfter(i, ts_, validData, firstAliveHelper_); + if (min == Null()) { + // First attempt; we take min and max either from + // explicit constructor parameter or from traits + min = (minValue_ != Null() ? minValue_ : + Traits::minValueAfter(i, ts_, validData, firstAliveHelper_)); + max = (maxValue_ != Null() ? maxValue_ : + Traits::maxValueAfter(i, ts_, validData, firstAliveHelper_)); } else { - maxValues[i - 1] = maxValues[i - 1] > 0.0 ? - maxFactor_ * maxValues[i - 1] : maxValues[i - 1] / maxFactor_; + // Extending a previous attempt. A negative min + // is enlarged; a positive one is shrunk towards 0. + min = (min < 0.0 ? min * minFactor_ : min / minFactor_); + // The opposite holds for the max. + max = (max > 0.0 ? max * maxFactor_ : max / maxFactor_); } Real guess = Traits::guess(i, ts_, validData, firstAliveHelper_); // adjust guess if needed - if (guess >= maxValues[i - 1]) - guess = maxValues[i - 1] - (maxValues[i - 1] - minValues[i - 1]) / 5.0; - else if (guess <= minValues[i - 1]) - guess = minValues[i - 1] + (maxValues[i - 1] - minValues[i - 1]) / 5.0; + if (guess >= max) + guess = max - (max - min) / 5.0; + else if (guess <= min) + guess = min + (max - min) / 5.0; // extend interpolation if needed if (!validData) { @@ -307,9 +311,9 @@ namespace detail { try { if (validData) - solver_.solve(*errors_[i], accuracy, guess, minValues[i - 1], maxValues[i - 1]); + solver_.solve(*errors_[i], accuracy, guess, min, max); else - firstSolver_.solve(*errors_[i], accuracy, guess, minValues[i - 1], maxValues[i - 1]); + firstSolver_.solve(*errors_[i], accuracy, guess, min, max); } catch (std::exception &e) { if (validCurve_) { // the previous curve state might have been a @@ -323,20 +327,19 @@ namespace detail { return; } - // If we have more attempts left on this iteration, try again. Note that the max and min + // If we have more attempts left on this iteration, try again. Note that the max and min // bounds will be widened on the retry. - if (attempts[i - 1] < maxAttempts_) { - attempts[i - 1]++; + if (attempts[i] < maxAttempts_) { + attempts[i]++; i--; continue; } if (dontThrow_) { // Use the fallback value - ts_->data_[i] = detail::dontThrowFallback(*errors_[i], minValues[i - 1], - maxValues[i - 1], dontThrowSteps_); + ts_->data_[i] = detail::dontThrowFallback(*errors_[i], min, max, dontThrowSteps_); - // Remember to update the interpolation. If we don't and we are on the last "i", we will still + // Remember to update the interpolation. If we don't and we are on the last "i", we will still // have the last attempted value in the solver being used in ts_->interpolation_. ts_->interpolation_.update(); } else { From 4b1f17b33c63034929fec55d378789b6043ef927 Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Thu, 30 Apr 2020 15:54:18 +0200 Subject: [PATCH 12/12] Re-trigger CI