From 86f28e55ac999cad5f7a6bade29d227d29bb0cbb Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Wed, 13 Nov 2024 10:37:08 +0100 Subject: [PATCH] Don't require helpers when passing fixed parameters to fitted curve --- .../yield/fittedbonddiscountcurve.cpp | 132 ++++++++++++------ .../yield/fittedbonddiscountcurve.hpp | 16 +++ test-suite/fittedbonddiscountcurve.cpp | 50 ++++--- 3 files changed, 134 insertions(+), 64 deletions(-) diff --git a/ql/termstructures/yield/fittedbonddiscountcurve.cpp b/ql/termstructures/yield/fittedbonddiscountcurve.cpp index 2d0aa4b279f..45dd13a1d6f 100644 --- a/ql/termstructures/yield/fittedbonddiscountcurve.cpp +++ b/ql/termstructures/yield/fittedbonddiscountcurve.cpp @@ -85,6 +85,35 @@ namespace QuantLib { setup(); } + FittedBondDiscountCurve::FittedBondDiscountCurve( + Natural settlementDays, + const Calendar& calendar, + const FittingMethod& fittingMethod, + Array parameters, + Date maxDate, + const DayCounter& dayCounter) + : YieldTermStructure(settlementDays, calendar, dayCounter), accuracy_(1e-10), + maxEvaluations_(0), guessSolution_(std::move(parameters)), + maxDate_(maxDate), fittingMethod_(fittingMethod) { + + fittingMethod_->curve_ = this; + setup(); + } + + FittedBondDiscountCurve::FittedBondDiscountCurve( + const Date& referenceDate, + const FittingMethod& fittingMethod, + Array parameters, + Date maxDate, + const DayCounter& dayCounter) + : YieldTermStructure(referenceDate, Calendar(), dayCounter), accuracy_(1e-10), + maxEvaluations_(0), guessSolution_(std::move(parameters)), + maxDate_(maxDate), fittingMethod_(fittingMethod) { + + fittingMethod_->curve_ = this; + setup(); + } + void FittedBondDiscountCurve::resetGuess(const Array& guess) { QL_REQUIRE(guess.empty() || guess.size() == fittingMethod_->size(), "guess is of wrong size"); @@ -92,33 +121,46 @@ namespace QuantLib { update(); } - + void FittedBondDiscountCurve::performCalculations() const { - QL_REQUIRE(!bondHelpers_.empty(), "no bondHelpers given"); - - maxDate_ = Date::minDate(); - Date refDate = referenceDate(); - - // double check bond quotes still valid and/or instruments not expired - for (Size i=0; i bond = bondHelpers_[i]->bond(); - QL_REQUIRE(bondHelpers_[i]->quote()->isValid(), - io::ordinal(i+1) << " bond (maturity: " << - bond->maturityDate() << ") has an invalid price quote"); - Date bondSettlement = bond->settlementDate(); - QL_REQUIRE(bondSettlement>=refDate, - io::ordinal(i+1) << " bond settlemente date (" << - bondSettlement << ") before curve reference date (" << - refDate << ")"); - QL_REQUIRE(BondFunctions::isTradable(*bond, bondSettlement), - io::ordinal(i+1) << " bond non tradable at " << - bondSettlement << " settlement date (maturity" - " being " << bond->maturityDate() << ")"); - maxDate_ = std::max(maxDate_, bondHelpers_[i]->pillarDate()); - bondHelpers_[i]->setTermStructure( - const_cast(this)); + if (maxEvaluations_!= 0) { + // we need to fit, so we require helpers + QL_REQUIRE(!bondHelpers_.empty(), "no bond helpers given"); + } + + if (maxEvaluations_ == 0) { + // no fit, but we need either an explicit max date or + // helpers from which to deduce it + QL_REQUIRE(maxDate_ != Date() || !bondHelpers_.empty(), + "no bond helpers or max date given"); + } + + if (!bondHelpers_.empty()) { + maxDate_ = Date::minDate(); + Date refDate = referenceDate(); + + // double check bond quotes still valid and/or instruments not expired + for (Size i=0; i bond = bondHelpers_[i]->bond(); + QL_REQUIRE(bondHelpers_[i]->quote()->isValid(), + io::ordinal(i+1) << " bond (maturity: " << + bond->maturityDate() << ") has an invalid price quote"); + Date bondSettlement = bond->settlementDate(); + QL_REQUIRE(bondSettlement>=refDate, + io::ordinal(i+1) << " bond settlemente date (" << + bondSettlement << ") before curve reference date (" << + refDate << ")"); + QL_REQUIRE(BondFunctions::isTradable(*bond, bondSettlement), + io::ordinal(i+1) << " bond non tradable at " << + bondSettlement << " settlement date (maturity" + " being " << bond->maturityDate() << ")"); + maxDate_ = std::max(maxDate_, bondHelpers_[i]->pillarDate()); + bondHelpers_[i]->setTermStructure( + const_cast(this)); + } } + fittingMethod_->init(); fittingMethod_->calculate(); } @@ -141,6 +183,10 @@ namespace QuantLib { } void FittedBondDiscountCurve::FittingMethod::init() { + + if (curve_->maxEvaluations_ == 0) + return; // we can skip the rest + // yield conventions DayCounter yieldDC = curve_->dayCounter(); Compounding yieldComp = Compounded; @@ -192,37 +238,39 @@ namespace QuantLib { void FittedBondDiscountCurve::FittingMethod::calculate() { - FittingCost& costFunction = *costFunction_; - - // 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_; - } - if (curve_->maxEvaluations_ == 0) { - // Don't calculate, simply use the given parameters to provide a fitted curve. - // This turns the fittedbonddiscountcurve into an evaluator of the parametric - // curve, for example allowing to use the parameters for a credit spread curve - // calculated with bonds in one currency to be coupled to a discount curve in - // another currency. + // Don't calculate, simply use the given parameters to + // provide a fitted curve. This turns the instance into + // an evaluator of the parametric curve, for example + // allowing to use the parameters for a credit spread + // curve calculated with bonds in one currency to be + // coupled to a discount curve in another currency. - QL_REQUIRE(!curve_->guessSolution_.empty(), "no guess provided"); + QL_REQUIRE(curve_->guessSolution_.size() == size(), + "wrong number of parameters"); solution_ = curve_->guessSolution_; numberOfIterations_ = 0; - costValue_ = costFunction.value(solution_); + costValue_ = Null(); errorCode_ = EndCriteria::None; return; } - //workaround for backwards compatibility + FittingCost& costFunction = *costFunction_; + + // 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_; + } + + // workaround for backwards compatibility ext::shared_ptr optimization = optimizationMethod_; - if(!optimization){ + if (!optimization) { optimization = ext::make_shared(curve_->simplexLambda_); } Problem problem(costFunction, constraint_, x); diff --git a/ql/termstructures/yield/fittedbonddiscountcurve.hpp b/ql/termstructures/yield/fittedbonddiscountcurve.hpp index 6db9eba8d43..5880bd0a8b5 100644 --- a/ql/termstructures/yield/fittedbonddiscountcurve.hpp +++ b/ql/termstructures/yield/fittedbonddiscountcurve.hpp @@ -96,6 +96,7 @@ namespace QuantLib { Array guess = Array(), Real simplexLambda = 1.0, Size maxStationaryStateIterations = 100); + //! curve reference date fixed for life of curve FittedBondDiscountCurve(const Date& referenceDate, std::vector > bonds, @@ -106,6 +107,21 @@ namespace QuantLib { Array guess = Array(), Real simplexLambda = 1.0, Size maxStationaryStateIterations = 100); + + //! don't fit, use precalculated parameters + FittedBondDiscountCurve(Natural settlementDays, + const Calendar& calendar, + const FittingMethod& fittingMethod, + Array parameters, + Date maxDate, + const DayCounter& dayCounter); + + //! don't fit, use precalculated parameters + FittedBondDiscountCurve(const Date& referenceDate, + const FittingMethod& fittingMethod, + Array parameters, + Date maxDate, + const DayCounter& dayCounter); //@} //! \name Inspectors diff --git a/test-suite/fittedbonddiscountcurve.cpp b/test-suite/fittedbonddiscountcurve.cpp index fd89877bb96..384cc7ce10b 100644 --- a/test-suite/fittedbonddiscountcurve.cpp +++ b/test-suite/fittedbonddiscountcurve.cpp @@ -42,31 +42,37 @@ BOOST_AUTO_TEST_CASE(testEvaluation) { BOOST_TEST_MESSAGE("Testing that fitted bond curves work as evaluators..."); Date today = Settings::instance().evaluationDate(); - ext::shared_ptr bond = ext::make_shared(3, TARGET(), 100.0, - today + Period(10, Years)); - Handle q(ext::make_shared(100.0)); - - std::vector > helpers(1); - helpers[0] = ext::make_shared(q, bond); + Date maxDate = today + Period(10, Years); ExponentialSplinesFitting fittingMethod; - Size maxIterations = 0; - Array guess(9); - guess[0] = -51293.44; - guess[1] = -212240.36; - guess[2] = 168668.51; - guess[3] = 88792.74; - guess[4] = 120712.13; - guess[5] = -34332.83; - guess[6] = -66479.66; - guess[7] = 13605.17; - guess[8] = 0.0; + Array parameters = { + -51293.44, + -212240.36, + 168668.51, + 88792.74, + 120712.13, + -34332.83, + -66479.66, + 13605.17, + 0.0 + }; - FittedBondDiscountCurve curve(0, TARGET(), helpers, Actual365Fixed(), - fittingMethod, 1e-10, maxIterations, guess); + FittedBondDiscountCurve curve1( + today, fittingMethod, parameters, maxDate, Actual365Fixed()); - BOOST_CHECK_NO_THROW(curve.discount(3.0)); + FittedBondDiscountCurve curve2( + 0, TARGET(), fittingMethod, parameters, maxDate, Actual365Fixed()); + + // they work... + BOOST_CHECK_NO_THROW(curve1.discount(3.0)); + BOOST_CHECK_NO_THROW(curve2.discount(3.0)); + + // ...but not after the max date + BOOST_CHECK_EXCEPTION(curve1.discount(12.0), Error, + ExpectedErrorMessage("past max curve time")); + BOOST_CHECK_EXCEPTION(curve2.discount(12.0), Error, + ExpectedErrorMessage("past max curve time")); } BOOST_AUTO_TEST_CASE(testFlatExtrapolation) { @@ -315,9 +321,9 @@ BOOST_AUTO_TEST_CASE(testConstraint) { Real accuracy = 1e-10; // default value Size maxIterations = 10000; // default value Array guess = {0.01}; // something positive so that initial value is in feasible region - + FlatZero unconstrainedMethod; - FittedBondDiscountCurve unconstrainedCurve(0, TARGET(), helpers, Actual365Fixed(), unconstrainedMethod, + FittedBondDiscountCurve unconstrainedCurve(0, TARGET(), helpers, Actual365Fixed(), unconstrainedMethod, accuracy, maxIterations, guess); BOOST_CHECK_LT(unconstrainedCurve.fitResults().solution()[0], 0.0);