From dccb63488d36737840509e95a97ec7320981b84f Mon Sep 17 00:00:00 2001 From: LO <46852948+losterbr@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:54:27 +0100 Subject: [PATCH 1/7] Test Gaussian float-float swaption coverage --- test-suite/floatfloatswap.cpp | 170 ++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/test-suite/floatfloatswap.cpp b/test-suite/floatfloatswap.cpp index cf97797c919..2290b36b4a7 100644 --- a/test-suite/floatfloatswap.cpp +++ b/test-suite/floatfloatswap.cpp @@ -19,10 +19,18 @@ #include "toplevelfixture.hpp" #include "utilities.hpp" +#include +#include #include +#include +#include +#include #include +#include #include +#include #include +#include #include #include #include @@ -266,6 +274,168 @@ BOOST_AUTO_TEST_CASE(testExpiredSwapFairSpread) { ExpectedErrorMessage("fair spread 2 not available")); } +BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionAndCalibrationBasket) { + CommonVars vars; + const auto model = ext::make_shared( + vars.termStructure, std::vector(), std::vector{0.01}, 0.03, 60.0); + const Date start = vars.settlement; + const Date maturity = vars.calendar.advance(start, 5 * Years); + const Schedule schedule1(start, maturity, 3 * Months, vars.calendar, + ModifiedFollowing, ModifiedFollowing, + DateGeneration::Forward, false); + const Schedule schedule2(start, maturity, 6 * Months, vars.calendar, + ModifiedFollowing, ModifiedFollowing, + DateGeneration::Forward, false); + const auto swap = ext::make_shared( + Swap::Payer, vars.nominal, vars.nominal, schedule1, vars.index1, + vars.index1->dayCounter(), schedule2, vars.index2, + vars.index2->dayCounter()); + const Date firstExercise = vars.calendar.advance(vars.today, 2 * Years); + const Date secondExercise = vars.calendar.advance(vars.today, 3 * Years); + const auto exercise = ext::make_shared( + std::vector{firstExercise, secondExercise}); + const auto swaption = ext::make_shared(swap, exercise); + const auto engine = ext::make_shared( + model, 16, 5.0); + swaption->setPricingEngine(engine); + + const Real value = swaption->NPV(); + BOOST_CHECK(std::isfinite(value)); + BOOST_CHECK(std::isfinite(swaption->result("underlyingValue"))); + + const auto swaptionVolatility = ext::make_shared( + vars.settlementDays, vars.calendar, Following, 0.20, Actual365Fixed()); + const auto standardSwapBase = ext::make_shared( + 5 * Years, vars.termStructure); + + const auto naiveBasket = swaption->calibrationBasket( + standardSwapBase, swaptionVolatility, BasketGeneratingEngine::Naive); + BOOST_CHECK_EQUAL(naiveBasket.size(), exercise->dates().size()); + + const auto fittedBasket = swaption->calibrationBasket( + standardSwapBase, swaptionVolatility, + BasketGeneratingEngine::MaturityStrikeByDeltaGamma); + BOOST_CHECK_EQUAL(fittedBasket.size(), exercise->dates().size()); +} + +BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { + CommonVars vars; + const auto model = ext::make_shared( + vars.termStructure, std::vector(), std::vector{0.01}, 0.03, 60.0); + const Date start = vars.settlement; + const Date maturity = vars.calendar.advance(start, 5 * Years); + const Schedule schedule1(start, maturity, 3 * Months, vars.calendar, + ModifiedFollowing, ModifiedFollowing, + DateGeneration::Forward, false); + const Schedule schedule2(start, maturity, 6 * Months, vars.calendar, + ModifiedFollowing, ModifiedFollowing, + DateGeneration::Forward, false); + const Date firstExercise = vars.calendar.advance(vars.today, 2 * Years); + const Date secondExercise = vars.calendar.advance(vars.today, 3 * Years); + const auto exercise = ext::make_shared( + std::vector{firstExercise, secondExercise}); + const Handle oas(ext::make_shared(0.002)); + + const auto makeSwap = [&](Swap::Type type, + const ext::shared_ptr& index1, + const ext::shared_ptr& index2, + bool exchangePrincipal, + bool capAndFloor) { + const Rate cap = capAndFloor ? 0.01 : Null(); + const Rate floor = capAndFloor ? 0.001 : Null(); + return ext::make_shared( + type, vars.nominal, vars.nominal, schedule1, index1, + index1->dayCounter(), schedule2, index2, index2->dayCounter(), + exchangePrincipal, exchangePrincipal, + 1.0, 0.001, cap, floor, 1.0, -0.001, cap, floor); + }; + + const auto price = [&](const ext::shared_ptr& swap, + const ext::shared_ptr& swaptionExercise, + Gaussian1dFloatFloatSwaptionEngine::Probabilities probabilities, + bool extrapolate, + bool flatExtrapolation, + bool useOas) { + const auto swaption = ext::make_shared(swap, swaptionExercise); + const auto engine = ext::make_shared( + model, 12, 5.0, extrapolate, flatExtrapolation, + useOas ? oas : Handle(), vars.termStructure, false, probabilities); + swaption->setPricingEngine(engine); + const Real value = swaption->NPV(); + BOOST_CHECK(std::isfinite(value)); + BOOST_CHECK(std::isfinite(swaption->result("underlyingValue"))); + if (probabilities != Gaussian1dFloatFloatSwaptionEngine::None) { + const auto exerciseProbabilities = swaption->result>( + "probabilities"); + BOOST_CHECK_EQUAL(exerciseProbabilities.size(), swaptionExercise->dates().size() + 1); + for (Real probability : exerciseProbabilities) { + BOOST_CHECK_GE(probability, 0.0); + BOOST_CHECK_LE(probability, 1.0 + 1.0e-12); + } + } + return value; + }; + + const auto iborSwap = makeSwap(Swap::Payer, vars.index1, vars.index2, false, false); + const Real nonFlatCall = price(iborSwap, exercise, + Gaussian1dFloatFloatSwaptionEngine::None, true, false, false); + const Real flatCall = price(iborSwap, exercise, + Gaussian1dFloatFloatSwaptionEngine::Naive, true, true, true); + const Real noExtrapolationPut = price( + makeSwap(Swap::Receiver, vars.index1, vars.index2, false, false), exercise, + Gaussian1dFloatFloatSwaptionEngine::Digital, false, false, false); + const Real extrapolatedDigitalCall = price(iborSwap, exercise, + Gaussian1dFloatFloatSwaptionEngine::Digital, true, false, false); + const Real extrapolatedDigitalPut = price( + makeSwap(Swap::Receiver, vars.index1, vars.index2, false, false), exercise, + Gaussian1dFloatFloatSwaptionEngine::Digital, true, false, false); + BOOST_CHECK(std::isfinite(nonFlatCall - flatCall)); + BOOST_CHECK(std::isfinite(noExtrapolationPut)); + BOOST_CHECK(std::isfinite(extrapolatedDigitalCall)); + BOOST_CHECK(std::isfinite(extrapolatedDigitalPut)); + + const auto rebateExercise = ext::make_shared( + *exercise, std::vector{100.0, 50.0}, 2, vars.calendar); + const Real rebatedValue = price(iborSwap, rebateExercise, + Gaussian1dFloatFloatSwaptionEngine::Naive, true, false, true); + BOOST_CHECK(std::isfinite(rebatedValue)); + + const auto exchangedCappedSwap = makeSwap( + Swap::Payer, vars.index1, vars.index2, true, true); + BOOST_CHECK(std::isfinite(price(exchangedCappedSwap, exercise, + Gaussian1dFloatFloatSwaptionEngine::None, true, false, false))); + + const auto cms3m = ext::make_shared( + 5 * Years, vars.termStructure); + const auto cms10y = ext::make_shared( + 10 * Years, vars.termStructure); + const auto spreadIndex = ext::make_shared( + "test cms spread", cms10y, cms3m); + BOOST_CHECK(std::isfinite(price(makeSwap( + Swap::Payer, cms10y, vars.index2, false, false), exercise, + Gaussian1dFloatFloatSwaptionEngine::None, false, false, false))); + BOOST_CHECK(std::isfinite(price(makeSwap( + Swap::Receiver, spreadIndex, cms3m, false, false), exercise, + Gaussian1dFloatFloatSwaptionEngine::None, true, false, false))); + BOOST_CHECK(std::isfinite(price(makeSwap( + Swap::Payer, vars.index1, spreadIndex, false, false), exercise, + Gaussian1dFloatFloatSwaptionEngine::None, true, false, false))); + + const auto expiredSwaption = ext::make_shared( + iborSwap, ext::make_shared(vars.settlement)); + expiredSwaption->setPricingEngine(ext::make_shared( + model, 12, 5.0)); + BOOST_CHECK_EQUAL(expiredSwaption->NPV(), 0.0); + + const auto cashSettledSwaption = ext::make_shared( + iborSwap, ext::make_shared(firstExercise), + Settlement::Cash, Settlement::ParYieldCurve); + cashSettledSwaption->setPricingEngine( + ext::make_shared(model, 12, 5.0)); + BOOST_CHECK_EXCEPTION(cashSettledSwaption->NPV(), Error, + ExpectedErrorMessage("cash settled (ParYieldCurve) swaptions not priced")); +} + BOOST_AUTO_TEST_SUITE_END() From 3e7673988f5bb9939ce5d7e136eaa7ad16c71dd9 Mon Sep 17 00:00:00 2001 From: LO <46852948+losterbr@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:12:22 +0100 Subject: [PATCH 2/7] Guard float-float calibration guess boundaries --- .../gaussian1dfloatfloatswaptionengine.cpp | 9 ++- test-suite/floatfloatswap.cpp | 61 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp index aa9e8cbb997..0c55c418941 100644 --- a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp +++ b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp @@ -75,6 +75,9 @@ namespace QuantLib { std::upper_bound(arguments_.leg1ResetDates.begin(), arguments_.leg1ResetDates.end(), expiry - 1) - arguments_.leg1ResetDates.begin(); + const Size remainingResetCount = arguments_.leg1ResetDates.size() - idx1; + QL_REQUIRE(remainingResetCount > 0, + "no leg 1 reset dates remain at calibration expiry " << expiry); // very simple initial guess // check guess for nominal and weighted maturity ! @@ -83,8 +86,10 @@ namespace QuantLib { for (Size i = idx1; i < arguments_.leg1ResetDates.size(); i++) { nominalSum1 += arguments_.nominal1[i]; } - Real nominalAvg1 = nominalSum1 / - (arguments_.leg1ResetDates.size() - idx1); + QL_REQUIRE(nominalSum1 != 0.0, + "remaining leg 1 notionals sum to zero at calibration expiry " + << expiry); + Real nominalAvg1 = nominalSum1 / remainingResetCount; Real weightedMaturity1 = 0.0; for (Size i = idx1; i < arguments_.leg1ResetDates.size(); i++) { weightedMaturity1 += diff --git a/test-suite/floatfloatswap.cpp b/test-suite/floatfloatswap.cpp index 2290b36b4a7..0711da983f1 100644 --- a/test-suite/floatfloatswap.cpp +++ b/test-suite/floatfloatswap.cpp @@ -381,6 +381,9 @@ BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { Gaussian1dFloatFloatSwaptionEngine::None, true, false, false); const Real flatCall = price(iborSwap, exercise, Gaussian1dFloatFloatSwaptionEngine::Naive, true, true, true); + const Real receiverPrice = price( + makeSwap(Swap::Receiver, vars.index1, vars.index2, false, false), exercise, + Gaussian1dFloatFloatSwaptionEngine::None, true, false, false); const Real noExtrapolationPut = price( makeSwap(Swap::Receiver, vars.index1, vars.index2, false, false), exercise, Gaussian1dFloatFloatSwaptionEngine::Digital, false, false, false); @@ -393,6 +396,8 @@ BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { BOOST_CHECK(std::isfinite(noExtrapolationPut)); BOOST_CHECK(std::isfinite(extrapolatedDigitalCall)); BOOST_CHECK(std::isfinite(extrapolatedDigitalPut)); + BOOST_CHECK_GT(nonFlatCall, 0.0); + BOOST_CHECK_GT(receiverPrice, 0.0); const auto rebateExercise = ext::make_shared( *exercise, std::vector{100.0, 50.0}, 2, vars.calendar); @@ -436,6 +441,62 @@ BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { ExpectedErrorMessage("cash settled (ParYieldCurve) swaptions not priced")); } +BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCalibrationInitialGuessBoundaries) { + CommonVars vars; + const auto model = ext::make_shared( + vars.termStructure, std::vector(), std::vector{0.01}, 0.03, 60.0); + const auto swaptionVolatility = ext::make_shared( + vars.settlementDays, vars.calendar, Following, 0.20, Actual365Fixed()); + const auto standardSwapBase = ext::make_shared( + 5 * Years, vars.termStructure); + const auto engine = ext::make_shared(model, 12, 5.0); + + const Date start = vars.settlement; + const Date oneYear = vars.calendar.advance(start, 1 * Years); + const Schedule oneYearSchedule(start, oneYear, 6 * Months, vars.calendar, + ModifiedFollowing, ModifiedFollowing, + DateGeneration::Forward, false); + const auto oneYearSwap = ext::make_shared( + Swap::Payer, vars.nominal, vars.nominal, + oneYearSchedule, vars.index2, vars.index2->dayCounter(), + oneYearSchedule, vars.index2, vars.index2->dayCounter()); + const Date expiryAfterLastReset = vars.calendar.advance(start, 9 * Months); + const EuropeanExercise europeanExpiry(expiryAfterLastReset); + const auto rebateExercise = ext::make_shared( + europeanExpiry, 100.0, 2, vars.calendar); + const auto noResetSwaption = ext::make_shared( + oneYearSwap, rebateExercise); + noResetSwaption->setPricingEngine(engine); + BOOST_CHECK_EXCEPTION( + noResetSwaption->calibrationBasket( + standardSwapBase, swaptionVolatility, + BasketGeneratingEngine::MaturityStrikeByDeltaGamma), + Error, ExpectedErrorMessage("no leg 1 reset dates remain")); + + const Date fiveYears = vars.calendar.advance(start, 5 * Years); + const Schedule quarterlySchedule(start, fiveYears, 3 * Months, vars.calendar, + ModifiedFollowing, ModifiedFollowing, + DateGeneration::Forward, false); + std::vector notionals1(quarterlySchedule.size() - 1, 0.0); + notionals1[notionals1.size() - 2] = vars.nominal; + notionals1.back() = -vars.nominal; + const std::vector notionals2( + quarterlySchedule.size() - 1, vars.nominal); + const auto cancellingNotionalSwap = ext::make_shared( + Swap::Payer, notionals1, notionals2, + quarterlySchedule, vars.index1, vars.index1->dayCounter(), + quarterlySchedule, vars.index1, vars.index1->dayCounter()); + const auto cancellingNotionalSwaption = ext::make_shared( + cancellingNotionalSwap, + ext::make_shared(vars.calendar.advance(vars.today, 2 * Years))); + cancellingNotionalSwaption->setPricingEngine(engine); + BOOST_CHECK_EXCEPTION( + cancellingNotionalSwaption->calibrationBasket( + standardSwapBase, swaptionVolatility, + BasketGeneratingEngine::MaturityStrikeByDeltaGamma), + Error, ExpectedErrorMessage("remaining leg 1 notionals sum to zero")); +} + BOOST_AUTO_TEST_SUITE_END() From 90e78802b824eebf88b171df7b74a24abcfa41ac Mon Sep 17 00:00:00 2001 From: LO <46852948+losterbr@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:26:11 +0100 Subject: [PATCH 3/7] Test Gaussian float-float rollback value --- test-suite/floatfloatswap.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test-suite/floatfloatswap.cpp b/test-suite/floatfloatswap.cpp index 0711da983f1..fd91e507432 100644 --- a/test-suite/floatfloatswap.cpp +++ b/test-suite/floatfloatswap.cpp @@ -392,6 +392,7 @@ BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { const Real extrapolatedDigitalPut = price( makeSwap(Swap::Receiver, vars.index1, vars.index2, false, false), exercise, Gaussian1dFloatFloatSwaptionEngine::Digital, true, false, false); + BOOST_CHECK_SMALL(nonFlatCall - 4.1426336441813841e-06, 1.0e-10); BOOST_CHECK(std::isfinite(nonFlatCall - flatCall)); BOOST_CHECK(std::isfinite(noExtrapolationPut)); BOOST_CHECK(std::isfinite(extrapolatedDigitalCall)); From e6286077edec11e7e087b8fd6e4f2bd499ea4412 Mon Sep 17 00:00:00 2001 From: LO <46852948+losterbr@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:27:52 +0100 Subject: [PATCH 4/7] Optimize Gaussian float-float rollback --- .../gaussian1dfloatfloatswaptionengine.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp index 0c55c418941..5480216ba5b 100644 --- a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp +++ b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp @@ -139,7 +139,6 @@ namespace QuantLib { int idx = events.size() - 1; - FloatFloatSwap swap = *arguments_.swap; Option::Type type = arguments_.type == Swap::Payer ? Option::Call : Option::Put; @@ -225,6 +224,17 @@ namespace QuantLib { // todo add openmp support later on (as in gaussian1dswaptionengine) + CubicInterpolation payoff0( + z.begin(), z.end(), npv1.begin(), + CubicInterpolation::Spline, true, + CubicInterpolation::Lagrange, 0.0, + CubicInterpolation::Lagrange, 0.0); + CubicInterpolation payoff0a( + z.begin(), z.end(), npv1a.begin(), + CubicInterpolation::Spline, true, + CubicInterpolation::Lagrange, 0.0, + CubicInterpolation::Lagrange, 0.0); + for (Size k = 0; k < (event0 > expiry ? npv0.size() : 1); k++) { // roll back @@ -238,16 +248,6 @@ namespace QuantLib { Array yg = model_->yGrid(stddevs_, integrationPoints_, event1Time, event0Time, event0 > expiry ? z[k] : y); - CubicInterpolation payoff0( - z.begin(), z.end(), npv1.begin(), - CubicInterpolation::Spline, true, - CubicInterpolation::Lagrange, 0.0, - CubicInterpolation::Lagrange, 0.0); - CubicInterpolation payoff0a( - z.begin(), z.end(), npv1a.begin(), - CubicInterpolation::Spline, true, - CubicInterpolation::Lagrange, 0.0, - CubicInterpolation::Lagrange, 0.0); for (Size i = 0; i < yg.size(); i++) { p[i] = payoff0(yg[i], true); pa[i] = payoff0a(yg[i], true); From 854e94bd4407a4fefe509c79b107e6584b916ff1 Mon Sep 17 00:00:00 2001 From: LO <46852948+losterbr@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:39:27 +0100 Subject: [PATCH 5/7] Strengthen float-float swaption regression tests --- test-suite/floatfloatswap.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test-suite/floatfloatswap.cpp b/test-suite/floatfloatswap.cpp index fd91e507432..dc116810f26 100644 --- a/test-suite/floatfloatswap.cpp +++ b/test-suite/floatfloatswap.cpp @@ -355,7 +355,8 @@ BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { Gaussian1dFloatFloatSwaptionEngine::Probabilities probabilities, bool extrapolate, bool flatExtrapolation, - bool useOas) { + bool useOas, + std::vector* probabilitiesOut = nullptr) { const auto swaption = ext::make_shared(swap, swaptionExercise); const auto engine = ext::make_shared( model, 12, 5.0, extrapolate, flatExtrapolation, @@ -367,20 +368,29 @@ BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { if (probabilities != Gaussian1dFloatFloatSwaptionEngine::None) { const auto exerciseProbabilities = swaption->result>( "probabilities"); + if (probabilitiesOut != nullptr) + *probabilitiesOut = exerciseProbabilities; BOOST_CHECK_EQUAL(exerciseProbabilities.size(), swaptionExercise->dates().size() + 1); + Real probabilitySum = 0.0; for (Real probability : exerciseProbabilities) { BOOST_CHECK_GE(probability, 0.0); BOOST_CHECK_LE(probability, 1.0 + 1.0e-12); + probabilitySum += probability; } + BOOST_CHECK_GT(probabilitySum, 0.0); } return value; }; const auto iborSwap = makeSwap(Swap::Payer, vars.index1, vars.index2, false, false); + std::vector flatCallProbabilities; const Real nonFlatCall = price(iborSwap, exercise, Gaussian1dFloatFloatSwaptionEngine::None, true, false, false); const Real flatCall = price(iborSwap, exercise, - Gaussian1dFloatFloatSwaptionEngine::Naive, true, true, true); + Gaussian1dFloatFloatSwaptionEngine::Naive, true, true, true, + &flatCallProbabilities); + const Real flatCallWithoutOas = price(iborSwap, exercise, + Gaussian1dFloatFloatSwaptionEngine::Naive, true, true, false); const Real receiverPrice = price( makeSwap(Swap::Receiver, vars.index1, vars.index2, false, false), exercise, Gaussian1dFloatFloatSwaptionEngine::None, true, false, false); @@ -392,6 +402,13 @@ BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { const Real extrapolatedDigitalPut = price( makeSwap(Swap::Receiver, vars.index1, vars.index2, false, false), exercise, Gaussian1dFloatFloatSwaptionEngine::Digital, true, false, false); + BOOST_CHECK_SMALL(flatCall - 4.1503974670013793e-06, 1.0e-10); + BOOST_CHECK_SMALL(flatCallWithoutOas - 4.1437978754246558e-06, 1.0e-10); + const std::vector expectedProbabilities{ + 0.0, 0.00021123900304688715, 0.99380672505088874}; + BOOST_CHECK_EQUAL(flatCallProbabilities.size(), expectedProbabilities.size()); + for (Size i = 0; i < expectedProbabilities.size(); ++i) + BOOST_CHECK_SMALL(flatCallProbabilities[i] - expectedProbabilities[i], 1.0e-10); BOOST_CHECK_SMALL(nonFlatCall - 4.1426336441813841e-06, 1.0e-10); BOOST_CHECK(std::isfinite(nonFlatCall - flatCall)); BOOST_CHECK(std::isfinite(noExtrapolationPut)); @@ -399,6 +416,7 @@ BOOST_AUTO_TEST_CASE(testGaussian1dFloatFloatSwaptionCoverage) { BOOST_CHECK(std::isfinite(extrapolatedDigitalPut)); BOOST_CHECK_GT(nonFlatCall, 0.0); BOOST_CHECK_GT(receiverPrice, 0.0); + BOOST_CHECK_GT(std::fabs(flatCall - flatCallWithoutOas), 1.0e-12); const auto rebateExercise = ext::make_shared( *exercise, std::vector{100.0, 50.0}, 2, vars.calendar); From 6a46c8d188eb23469416cec77ca6795d523720dc Mon Sep 17 00:00:00 2001 From: LO <46852948+losterbr@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:46:41 +0100 Subject: [PATCH 6/7] Optimize float-float probability rollback --- .../gaussian1dfloatfloatswaptionengine.cpp | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp index 5480216ba5b..bddd85ed4c2 100644 --- a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp +++ b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp @@ -170,7 +170,7 @@ namespace QuantLib { } // end probability computation - Date event1 = Date(), event0; + Date event0; Time event1Time = Null(), event0Time; ext::shared_ptr ibor1 = @@ -235,6 +235,24 @@ namespace QuantLib { CubicInterpolation::Lagrange, 0.0, CubicInterpolation::Lagrange, 0.0); + std::vector probabilityPayoffs; + if (considerProbabilities && probabilities_ != None && + event1Time != Null()) { + probabilityPayoffs.reserve(npvp1.size()); + for (Size m = 0; m < npvp1.size(); ++m) { + probabilityPayoffs.emplace_back( + z.begin(), z.end(), npvp1[m].begin(), + CubicInterpolation::Spline, true, + CubicInterpolation::Lagrange, 0.0, + CubicInterpolation::Lagrange, 0.0); + } + } + + const Real rollbackZSpreadDf = + event1Time == Null() || oas_.empty() + ? Real(1.0) + : std::exp(-oas_->value() * (event1Time - event0Time)); + for (Size k = 0; k < (event0 > expiry ? npv0.size() : 1); k++) { // roll back @@ -345,21 +363,11 @@ namespace QuantLib { for (Size m = 0; m < npvp0.size(); m++) { Real price = 0.0; if (event1Time != Null()) { - Real zSpreadDf = - oas_.empty() - ? Real(1.0) - : std::exp(-oas_->value() * - (event1Time - event0Time)); Array yg = model_->yGrid( stddevs_, integrationPoints_, event1Time, event0Time, event0 > expiry ? z[k] : 0.0); - CubicInterpolation payoff0( - z.begin(), z.end(), npvp1[m].begin(), - CubicInterpolation::Spline, true, - CubicInterpolation::Lagrange, 0.0, - CubicInterpolation::Lagrange, 0.0); for (Size i = 0; i < yg.size(); i++) { - p[i] = payoff0(yg[i], true); + p[i] = probabilityPayoffs[m](yg[i], true); } CubicInterpolation payoff1( z.begin(), z.end(), p.begin(), @@ -373,7 +381,7 @@ namespace QuantLib { payoff1.bCoefficients()[i], payoff1.aCoefficients()[i], p[i], z[i], z[i], z[i + 1]) * - zSpreadDf; + rollbackZSpreadDf; } if (extrapolatePayoff_) { if (flatPayoffExtrapolation_) { @@ -383,12 +391,12 @@ namespace QuantLib { p[z.size() - 2], z[z.size() - 2], z[z.size() - 1], 100.0) * - zSpreadDf; + rollbackZSpreadDf; price += Gaussian1dModel::gaussianShiftedPolynomialIntegral( 0.0, 0.0, 0.0, 0.0, p[0], z[0], -100.0, z[0]) * - zSpreadDf; + rollbackZSpreadDf; } else { if (type == Option::Call) price += @@ -403,7 +411,7 @@ namespace QuantLib { p[z.size() - 2], z[z.size() - 2], z[z.size() - 1], 100.0) * - zSpreadDf; + rollbackZSpreadDf; if (type == Option::Put) price += Gaussian1dModel::gaussianShiftedPolynomialIntegral( @@ -416,7 +424,7 @@ namespace QuantLib { .aCoefficients()[0], p[0], z[0], -100.0, z[0]) * - zSpreadDf; + rollbackZSpreadDf; } } } @@ -669,7 +677,6 @@ namespace QuantLib { } // end probability computation - event1 = event0; event1Time = event0Time; } while (--idx >= -1); From 3218632aca633a319380292656888dee07ae55b9 Mon Sep 17 00:00:00 2001 From: LO <46852948+losterbr@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:51:29 +0100 Subject: [PATCH 7/7] Reuse rollback OAS discount factor --- .../gaussian1dfloatfloatswaptionengine.cpp | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp index bddd85ed4c2..d75e6296d44 100644 --- a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp +++ b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp @@ -259,10 +259,6 @@ namespace QuantLib { Real price = 0.0, pricea = 0.0; if (event1Time != Null()) { - Real zSpreadDf = oas_.empty() - ? Real(1.0) - : std::exp(-oas_->value() * - (event1Time - event0Time)); Array yg = model_->yGrid(stddevs_, integrationPoints_, event1Time, event0Time, event0 > expiry ? z[k] : y); @@ -286,13 +282,13 @@ namespace QuantLib { payoff1.bCoefficients()[i], payoff1.aCoefficients()[i], p[i], z[i], z[i], z[i + 1]) * - zSpreadDf; + rollbackZSpreadDf; pricea += Gaussian1dModel::gaussianShiftedPolynomialIntegral( 0.0, payoff1a.cCoefficients()[i], payoff1a.bCoefficients()[i], payoff1a.aCoefficients()[i], pa[i], z[i], z[i], z[i + 1]) * - zSpreadDf; + rollbackZSpreadDf; } if (extrapolatePayoff_) { if (flatPayoffExtrapolation_) { @@ -300,20 +296,20 @@ namespace QuantLib { Gaussian1dModel::gaussianShiftedPolynomialIntegral( 0.0, 0.0, 0.0, 0.0, p[z.size() - 2], z[z.size() - 2], z[z.size() - 1], 100.0) * - zSpreadDf; + rollbackZSpreadDf; price += Gaussian1dModel::gaussianShiftedPolynomialIntegral( 0.0, 0.0, 0.0, 0.0, p[0], z[0], -100.0, z[0]) * - zSpreadDf; + rollbackZSpreadDf; pricea += Gaussian1dModel::gaussianShiftedPolynomialIntegral( 0.0, 0.0, 0.0, 0.0, pa[z.size() - 2], z[z.size() - 2], z[z.size() - 1], 100.0) * - zSpreadDf; + rollbackZSpreadDf; pricea += Gaussian1dModel::gaussianShiftedPolynomialIntegral( 0.0, 0.0, 0.0, 0.0, pa[0], z[0], -100.0, z[0]) * - zSpreadDf; + rollbackZSpreadDf; } else { if (type == Option::Call) price += @@ -324,7 +320,7 @@ namespace QuantLib { payoff1.aCoefficients()[z.size() - 2], p[z.size() - 2], z[z.size() - 2], z[z.size() - 1], 100.0) * - zSpreadDf; + rollbackZSpreadDf; if (type == Option::Put) price += Gaussian1dModel::gaussianShiftedPolynomialIntegral( @@ -332,7 +328,7 @@ namespace QuantLib { payoff1.bCoefficients()[0], payoff1.aCoefficients()[0], p[0], z[0], -100.0, z[0]) * - zSpreadDf; + rollbackZSpreadDf; if (type == Option::Call) pricea += Gaussian1dModel::gaussianShiftedPolynomialIntegral( @@ -342,7 +338,7 @@ namespace QuantLib { payoff1a.aCoefficients()[z.size() - 2], pa[z.size() - 2], z[z.size() - 2], z[z.size() - 1], 100.0) * - zSpreadDf; + rollbackZSpreadDf; if (type == Option::Put) pricea += Gaussian1dModel::gaussianShiftedPolynomialIntegral( @@ -350,7 +346,7 @@ namespace QuantLib { payoff1a.bCoefficients()[0], payoff1a.aCoefficients()[0], pa[0], z[0], -100.0, z[0]) * - zSpreadDf; + rollbackZSpreadDf; } } }