From e709cb6dee758b03c96d404e1e5d7d411881a390 Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Wed, 8 Jan 2025 16:12:44 -0500 Subject: [PATCH] Move transformation used in GlobalBootstrap into Traits Also, use exp instead of atan transformation for discount factors. This is an improvement on #2120 based on these observations: * Rates-based traits don't need any constraints in the optimization, so we don't need any transformation with them. This removes unnecessary computations. * Discount factors are only bounded on one side (have to be positive), so instead of estimating a max value to use atan, we can simply use exp. This is faster and uses fewer magic numbers. Another issue with atan is that it changes the gradients too much when the value is far from the middle of the range. This pushes optimization away from the correct solution when rates are low. This makes it hard to find value of maxDF that works for both very low and very high rates. --- ql/termstructures/globalbootstrap.hpp | 24 +++--------- ql/termstructures/yield/bootstraptraits.hpp | 42 ++++++++++----------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/ql/termstructures/globalbootstrap.hpp b/ql/termstructures/globalbootstrap.hpp index ceb1f393dd..9512981edb 100644 --- a/ql/termstructures/globalbootstrap.hpp +++ b/ql/termstructures/globalbootstrap.hpp @@ -57,7 +57,7 @@ template class GlobalBootstrap { The additional helpers are treated like the usual rate helpers, but no standard pillar dates are added for them. WARNING: This class is known to work with Traits Discount, ZeroYield, Forward, i.e. the usual traits for IR curves - in QL. It requires Traits::minValueGlobal() and Traits::maxValueGlobal() to be implemented. Also, check the usage + in QL. It requires Traits::transformDirect() and Traits::transformInverse() to be implemented. Also, check the usage of Traits::updateGuess(), Traits::guess() in this class. */ GlobalBootstrap(std::vector > additionalHelpers, @@ -200,6 +200,7 @@ template void GlobalBootstrap::initialize() const { // but reasonable numbers might be needed for the whole data vector // because, e.g., of interpolation's early checks ts_->data_ = std::vector(dates.size(), Traits::initialValue(ts_)); + validCurve_ = false; } initialized_ = true; } @@ -242,26 +243,10 @@ template void GlobalBootstrap::calculate() const { ts_->interpolator_.interpolate(ts_->times_.begin(), ts_->times_.end(), ts_->data_.begin()); } - // determine bounds, we use an unconstrained optimisation transforming the free variables to [lowerBound,upperBound] - const Size numberBounds = ts_->times_.size() - 1; - std::vector lowerBounds(numberBounds), upperBounds(numberBounds); - for (Size i = 0; i < numberBounds; ++i) { - lowerBounds[i] = Traits::minValueGlobal(i + 1, ts_, validCurve_); - upperBounds[i] = Traits::maxValueGlobal(i + 1, ts_, validCurve_); - } - // setup cost function - auto transformDirect = [&](const Real x, const Size i) { - return (std::atan(x) + M_PI_2) / M_PI * (upperBounds[i] - lowerBounds[i]) + lowerBounds[i]; - }; - - auto transformInverse = [&](const Real y, const Size i) { - return std::tan((y - lowerBounds[i]) * M_PI / (upperBounds[i] - lowerBounds[i]) - M_PI_2); - }; - SimpleCostFunction cost([&](const Array& x) { for (Size i = 0; i < x.size(); ++i) { - Traits::updateGuess(ts_->data_, transformDirect(x[i], i), i + 1); + Traits::updateGuess(ts_->data_, Traits::transformDirect(x[i], i + 1, ts_), i + 1); } ts_->interpolation_.update(); std::vector result(numberHelpers_); @@ -280,12 +265,13 @@ template void GlobalBootstrap::calculate() const { }); // setup guess + const Size numberBounds = ts_->times_.size() - 1; Array guess(numberBounds); for (Size i = 0; i < numberBounds; ++i) { // just pass zero as the first alive helper, it's not used in the standard QL traits anyway // update ts_->data_ since Traits::guess() usually depends on previous values Traits::updateGuess(ts_->data_, Traits::guess(i + 1, ts_, validCurve_, 0), i + 1); - guess[i] = transformInverse(ts_->data_[i + 1], i); + guess[i] = Traits::transformInverse(ts_->data_[i + 1], i + 1, ts_); } // setup problem diff --git a/ql/termstructures/yield/bootstraptraits.hpp b/ql/termstructures/yield/bootstraptraits.hpp index 02999a2d98..3aa9eb0b78 100644 --- a/ql/termstructures/yield/bootstraptraits.hpp +++ b/ql/termstructures/yield/bootstraptraits.hpp @@ -38,7 +38,6 @@ namespace QuantLib { namespace detail { const Real avgRate = 0.05; const Real maxRate = 1.0; - const Real maxDF = 10.0; } //! Discount-curve traits @@ -102,16 +101,16 @@ namespace QuantLib { return c->data()[i-1] * std::exp(detail::maxRate * dt); } - // possible constraints for global optimization + // transformation to add constraints to an unconstrained optimization template - static Real minValueGlobal(Size i, const C* c, bool validData) + static Real transformDirect(Real x, Size i, const C* c) { - return 0; + return std::exp(x); } template - static Real maxValueGlobal(Size i, const C* c, bool validData) + static Real transformInverse(Real x, Size i, const C* c) { - return detail::maxDF; + return std::log(x); } // root-finding update @@ -193,16 +192,16 @@ namespace QuantLib { return detail::maxRate; } - // possible constraints for global optimization + // transformation to add constraints to an unconstrained optimization template - static Real minValueGlobal(Size i, const C* c, bool validData) + static Real transformDirect(Real x, Size i, const C* c) { - return -detail::maxRate; + return x; } template - static Real maxValueGlobal(Size i, const C* c, bool validData) + static Real transformInverse(Real x, Size i, const C* c) { - return detail::maxRate; + return x; } // root-finding update @@ -286,18 +285,19 @@ namespace QuantLib { return detail::maxRate; } - // possible constraints for global optimization + // transformation to add constraints to an unconstrained optimization template - static Real minValueGlobal(Size i, const C* c, bool validData) + static Real transformDirect(Real x, Size i, const C* c) { - return -detail::maxRate; + return x; } template - static Real maxValueGlobal(Size i, const C* c, bool validData) + static Real transformInverse(Real x, Size i, const C* c) { - return detail::maxRate; + return x; } + // root-finding update static void updateGuess(std::vector& data, Real forward, @@ -381,16 +381,16 @@ namespace QuantLib { return detail::maxRate; } - // possible constraints for global optimization + // transformation to add constraints to an unconstrained optimization template - static Real minValueGlobal(Size i, const C* c, bool validData) + static Real transformDirect(Real x, Size i, const C* c) { - return std::max(-detail::maxRate, -1.0 / c->times()[i] + 1E-8); + return std::exp(x) + (-1.0 / c->times()[i] + 1E-8); } template - static Real maxValueGlobal(Size i, const C* c, bool validData) + static Real transformInverse(Real x, Size i, const C* c) { - return detail::maxRate; + return std::log(x - (-1.0 / c->times()[i] + 1E-8)); } // root-finding update