From ded4e6ed5449c852b698bfa3c7f09220ffa8f3a1 Mon Sep 17 00:00:00 2001 From: RAJ-A58 Date: Wed, 11 Nov 2026 23:41:52 +0530 Subject: [PATCH] Include all coefficients in ExponentialSplinesFitting when kappa is fixed When fixedKappa is specified, the parameter array x only contains the spline coefficients and not kappa. In discountFunction(), size() returns the number of parameters in x, so looping up to N - 1 was dropping the last coefficient. Fixes #3046. --- .../yield/nonlinearfittingmethods.cpp | 5 +- test-suite/fittedbonddiscountcurve.cpp | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/ql/termstructures/yield/nonlinearfittingmethods.cpp b/ql/termstructures/yield/nonlinearfittingmethods.cpp index f36cf04fd2e..278ff5d0d62 100644 --- a/ql/termstructures/yield/nonlinearfittingmethods.cpp +++ b/ql/termstructures/yield/nonlinearfittingmethods.cpp @@ -79,16 +79,17 @@ namespace QuantLib { //Use the interal fixedKappa_ member if non-zero, otherwise take kappa from the passed x[] array Real kappa = (fixedKappa_ != Null()) ? fixedKappa_: x[N-1]; Real coeff = 0; + Size n = (fixedKappa_ != Null()) ? N : N - 1; if (!constrainAtZero_) { - for (Size i = 0; i < N - 1; ++i) { + for (Size i = 0; i < n; ++i) { d += x[i] * std::exp(-kappa * (i + 1) * t); } } else { // notation: // d(t) = coeff* exp(-kappa*1*t) + x[0]* exp(-kappa*2*t) + // x[1]* exp(-kappa*3*t) + ..+ x[7]* exp(-kappa*9*t) - for (Size i = 0; i < N - 1; i++) { + for (Size i = 0; i < n; i++) { d += x[i] * std::exp(-kappa * (i + 2) * t); coeff += x[i]; } diff --git a/test-suite/fittedbonddiscountcurve.cpp b/test-suite/fittedbonddiscountcurve.cpp index 3e40b2f721d..b7c99f8254a 100644 --- a/test-suite/fittedbonddiscountcurve.cpp +++ b/test-suite/fittedbonddiscountcurve.cpp @@ -340,6 +340,56 @@ BOOST_AUTO_TEST_CASE(testConstraint) { BOOST_CHECK_GT(positiveCurve.fitResults().solution()[0], 0.0); } +BOOST_AUTO_TEST_CASE(testExponentialSplinesFixedKappa) { + BOOST_TEST_MESSAGE("Testing exponential splines fitting with fixed kappa..."); + + Date today = Settings::instance().evaluationDate(); + Date maxDate = today + Period(30, Years); + Actual365Fixed dayCounter; + Real t = 1.0; + Real kappa = 0.1; + + // Expected discount factor: + // d(1.0) = 0.2 * exp(-0.1 * 1.0) + 0.3 * exp(-0.2 * 1.0) + 0.5 * exp(-0.3 * 1.0) + Real expected = 0.2 * std::exp(-kappa * 1.0 * t) + + 0.3 * std::exp(-kappa * 2.0 * t) + + 0.5 * std::exp(-kappa * 3.0 * t); + + // 1. Unconstrained: 3 coefficients, fixed kappa + ExponentialSplinesFitting unconstrainedFixed(false, 3, kappa); + Array unconstrainedCoeffs = {0.2, 0.3, 0.5}; + FittedBondDiscountCurve curveUnconstrainedFixed(today, unconstrainedFixed, unconstrainedCoeffs, maxDate, dayCounter); + BOOST_CHECK_CLOSE(curveUnconstrainedFixed.discount(t), expected, 1e-10); + + // Free kappa with same parameters + ExponentialSplinesFitting unconstrainedFree(false, 3); + Array unconstrainedParamsFree = {0.2, 0.3, 0.5, kappa}; + FittedBondDiscountCurve curveUnconstrainedFree(today, unconstrainedFree, unconstrainedParamsFree, maxDate, dayCounter); + BOOST_CHECK_CLOSE(curveUnconstrainedFree.discount(t), expected, 1e-10); + + // Verify last coefficient affects the discount factor (not ignored) + Array unconstrainedBumped = {0.2, 0.3, 0.6}; + FittedBondDiscountCurve curveUnconstrainedBumped(today, unconstrainedFixed, unconstrainedBumped, maxDate, dayCounter); + BOOST_CHECK(curveUnconstrainedBumped.discount(t) != curveUnconstrainedFixed.discount(t)); + + // 2. Constrained at zero: first coefficient is 1 - (0.3 + 0.5) = 0.2 + ExponentialSplinesFitting constrainedFixed(true, 3, kappa); + Array constrainedCoeffs = {0.3, 0.5}; + FittedBondDiscountCurve curveConstrainedFixed(today, constrainedFixed, constrainedCoeffs, maxDate, dayCounter); + BOOST_CHECK_CLOSE(curveConstrainedFixed.discount(t), expected, 1e-10); + + // Free kappa with same parameters + ExponentialSplinesFitting constrainedFree(true, 3); + Array constrainedParamsFree = {0.3, 0.5, kappa}; + FittedBondDiscountCurve curveConstrainedFree(today, constrainedFree, constrainedParamsFree, maxDate, dayCounter); + BOOST_CHECK_CLOSE(curveConstrainedFree.discount(t), expected, 1e-10); + + // Verify last coefficient affects the discount factor (not ignored) + Array constrainedBumped = {0.3, 0.6}; + FittedBondDiscountCurve curveConstrainedBumped(today, constrainedFixed, constrainedBumped, maxDate, dayCounter); + BOOST_CHECK(curveConstrainedBumped.discount(t) != curveConstrainedFixed.discount(t)); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()