Skip to content

Commit

Permalink
Don't require helpers when passing fixed parameters to fitted bond cu…
Browse files Browse the repository at this point in the history
…rve (#2113)
  • Loading branch information
lballabio authored Nov 13, 2024
2 parents 06f6172 + 86f28e5 commit 1aae346
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 64 deletions.
132 changes: 90 additions & 42 deletions ql/termstructures/yield/fittedbonddiscountcurve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,40 +85,82 @@ 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");
guessSolution_ = guess;
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<bondHelpers_.size(); ++i) {
ext::shared_ptr<Bond> 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<FittedBondDiscountCurve*>(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<bondHelpers_.size(); ++i) {
ext::shared_ptr<Bond> 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<FittedBondDiscountCurve*>(this));
}
}

fittingMethod_->init();
fittingMethod_->calculate();
}
Expand All @@ -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;
Expand Down Expand Up @@ -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<Real>();
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<OptimizationMethod> optimization = optimizationMethod_;
if(!optimization){
if (!optimization) {
optimization = ext::make_shared<Simplex>(curve_->simplexLambda_);
}
Problem problem(costFunction, constraint_, x);
Expand Down
16 changes: 16 additions & 0 deletions ql/termstructures/yield/fittedbonddiscountcurve.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ext::shared_ptr<BondHelper> > bonds,
Expand All @@ -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
Expand Down
50 changes: 28 additions & 22 deletions test-suite/fittedbonddiscountcurve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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> bond = ext::make_shared<ZeroCouponBond>(3, TARGET(), 100.0,
today + Period(10, Years));
Handle<Quote> q(ext::make_shared<SimpleQuote>(100.0));

std::vector<ext::shared_ptr<BondHelper> > helpers(1);
helpers[0] = ext::make_shared<BondHelper>(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) {
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 1aae346

Please sign in to comment.