From 5a22e175be20e29c3a35718fa4ba884a45490d23 Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Thu, 20 Jun 2024 12:07:15 +0200 Subject: [PATCH 1/2] Add possibility to reset guess in fitted bond curves --- .../yield/fittedbonddiscountcurve.cpp | 7 +++++++ .../yield/fittedbonddiscountcurve.hpp | 6 ++++++ test-suite/fittedbonddiscountcurve.cpp | 21 +++++++++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/ql/termstructures/yield/fittedbonddiscountcurve.cpp b/ql/termstructures/yield/fittedbonddiscountcurve.cpp index 0ce3ebbac3e..591902f3802 100644 --- a/ql/termstructures/yield/fittedbonddiscountcurve.cpp +++ b/ql/termstructures/yield/fittedbonddiscountcurve.cpp @@ -86,6 +86,13 @@ namespace QuantLib { } + void FittedBondDiscountCurve::resetGuess(const Array& guess) { + QL_REQUIRE(guess.empty() || guess.size() == fittingMethod_->size(), "guess is of wrong size"); + guessSolution_ = guess; + update(); + } + + void FittedBondDiscountCurve::performCalculations() const { QL_REQUIRE(!bondHelpers_.empty(), "no bondHelpers given"); diff --git a/ql/termstructures/yield/fittedbonddiscountcurve.hpp b/ql/termstructures/yield/fittedbonddiscountcurve.hpp index eb06165819e..dd601d0868a 100644 --- a/ql/termstructures/yield/fittedbonddiscountcurve.hpp +++ b/ql/termstructures/yield/fittedbonddiscountcurve.hpp @@ -117,6 +117,12 @@ namespace QuantLib { const FittingMethod& fitResults() const; //@} + //! \name Other utilities + //@{ + /*! This allows to try out multiple guesses and avoid local minima */ + void resetGuess(const Array& guess); + //@} + //! \name Observer interface //@{ void update() override; diff --git a/test-suite/fittedbonddiscountcurve.cpp b/test-suite/fittedbonddiscountcurve.cpp index 0770be201e5..fbe41482bae 100644 --- a/test-suite/fittedbonddiscountcurve.cpp +++ b/test-suite/fittedbonddiscountcurve.cpp @@ -189,12 +189,29 @@ BOOST_AUTO_TEST_CASE(testFlatExtrapolation) { // Real modelYield1 = bonds[i]->yield(modelPrices1[i], Actual365Fixed(), Continuous, NoFrequency); Real modelYield2 = bonds[i]->yield(modelPrices2[i], Actual365Fixed(), Continuous, NoFrequency); - // Real curveYield1 = curve1->zeroRate(t, Continuous).rate(); + Real curveYield1 = curve1->zeroRate(t, Continuous).rate(); Real curveYield2 = curve2->zeroRate(t, Continuous).rate(); + if (curveYield1 < 1.0) { + BOOST_ERROR("Expecting huge yield; the test premise might be outdated"); + } QL_CHECK_CLOSE(modelYield2, curveYield2, 1.0); // 1.0 percent relative tolerance } - + + // resetting the guess changes the calibration + + curve1->resetGuess({ 0.02, 0.0, 0.0, 0.0 }); + + BOOST_CHECK_EQUAL(curve1->fitResults().errorCode(), EndCriteria::StationaryPoint); + + for (Size i = 0; i < helpers.size(); ++i) { + Real t = curve1->timeFromReference(helpers[i]->bond()->maturityDate()); + Real modelYield1 = bonds[i]->yield(modelPrices1[i], Actual365Fixed(), Continuous, NoFrequency); + Real curveYield1 = curve1->zeroRate(t, Continuous).rate(); + + QL_CHECK_CLOSE(modelYield1, curveYield1, 6); // somewhat better, within a dozen bps + } + } BOOST_AUTO_TEST_CASE(testRequiredGuess) { From c79c8e0b25f10ef0292e02cc16cb8d48a740ac30 Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Thu, 20 Jun 2024 12:51:32 +0200 Subject: [PATCH 2/2] Add check on guess size for fitted bond curves --- .../yield/fittedbonddiscountcurve.cpp | 1 + test-suite/fittedbonddiscountcurve.cpp | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/ql/termstructures/yield/fittedbonddiscountcurve.cpp b/ql/termstructures/yield/fittedbonddiscountcurve.cpp index 591902f3802..d7f466137a5 100644 --- a/ql/termstructures/yield/fittedbonddiscountcurve.cpp +++ b/ql/termstructures/yield/fittedbonddiscountcurve.cpp @@ -193,6 +193,7 @@ namespace QuantLib { // start with the guess solution, if it exists Array x(size(), 0.0); if (!curve_->guessSolution_.empty()) { + QL_REQUIRE(curve_->guessSolution_.size() == size(), "wrong size for guess"); x = curve_->guessSolution_; } diff --git a/test-suite/fittedbonddiscountcurve.cpp b/test-suite/fittedbonddiscountcurve.cpp index fbe41482bae..61a20a98cf8 100644 --- a/test-suite/fittedbonddiscountcurve.cpp +++ b/test-suite/fittedbonddiscountcurve.cpp @@ -244,6 +244,34 @@ BOOST_AUTO_TEST_CASE(testRequiredGuess) { ExpectedErrorMessage("L2 penalty requires a guess")); } +BOOST_AUTO_TEST_CASE(testGuessSize) { + + BOOST_TEST_MESSAGE("Testing that fitted bond curves check the guess size when given..."); + + Date today = Settings::instance().evaluationDate(); + auto bond1 = ext::make_shared(3, TARGET(), 100.0, today + Period(1, Years)); + auto bond2 = ext::make_shared(3, TARGET(), 100.0, today + Period(2, Years)); + auto bond3 = ext::make_shared(3, TARGET(), 100.0, today + Period(5, Years)); + auto bond4 = ext::make_shared(3, TARGET(), 100.0, today + Period(10, Years)); + + std::vector > helpers(4); + helpers[0] = ext::make_shared(makeQuoteHandle(99.0), bond1); + helpers[1] = ext::make_shared(makeQuoteHandle(98.0), bond2); + helpers[2] = ext::make_shared(makeQuoteHandle(95.0), bond3); + helpers[3] = ext::make_shared(makeQuoteHandle(90.0), bond4); + + NelsonSiegelFitting fittingMethod; + + Real accuracy = 1e-10; + Size maxIterations = 10000; + Array guess = { 0.01, 0.0, 0.0 }; // too few + FittedBondDiscountCurve curve(0, TARGET(), helpers, Actual365Fixed(), + fittingMethod, accuracy, maxIterations, guess); + + BOOST_CHECK_EXCEPTION(curve.discount(3.0), Error, + ExpectedErrorMessage("wrong size for guess")); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()