diff --git a/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp b/ql/pricingengines/swaption/gaussian1dfloatfloatswaptionengine.cpp index aa9e8cbb997..d75e6296d44 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 += @@ -134,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; @@ -166,7 +170,7 @@ namespace QuantLib { } // end probability computation - Date event1 = Date(), event0; + Date event0; Time event1Time = Null(), event0Time; ext::shared_ptr ibor1 = @@ -220,29 +224,44 @@ 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); + + 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 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); - 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); @@ -263,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_) { @@ -277,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 += @@ -301,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( @@ -309,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( @@ -319,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( @@ -327,7 +346,7 @@ namespace QuantLib { payoff1a.bCoefficients()[0], payoff1a.aCoefficients()[0], pa[0], z[0], -100.0, z[0]) * - zSpreadDf; + rollbackZSpreadDf; } } } @@ -340,21 +359,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(), @@ -368,7 +377,7 @@ namespace QuantLib { payoff1.bCoefficients()[i], payoff1.aCoefficients()[i], p[i], z[i], z[i], z[i + 1]) * - zSpreadDf; + rollbackZSpreadDf; } if (extrapolatePayoff_) { if (flatPayoffExtrapolation_) { @@ -378,12 +387,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 += @@ -398,7 +407,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( @@ -411,7 +420,7 @@ namespace QuantLib { .aCoefficients()[0], p[0], z[0], -100.0, z[0]) * - zSpreadDf; + rollbackZSpreadDf; } } } @@ -664,7 +673,6 @@ namespace QuantLib { } // end probability computation - event1 = event0; event1Time = event0Time; } while (--idx >= -1); diff --git a/test-suite/floatfloatswap.cpp b/test-suite/floatfloatswap.cpp index cf97797c919..dc116810f26 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,248 @@ 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, + std::vector* probabilitiesOut = nullptr) { + 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"); + 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, + &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); + 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_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)); + BOOST_CHECK(std::isfinite(extrapolatedDigitalCall)); + 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); + 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_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()