diff --git a/ql/experimental/termstructures/basisswapratehelpers.cpp b/ql/experimental/termstructures/basisswapratehelpers.cpp index e0ed60cf3b..f6b09c0a7d 100644 --- a/ql/experimental/termstructures/basisswapratehelpers.cpp +++ b/ql/experimental/termstructures/basisswapratehelpers.cpp @@ -39,11 +39,15 @@ namespace QuantLib { Handle discountHandle, bool bootstrapBaseCurve, std::optional useIndexedCoupons, - DateGeneration::Rule rule) + DateGeneration::Rule rule, + Integer paymentLag) : RelativeDateRateHelper(basis), tenor_(tenor), settlementDays_(settlementDays), calendar_(std::move(calendar)), convention_(convention), endOfMonth_(endOfMonth), discountHandle_(std::move(discountHandle)), bootstrapBaseCurve_(bootstrapBaseCurve), - useIndexedCoupons_(useIndexedCoupons), rule_(rule) { + useIndexedCoupons_(useIndexedCoupons), rule_(rule), paymentLag_(paymentLag) { + + QL_REQUIRE(baseIndex, "null base ibor index"); + QL_REQUIRE(otherIndex, "null other ibor index"); // we need to clone the index whose forecast curve we want to bootstrap // and copy the other one @@ -79,6 +83,7 @@ namespace QuantLib { .withRule(rule_); Leg baseLeg = IborLeg(baseSchedule, baseIndex_) .withNotionals(100.0) + .withPaymentLag(paymentLag_) .withIndexedCoupons(useIndexedCoupons_); auto lastBaseCoupon = ext::dynamic_pointer_cast(baseLeg.back()); @@ -91,14 +96,16 @@ namespace QuantLib { .withRule(rule_); Leg otherLeg = IborLeg(otherSchedule, otherIndex_) .withNotionals(100.0) + .withPaymentLag(paymentLag_) .withIndexedCoupons(useIndexedCoupons_); auto lastOtherCoupon = ext::dynamic_pointer_cast(otherLeg.back()); maturityDate_ = std::max(baseSchedule.endDate(), otherSchedule.endDate()); - latestRelevantDate_ = std::max(maturityDate_, - std::max(lastBaseCoupon->fixingEndDate(), - lastOtherCoupon->fixingEndDate())); + Date lastPaymentDate = std::max(baseLeg.back()->date(), otherLeg.back()->date()); + latestRelevantDate_ = std::max({maturityDate_, lastPaymentDate, + lastBaseCoupon->fixingEndDate(), + lastOtherCoupon->fixingEndDate()}); pillarDate_ = latestRelevantDate_; swap_ = ext::make_shared(baseLeg, otherLeg); @@ -143,20 +150,24 @@ namespace QuantLib { Integer paymentLag, std::optional overnightPaymentFrequency, std::optional useIndexedCoupons, - DateGeneration::Rule rule) + DateGeneration::Rule rule, + RateAveraging::Type averagingMethod, + bool telescopicValueDates) : RelativeDateRateHelper(basis), tenor_(tenor), settlementDays_(settlementDays), calendar_(std::move(calendar)), convention_(convention), endOfMonth_(endOfMonth), discountHandle_(std::move(discountHandle)), bootstrapBaseCurve_(bootstrapBaseCurve), paymentLag_(paymentLag), overnightPaymentFrequency_(overnightPaymentFrequency), - useIndexedCoupons_(useIndexedCoupons), rule_(rule) { + useIndexedCoupons_(useIndexedCoupons), rule_(rule), averagingMethod_(averagingMethod), + telescopicValueDates_(telescopicValueDates) { + + QL_REQUIRE(baseIndex, "null base overnight index"); + QL_REQUIRE(otherIndex, "null other ibor index"); // we need to clone the index whose forecast curve we want to bootstrap // and copy the other one if (bootstrapBaseCurve_) { baseIndex_ = ext::dynamic_pointer_cast( baseIndex->clone(termStructureHandle_)); - QL_REQUIRE(baseIndex_ != nullptr, - "the base index did not clone into an overnight index"); baseIndex_->unregisterWith(termStructureHandle_); otherIndex_ = otherIndex; } else { @@ -190,7 +201,12 @@ namespace QuantLib { Leg baseLeg = OvernightLeg(overnightSchedule, baseIndex_) .withNotionals(100.0) - .withPaymentLag(paymentLag_); + .withPaymentLag(paymentLag_) + .withTelescopicValueDates( + telescopicValueDates_ && averagingMethod_ == RateAveraging::Compound) + .withAveragingMethod(averagingMethod_); + auto lastBaseCoupon = + ext::dynamic_pointer_cast(baseLeg.back()); // an ibor leg pays one coupon per fixing, so its payment frequency // is the tenor of its own index @@ -213,14 +229,17 @@ namespace QuantLib { // the payment lag can push the last payment past the maturity date, // in which case the discount curve is needed up to that date Date lastPaymentDate = std::max(baseLeg.back()->date(), otherLeg.back()->date()); + Date lastBaseFixingEndDate = baseIndex_->maturityDate( + baseIndex_->valueDate(lastBaseCoupon->fixingDate())); latestRelevantDate_ = std::max({maturityDate_, lastPaymentDate, + lastBaseFixingEndDate, lastOtherCoupon->fixingEndDate()}); pillarDate_ = latestRelevantDate_; swap_ = ext::make_shared(baseLeg, otherLeg); - swap_->setPricingEngine(ext::make_shared( - discountHandle_.empty() ? termStructureHandle_ : discountHandle_)); + swap_->setPricingEngine( + ext::make_shared(discountRelinkableHandle_)); } void OvernightIborBasisSwapRateHelper::setTermStructure(YieldTermStructure* t) { @@ -231,6 +250,11 @@ namespace QuantLib { ext::shared_ptr temp(t, null_deleter()); termStructureHandle_.linkTo(temp, observer); + if (discountHandle_.empty()) + discountRelinkableHandle_.linkTo(temp, observer); + else + discountRelinkableHandle_.linkTo(*discountHandle_, observer); + RelativeDateRateHelper::setTermStructure(t); } @@ -247,4 +271,135 @@ namespace QuantLib { RateHelper::accept(v); } + + + OvernightOvernightBasisSwapRateHelper::OvernightOvernightBasisSwapRateHelper( + const Handle& basis, + const Period& tenor, + Natural settlementDays, + Calendar calendar, + BusinessDayConvention convention, + bool endOfMonth, + const ext::shared_ptr& baseIndex, + const ext::shared_ptr& otherIndex, + Handle discountHandle, + bool bootstrapBaseCurve, + Integer paymentLag, + Frequency paymentFrequency, + RateAveraging::Type baseAveragingMethod, + RateAveraging::Type otherAveragingMethod, + bool telescopicValueDates, + DateGeneration::Rule rule) + : RelativeDateRateHelper(basis), tenor_(tenor), settlementDays_(settlementDays), + calendar_(std::move(calendar)), convention_(convention), endOfMonth_(endOfMonth), + discountHandle_(std::move(discountHandle)), bootstrapBaseCurve_(bootstrapBaseCurve), + paymentLag_(paymentLag), paymentFrequency_(paymentFrequency), + baseAveragingMethod_(baseAveragingMethod), + otherAveragingMethod_(otherAveragingMethod), + telescopicValueDates_(telescopicValueDates), rule_(rule) { + + QL_REQUIRE(baseIndex, "null base overnight index"); + QL_REQUIRE(otherIndex, "null other overnight index"); + + // We need to clone the index whose forecast curve we want to + // bootstrap and copy the other one. + if (bootstrapBaseCurve_) { + baseIndex_ = ext::dynamic_pointer_cast( + baseIndex->clone(termStructureHandle_)); + baseIndex_->unregisterWith(termStructureHandle_); + otherIndex_ = otherIndex; + } else { + baseIndex_ = baseIndex; + otherIndex_ = ext::dynamic_pointer_cast( + otherIndex->clone(termStructureHandle_)); + otherIndex_->unregisterWith(termStructureHandle_); + } + + registerWith(baseIndex_); + registerWith(otherIndex_); + registerWith(discountHandle_); + + OvernightOvernightBasisSwapRateHelper::initializeDates(); + } + + void OvernightOvernightBasisSwapRateHelper::initializeDates() { + Date today = Settings::instance().evaluationDate(); + earliestDate_ = calendar_.advance(today, settlementDays_ * Days, Following); + // unadjusted, to avoid a spurious stub + Date terminationDate = earliestDate_ + tenor_; + + Schedule schedule = + MakeSchedule().from(earliestDate_).to(terminationDate) + .withFrequency(paymentFrequency_) + .withCalendar(calendar_) + .withConvention(convention_) + .endOfMonth(endOfMonth_) + .withRule(rule_); + + maturityDate_ = schedule.endDate(); + + Leg baseLeg = OvernightLeg(schedule, baseIndex_) + .withNotionals(100.0) + .withPaymentLag(paymentLag_) + .withTelescopicValueDates( + telescopicValueDates_ && baseAveragingMethod_ == RateAveraging::Compound) + .withAveragingMethod(baseAveragingMethod_); + + Leg otherLeg = OvernightLeg(schedule, otherIndex_) + .withNotionals(100.0) + .withPaymentLag(paymentLag_) + .withTelescopicValueDates( + telescopicValueDates_ && otherAveragingMethod_ == RateAveraging::Compound) + .withAveragingMethod(otherAveragingMethod_); + + auto lastBaseCoupon = + ext::dynamic_pointer_cast(baseLeg.back()); + auto lastOtherCoupon = + ext::dynamic_pointer_cast(otherLeg.back()); + + Date lastPaymentDate = std::max(baseLeg.back()->date(), otherLeg.back()->date()); + Date lastBaseFixingEndDate = baseIndex_->maturityDate( + baseIndex_->valueDate(lastBaseCoupon->fixingDate())); + Date lastOtherFixingEndDate = otherIndex_->maturityDate( + otherIndex_->valueDate(lastOtherCoupon->fixingDate())); + + latestRelevantDate_ = std::max({maturityDate_, lastPaymentDate, + lastBaseFixingEndDate, + lastOtherFixingEndDate}); + pillarDate_ = latestRelevantDate_; + + swap_ = ext::make_shared(baseLeg, otherLeg); + swap_->setPricingEngine( + ext::make_shared(discountRelinkableHandle_)); + } + + void OvernightOvernightBasisSwapRateHelper::setTermStructure(YieldTermStructure* t) { + // Do not set the relinkable handle as an observer: force + // recalculation when needed; the index is not lazy. + bool observer = false; + + ext::shared_ptr temp(t, null_deleter()); + termStructureHandle_.linkTo(temp, observer); + + if (discountHandle_.empty()) + discountRelinkableHandle_.linkTo(temp, observer); + else + discountRelinkableHandle_.linkTo(*discountHandle_, observer); + + RelativeDateRateHelper::setTermStructure(t); + } + + Real OvernightOvernightBasisSwapRateHelper::impliedQuote() const { + swap_->deepUpdate(); + return -(swap_->NPV() / swap_->legBPS(0)) * 1.0e-4; + } + + void OvernightOvernightBasisSwapRateHelper::accept(AcyclicVisitor& v) { + auto* v1 = dynamic_cast*>(&v); + if (v1 != nullptr) + v1->visit(*this); + else + RateHelper::accept(v); + } + } diff --git a/ql/experimental/termstructures/basisswapratehelpers.hpp b/ql/experimental/termstructures/basisswapratehelpers.hpp index b53fea3af9..fb9741e21d 100644 --- a/ql/experimental/termstructures/basisswapratehelpers.hpp +++ b/ql/experimental/termstructures/basisswapratehelpers.hpp @@ -19,12 +19,13 @@ */ /*! \file basisswapratehelpers.hpp - \brief ibor-ibor and ois-ibor basis swap rate helpers + \brief ibor-ibor, ois-ibor and ois-ois basis swap rate helpers */ #ifndef quantlib_basisswapratehelpers_hpp #define quantlib_basisswapratehelpers_hpp +#include #include #include #include @@ -40,6 +41,8 @@ namespace QuantLib { case bootstrapBaseCurve = false and baseIndex will need a forecast curve). In both cases, an exogenous discount curve is required. + + A payment lag can also be passed; it is applied to both legs. */ class IborIborBasisSwapRateHelper : public RelativeDateRateHelper { public: @@ -54,7 +57,8 @@ namespace QuantLib { Handle discountHandle, bool bootstrapBaseCurve, std::optional useIndexedCoupons = std::nullopt, - DateGeneration::Rule rule = DateGeneration::Backward); + DateGeneration::Rule rule = DateGeneration::Backward, + Integer paymentLag = 0); Real impliedQuote() const override; void accept(AcyclicVisitor&) override; @@ -75,6 +79,7 @@ namespace QuantLib { bool bootstrapBaseCurve_; std::optional useIndexedCoupons_; DateGeneration::Rule rule_; + Integer paymentLag_; ext::shared_ptr swap_; @@ -91,16 +96,19 @@ namespace QuantLib { curve for the overnight index; in this case, the ibor index will need to be given a forecast curve. - An exogenous discount curve can be passed; if not, the - overnight-index curve will be used. Note that when - bootstrapBaseCurve = true and no discount curve is passed, the - curve being bootstrapped is also used for discounting. + An exogenous discount curve can be passed; if not, the curve being + bootstrapped is also used for discounting. A payment lag can also be passed; it is applied to both legs. The payment frequency of the overnight leg can be overridden. It defaults to the tenor of the ibor index. The ibor leg - always pays at the tenor of its own index. + always pays at the tenor of its own index. Passing NoFrequency + creates a single overnight coupon spanning the full swap tenor. + + The averaging method and use of telescopic value dates can be + configured for the overnight leg. Telescopic value dates are only + applied to compounded coupons. */ class OvernightIborBasisSwapRateHelper : public RelativeDateRateHelper { public: @@ -117,7 +125,9 @@ namespace QuantLib { Integer paymentLag = 0, std::optional overnightPaymentFrequency = std::nullopt, std::optional useIndexedCoupons = std::nullopt, - DateGeneration::Rule rule = DateGeneration::Backward); + DateGeneration::Rule rule = DateGeneration::Backward, + RateAveraging::Type averagingMethod = RateAveraging::Compound, + bool telescopicValueDates = false); Real impliedQuote() const override; void accept(AcyclicVisitor&) override; @@ -140,10 +150,82 @@ namespace QuantLib { std::optional overnightPaymentFrequency_; std::optional useIndexedCoupons_; DateGeneration::Rule rule_; + RateAveraging::Type averagingMethod_; + bool telescopicValueDates_; + + ext::shared_ptr swap_; + + RelinkableHandle termStructureHandle_; + RelinkableHandle discountRelinkableHandle_; + }; + + + //! Rate helper for bootstrapping over overnight-overnight basis swaps + /*! The swap is assumed to pay baseIndex + basis and receive otherIndex. + The helper can be used to bootstrap the forecast curve for either + index; the other index must have an existing forecast curve. + + An exogenous discount curve can be passed. If none is passed, the + curve being bootstrapped is also used for discounting. + + Both legs share the same schedule and payment lag, but their + averaging methods can be configured independently. This allows, + for instance, an arithmetically averaged Fed Funds leg to be matched + against a compounded SOFR leg. Telescopic value dates are only + applied to compounded legs. Arithmetically averaged legs retain their + full value-date schedule so that they are priced exactly. + + Passing NoFrequency as the payment frequency creates one coupon on + each leg spanning the full swap tenor. + */ + class OvernightOvernightBasisSwapRateHelper : public RelativeDateRateHelper { + public: + OvernightOvernightBasisSwapRateHelper( + const Handle& basis, + const Period& tenor, + Natural settlementDays, + Calendar calendar, + BusinessDayConvention convention, + bool endOfMonth, + const ext::shared_ptr& baseIndex, + const ext::shared_ptr& otherIndex, + Handle discountHandle = Handle(), + bool bootstrapBaseCurve = false, + Integer paymentLag = 0, + Frequency paymentFrequency = Annual, + RateAveraging::Type baseAveragingMethod = RateAveraging::Compound, + RateAveraging::Type otherAveragingMethod = RateAveraging::Compound, + bool telescopicValueDates = false, + DateGeneration::Rule rule = DateGeneration::Backward); + + Real impliedQuote() const override; + void accept(AcyclicVisitor&) override; + // NOLINTNEXTLINE(cppcoreguidelines-noexcept-swap,performance-noexcept-swap) + ext::shared_ptr swap() const { return swap_; } + private: + void initializeDates() override; + void setTermStructure(YieldTermStructure*) override; + + Period tenor_; + Natural settlementDays_; + Calendar calendar_; + BusinessDayConvention convention_; + bool endOfMonth_; + ext::shared_ptr baseIndex_; + ext::shared_ptr otherIndex_; + Handle discountHandle_; + bool bootstrapBaseCurve_; + Integer paymentLag_; + Frequency paymentFrequency_; + RateAveraging::Type baseAveragingMethod_; + RateAveraging::Type otherAveragingMethod_; + bool telescopicValueDates_; + DateGeneration::Rule rule_; ext::shared_ptr swap_; RelinkableHandle termStructureHandle_; + RelinkableHandle discountRelinkableHandle_; }; } diff --git a/test-suite/basisswapratehelpers.cpp b/test-suite/basisswapratehelpers.cpp index 8f83aa68a7..94b15bd048 100644 --- a/test-suite/basisswapratehelpers.cpp +++ b/test-suite/basisswapratehelpers.cpp @@ -21,6 +21,7 @@ #include "toplevelfixture.hpp" #include "utilities.hpp" #include +#include #include #include #include @@ -43,7 +44,7 @@ struct BasisSwapQuote { Spread basis; }; -void testIborIborBootstrap(bool bootstrapBaseCurve) { +void testIborIborBootstrap(bool bootstrapBaseCurve, Integer paymentLag = 0) { std::vector quotes = { { 1, Years, 0.0010 }, { 2, Years, 0.0012 }, @@ -78,7 +79,8 @@ void testIborIborBootstrap(bool bootstrapBaseCurve) { auto h = ext::make_shared( Handle(ext::make_shared(q.basis)), Period(q.n, q.units), settlementDays, calendar, convention, endOfMonth, - baseIndex, otherIndex, discountCurve, bootstrapBaseCurve); + baseIndex, otherIndex, discountCurve, bootstrapBaseCurve, std::nullopt, + DateGeneration::Backward, paymentLag); helpers.push_back(h); } @@ -109,7 +111,8 @@ void testIborIborBootstrap(bool bootstrapBaseCurve) { .withRule(DateGeneration::Backward); Leg leg1 = IborLeg(s1, baseIndex) .withSpreads(q.basis) - .withNotionals(100.0); + .withNotionals(100.0) + .withPaymentLag(paymentLag); Schedule s2 = MakeSchedule() @@ -119,7 +122,8 @@ void testIborIborBootstrap(bool bootstrapBaseCurve) { .withConvention(convention) .withRule(DateGeneration::Backward); Leg leg2 = IborLeg(s2, otherIndex) - .withNotionals(100.0); + .withNotionals(100.0) + .withPaymentLag(paymentLag); Swap swap(leg1, leg2); swap.setPricingEngine(ext::make_shared(discountCurve)); @@ -135,7 +139,10 @@ void testIborIborBootstrap(bool bootstrapBaseCurve) { void testOvernightIborBootstrap(bool externalDiscountCurve, bool bootstrapBaseCurve = false, - Integer paymentLag = 0) { + Integer paymentLag = 0, + bool linkDiscountCurveAfterConstruction = false, + RateAveraging::Type averagingMethod = RateAveraging::Compound, + bool telescopicValueDates = false) { std::vector quotes = { { 1, Years, 0.0010 }, { 2, Years, 0.0012 }, @@ -155,7 +162,7 @@ void testOvernightIborBootstrap(bool externalDiscountCurve, Handle knownForecastCurve(flatRate(0.01, Actual365Fixed())); RelinkableHandle discountCurve; - if (externalDiscountCurve) + if (externalDiscountCurve && !linkDiscountCurveAfterConstruction) discountCurve.linkTo(flatRate(0.005, Actual365Fixed())); ext::shared_ptr baseIndex; @@ -174,13 +181,20 @@ void testOvernightIborBootstrap(bool externalDiscountCurve, auto h = ext::make_shared( Handle(ext::make_shared(q.basis)), Period(q.n, q.units), settlementDays, calendar, convention, endOfMonth, - baseIndex, otherIndex, discountCurve, bootstrapBaseCurve, paymentLag); + baseIndex, otherIndex, discountCurve, bootstrapBaseCurve, paymentLag, + std::nullopt, std::nullopt, DateGeneration::Backward, averagingMethod, + telescopicValueDates); helpers.push_back(h); } auto bootstrappedCurve = ext::make_shared> (0, calendar, helpers, Actual365Fixed()); + if (linkDiscountCurveAfterConstruction) { + bootstrappedCurve->discount(helpers.back()->pillarDate()); + discountCurve.linkTo(flatRate(0.005, Actual365Fixed())); + } + Date today = Settings::instance().evaluationDate(); Date spot = calendar.advance(today, settlementDays, Days); @@ -207,7 +221,10 @@ void testOvernightIborBootstrap(bool externalDiscountCurve, Leg leg1 = OvernightLeg(s, baseIndex) .withSpreads(q.basis) .withNotionals(100.0) - .withPaymentLag(paymentLag); + .withPaymentLag(paymentLag) + .withTelescopicValueDates( + telescopicValueDates && averagingMethod == RateAveraging::Compound) + .withAveragingMethod(averagingMethod); Leg leg2 = IborLeg(s, otherIndex) .withNotionals(100.0) .withPaymentLag(paymentLag); @@ -229,6 +246,115 @@ void testOvernightIborBootstrap(bool externalDiscountCurve, } } +void testOvernightOvernightBootstrap(bool externalDiscountCurve, + bool bootstrapBaseCurve, + bool linkDiscountCurveAfterConstruction = false) { + std::vector quotes = { + { 1, Years, 0.0008 }, + { 2, Years, 0.0010 }, + { 3, Years, 0.0011 }, + { 5, Years, 0.0013 }, + { 10, Years, 0.0015 }, + }; + + auto settlementDays = 2; + auto calendar = UnitedStates(UnitedStates::GovernmentBond); + auto convention = Following; + auto endOfMonth = false; + auto paymentLag = 2; + auto paymentFrequency = Quarterly; + auto baseAveragingMethod = RateAveraging::Simple; + auto otherAveragingMethod = RateAveraging::Compound; + + Handle knownForecastCurve(flatRate(0.01, Actual365Fixed())); + + RelinkableHandle discountCurve; + if (externalDiscountCurve && !linkDiscountCurveAfterConstruction) + discountCurve.linkTo(flatRate(0.005, Actual365Fixed())); + + ext::shared_ptr baseIndex, otherIndex; + if (bootstrapBaseCurve) { + baseIndex = ext::make_shared(); + otherIndex = ext::make_shared(knownForecastCurve); + } else { + baseIndex = ext::make_shared(knownForecastCurve); + otherIndex = ext::make_shared(); + } + + std::vector> basisHelpers; + std::vector> helpers; + for (auto q : quotes) { + auto h = ext::make_shared( + Handle(ext::make_shared(q.basis)), + Period(q.n, q.units), settlementDays, calendar, convention, endOfMonth, + baseIndex, otherIndex, discountCurve, bootstrapBaseCurve, paymentLag, + paymentFrequency, baseAveragingMethod, otherAveragingMethod); + basisHelpers.push_back(h); + helpers.push_back(h); + } + + auto firstBaseCoupon = ext::dynamic_pointer_cast( + basisHelpers.front()->swap()->leg(0).front()); + auto firstOtherCoupon = ext::dynamic_pointer_cast( + basisHelpers.front()->swap()->leg(1).front()); + BOOST_REQUIRE(firstBaseCoupon); + BOOST_REQUIRE(firstOtherCoupon); + BOOST_CHECK_EQUAL(firstBaseCoupon->averagingMethod(), baseAveragingMethod); + BOOST_CHECK_EQUAL(firstOtherCoupon->averagingMethod(), otherAveragingMethod); + BOOST_CHECK_EQUAL(basisHelpers.front()->swap()->leg(0).size(), 4); + BOOST_CHECK_EQUAL(basisHelpers.front()->swap()->leg(1).size(), 4); + + auto bootstrappedCurve = ext::make_shared>( + 0, calendar, helpers, Actual365Fixed()); + Handle bootstrappedCurveHandle(bootstrappedCurve); + + if (linkDiscountCurveAfterConstruction) { + bootstrappedCurve->discount(basisHelpers.back()->pillarDate()); + discountCurve.linkTo(flatRate(0.005, Actual365Fixed())); + } + + if (bootstrapBaseCurve) { + baseIndex = ext::make_shared(bootstrappedCurveHandle); + otherIndex = ext::make_shared(knownForecastCurve); + } else { + baseIndex = ext::make_shared(knownForecastCurve); + otherIndex = ext::make_shared(bootstrappedCurveHandle); + } + + Date today = Settings::instance().evaluationDate(); + Date spot = calendar.advance(today, settlementDays, Days); + + for (Size i = 0; i < quotes.size(); ++i) { + const auto& q = quotes[i]; + Date maturity = calendar.advance(spot, q.n, q.units, convention); + + Schedule schedule = + MakeSchedule().from(spot).to(maturity) + .withFrequency(paymentFrequency) + .withCalendar(calendar) + .withConvention(convention) + .withRule(DateGeneration::Forward); + + Leg baseLeg = OvernightLeg(schedule, baseIndex) + .withNotionals(100.0) + .withSpreads(q.basis) + .withPaymentLag(paymentLag) + .withAveragingMethod(baseAveragingMethod); + Leg otherLeg = OvernightLeg(schedule, otherIndex) + .withNotionals(100.0) + .withPaymentLag(paymentLag) + .withAveragingMethod(otherAveragingMethod); + + Swap swap(baseLeg, otherLeg); + swap.setPricingEngine(ext::make_shared( + externalDiscountCurve ? discountCurve : bootstrappedCurveHandle)); + + Real tolerance = 1e-8; + BOOST_CHECK_SMALL(swap.NPV(), tolerance); + BOOST_CHECK_SMALL(basisHelpers[i]->impliedQuote() - q.basis, tolerance); + } +} + BOOST_AUTO_TEST_CASE(testIborIborBaseCurveBootstrap) { BOOST_TEST_MESSAGE("Testing IBOR-IBOR basis-swap rate helpers (base curve bootstrap)..."); @@ -242,6 +368,43 @@ BOOST_AUTO_TEST_CASE(testIborIborOtherCurveBootstrap) { testIborIborBootstrap(false); } +BOOST_AUTO_TEST_CASE(testIborIborBootstrapWithPaymentLag) { + BOOST_TEST_MESSAGE("Testing IBOR-IBOR basis-swap rate helpers with payment lag..."); + + testIborIborBootstrap(true, 2); + testIborIborBootstrap(false, 2); +} + +BOOST_AUTO_TEST_CASE(testBasisSwapRateHelpersRejectNullIndexes) { + BOOST_TEST_MESSAGE("Testing basis-swap rate helpers with null indexes..."); + + auto calendar = UnitedStates(UnitedStates::GovernmentBond); + Handle curve(flatRate(0.01, Actual365Fixed())); + Handle basis(ext::make_shared(0.0010)); + auto overnightIndex = ext::make_shared(curve); + auto iborIndex = ext::make_shared(3 * Months, curve); + ext::shared_ptr nullOvernightIndex; + ext::shared_ptr nullIborIndex; + + auto makeIborIborHelper = [&](const ext::shared_ptr& baseIndex, + const ext::shared_ptr& otherIndex) { + return ext::make_shared( + basis, 2 * Years, 2, calendar, Following, false, baseIndex, otherIndex, + curve, false); + }; + auto makeOvernightIborHelper = [&](const ext::shared_ptr& baseIndex, + const ext::shared_ptr& otherIndex) { + return ext::make_shared( + basis, 2 * Years, 2, calendar, Following, false, baseIndex, otherIndex, + curve, false); + }; + + BOOST_CHECK_THROW(makeIborIborHelper(nullIborIndex, iborIndex), Error); + BOOST_CHECK_THROW(makeIborIborHelper(iborIndex, nullIborIndex), Error); + BOOST_CHECK_THROW(makeOvernightIborHelper(nullOvernightIndex, iborIndex), Error); + BOOST_CHECK_THROW(makeOvernightIborHelper(overnightIndex, nullIborIndex), Error); +} + BOOST_AUTO_TEST_CASE(testIborIborIndexedCouponOverride) { BOOST_TEST_MESSAGE("Testing the indexed-coupon override for IBOR-IBOR basis-swap rate helpers..."); @@ -299,9 +462,14 @@ BOOST_AUTO_TEST_CASE(testOvernightIborDifferentPaymentFrequencies) { OvernightIborBasisSwapRateHelper helper( Handle(ext::make_shared(0.0010)), 2 * Years, 2, calendar, Following, false, overnightIndex, iborIndex, curve, false, 0, Annual); + OvernightIborBasisSwapRateHelper zeroCouponHelper( + Handle(ext::make_shared(0.0010)), 2 * Years, 2, calendar, Following, + false, overnightIndex, iborIndex, curve, false, 0, NoFrequency); BOOST_CHECK_EQUAL(helper.swap()->leg(0).size(), 2); BOOST_CHECK_EQUAL(helper.swap()->leg(1).size(), 8); + BOOST_CHECK_EQUAL(zeroCouponHelper.swap()->leg(0).size(), 1); + BOOST_CHECK_EQUAL(zeroCouponHelper.swap()->leg(1).size(), 8); } BOOST_AUTO_TEST_CASE(testOvernightIborIndexedCouponOverride) { @@ -398,6 +566,13 @@ BOOST_AUTO_TEST_CASE(testOvernightIborBootstrapWithDiscountCurve) { testOvernightIborBootstrap(true); } +BOOST_AUTO_TEST_CASE(testOvernightIborBootstrapWithLateLinkedDiscountCurve) { + BOOST_TEST_MESSAGE("Testing overnight-IBOR basis-swap rate helpers with a " + "late-linked discount curve..."); + + testOvernightIborBootstrap(true, false, 0, true); +} + BOOST_AUTO_TEST_CASE(testOvernightIborBaseCurveBootstrapWithoutDiscountCurve) { BOOST_TEST_MESSAGE("Testing overnight-IBOR basis-swap rate helpers (base curve bootstrap)..."); @@ -420,6 +595,203 @@ BOOST_AUTO_TEST_CASE(testOvernightIborBootstrapWithPaymentLag) { testOvernightIborBootstrap(true, true, 2); } +BOOST_AUTO_TEST_CASE(testOvernightIborAveragingAndTelescopicValueDates) { + BOOST_TEST_MESSAGE( + "Testing averaging and telescopic value dates for overnight-IBOR helpers..."); + + auto calendar = UnitedStates(UnitedStates::GovernmentBond); + Handle curve(flatRate(0.01, Actual365Fixed())); + auto overnightIndex = ext::make_shared(curve); + auto iborIndex = ext::make_shared(3 * Months, curve); + Handle basis(ext::make_shared(0.0010)); + + auto makeHelper = [&](RateAveraging::Type averagingMethod, bool telescopicValueDates) { + return ext::make_shared( + basis, 2 * Years, 2, calendar, Following, false, overnightIndex, + iborIndex, curve, false, 0, Annual, std::nullopt, + DateGeneration::Backward, averagingMethod, telescopicValueDates); + }; + + auto fullSimpleHelper = makeHelper(RateAveraging::Simple, false); + auto telescopicSimpleHelper = makeHelper(RateAveraging::Simple, true); + auto fullCompoundedHelper = makeHelper(RateAveraging::Compound, false); + auto telescopicCompoundedHelper = makeHelper(RateAveraging::Compound, true); + + auto overnightCoupon = [](const OvernightIborBasisSwapRateHelper& helper) { + return ext::dynamic_pointer_cast( + helper.swap()->leg(0).front()); + }; + auto fullSimpleCoupon = overnightCoupon(*fullSimpleHelper); + auto telescopicSimpleCoupon = overnightCoupon(*telescopicSimpleHelper); + auto fullCompoundedCoupon = overnightCoupon(*fullCompoundedHelper); + auto telescopicCompoundedCoupon = overnightCoupon(*telescopicCompoundedHelper); + BOOST_REQUIRE(fullSimpleCoupon); + BOOST_REQUIRE(telescopicSimpleCoupon); + BOOST_REQUIRE(fullCompoundedCoupon); + BOOST_REQUIRE(telescopicCompoundedCoupon); + + BOOST_CHECK_EQUAL(fullSimpleCoupon->averagingMethod(), RateAveraging::Simple); + BOOST_CHECK_EQUAL(fullCompoundedCoupon->averagingMethod(), RateAveraging::Compound); + BOOST_CHECK_EQUAL(telescopicSimpleCoupon->valueDates().size(), + fullSimpleCoupon->valueDates().size()); + BOOST_CHECK_LT(telescopicCompoundedCoupon->valueDates().size(), + fullCompoundedCoupon->valueDates().size()); + + auto lastBaseCoupon = ext::dynamic_pointer_cast( + telescopicCompoundedHelper->swap()->leg(0).back()); + auto lastOtherCoupon = ext::dynamic_pointer_cast( + telescopicCompoundedHelper->swap()->leg(1).back()); + BOOST_REQUIRE(lastBaseCoupon); + BOOST_REQUIRE(lastOtherCoupon); + Date lastBaseFixingEndDate = overnightIndex->maturityDate( + overnightIndex->valueDate(lastBaseCoupon->fixingDate())); + Date lastPaymentDate = std::max( + telescopicCompoundedHelper->swap()->leg(0).back()->date(), + telescopicCompoundedHelper->swap()->leg(1).back()->date()); + Date expectedLatestRelevantDate = + std::max({telescopicCompoundedHelper->maturityDate(), lastPaymentDate, + lastBaseFixingEndDate, lastOtherCoupon->fixingEndDate()}); + BOOST_CHECK_EQUAL(telescopicCompoundedHelper->latestRelevantDate(), + expectedLatestRelevantDate); + + testOvernightIborBootstrap(true, false, 0, false, RateAveraging::Simple, true); +} + +BOOST_AUTO_TEST_CASE(testOvernightOvernightOtherCurveBootstrap) { + BOOST_TEST_MESSAGE("Testing overnight-overnight basis-swap rate helpers " + "(other curve bootstrap)..."); + + testOvernightOvernightBootstrap(false, false); + testOvernightOvernightBootstrap(true, false); +} + +BOOST_AUTO_TEST_CASE(testOvernightOvernightBaseCurveBootstrap) { + BOOST_TEST_MESSAGE("Testing overnight-overnight basis-swap rate helpers " + "(base curve bootstrap)..."); + + testOvernightOvernightBootstrap(false, true); + testOvernightOvernightBootstrap(true, true); +} + +BOOST_AUTO_TEST_CASE(testOvernightOvernightBootstrapWithLateLinkedDiscountCurve) { + BOOST_TEST_MESSAGE("Testing overnight-overnight basis-swap rate helpers with a " + "late-linked discount curve..."); + + testOvernightOvernightBootstrap(true, false, true); +} + +BOOST_AUTO_TEST_CASE(testOvernightOvernightDateGenerationRule) { + BOOST_TEST_MESSAGE( + "Testing the date-generation rule of overnight-overnight basis-swap helpers..."); + + Settings::instance().evaluationDate() = Date(27, January, 2010); + auto calendar = UnitedStates(UnitedStates::GovernmentBond); + Handle curve(flatRate(0.01, Actual365Fixed())); + auto baseIndex = ext::make_shared(curve); + auto otherIndex = ext::make_shared(curve); + Handle basis(ext::make_shared(0.0010)); + + OvernightOvernightBasisSwapRateHelper backwardHelper( + basis, 18 * Months, 2, calendar, Following, false, baseIndex, otherIndex, + curve, false, 0, Annual, RateAveraging::Compound, RateAveraging::Compound, + false, DateGeneration::Backward); + OvernightOvernightBasisSwapRateHelper defaultHelper( + basis, 18 * Months, 2, calendar, Following, false, baseIndex, otherIndex, + curve); + OvernightOvernightBasisSwapRateHelper forwardHelper( + basis, 18 * Months, 2, calendar, Following, false, baseIndex, otherIndex, + curve, false, 0, Annual, RateAveraging::Compound, RateAveraging::Compound, + false, DateGeneration::Forward); + + auto firstAccrualEnd = [](const OvernightOvernightBasisSwapRateHelper& helper, + Size leg) { + return ext::dynamic_pointer_cast(helper.swap()->leg(leg).front()) + ->accrualEndDate(); + }; + Date spot = calendar.advance(Settings::instance().evaluationDate(), 2, Days); + for (Size i = 0; i < 2; ++i) { + BOOST_CHECK_EQUAL(backwardHelper.swap()->leg(i).size(), 2); + BOOST_CHECK_EQUAL(defaultHelper.swap()->leg(i).size(), 2); + BOOST_CHECK_EQUAL(forwardHelper.swap()->leg(i).size(), 2); + BOOST_CHECK_EQUAL(firstAccrualEnd(backwardHelper, i), + calendar.adjust(spot + 6 * Months, Following)); + BOOST_CHECK_EQUAL(firstAccrualEnd(defaultHelper, i), + firstAccrualEnd(backwardHelper, i)); + BOOST_CHECK_EQUAL(firstAccrualEnd(forwardHelper, i), + calendar.adjust(spot + 1 * Years, Following)); + } + + Date expectedMaturity = calendar.adjust(spot + 18 * Months, Following); + BOOST_CHECK_EQUAL(backwardHelper.maturityDate(), expectedMaturity); + BOOST_CHECK_EQUAL(defaultHelper.maturityDate(), expectedMaturity); + BOOST_CHECK_EQUAL(forwardHelper.maturityDate(), expectedMaturity); +} + +BOOST_AUTO_TEST_CASE(testOvernightOvernightNoFrequency) { + BOOST_TEST_MESSAGE( + "Testing zero-coupon overnight-overnight basis-swap helpers..."); + + auto calendar = UnitedStates(UnitedStates::GovernmentBond); + Handle curve(flatRate(0.01, Actual365Fixed())); + auto baseIndex = ext::make_shared(curve); + auto otherIndex = ext::make_shared(curve); + Handle basis(ext::make_shared(0.0010)); + + OvernightOvernightBasisSwapRateHelper helper( + basis, 5 * Years, 2, calendar, Following, false, baseIndex, otherIndex, + curve, false, 0, NoFrequency); + + BOOST_CHECK_EQUAL(helper.swap()->leg(0).size(), 1); + BOOST_CHECK_EQUAL(helper.swap()->leg(1).size(), 1); + auto baseCoupon = ext::dynamic_pointer_cast(helper.swap()->leg(0).front()); + auto otherCoupon = ext::dynamic_pointer_cast(helper.swap()->leg(1).front()); + BOOST_REQUIRE(baseCoupon); + BOOST_REQUIRE(otherCoupon); + BOOST_CHECK_EQUAL(baseCoupon->accrualStartDate(), helper.earliestDate()); + BOOST_CHECK_EQUAL(otherCoupon->accrualStartDate(), helper.earliestDate()); + BOOST_CHECK_EQUAL(baseCoupon->accrualEndDate(), helper.maturityDate()); + BOOST_CHECK_EQUAL(otherCoupon->accrualEndDate(), helper.maturityDate()); +} + +BOOST_AUTO_TEST_CASE(testOvernightOvernightTelescopicValueDatesWithSimpleAveraging) { + BOOST_TEST_MESSAGE("Testing telescopic value dates with simple overnight averaging..."); + + auto calendar = UnitedStates(UnitedStates::GovernmentBond); + Handle forecastCurve(flatRate(0.01, Actual365Fixed())); + auto baseIndex = ext::make_shared(forecastCurve); + auto otherIndex = ext::make_shared(); + + auto makeHelper = [&](bool telescopicValueDates) { + return ext::make_shared( + Handle(ext::make_shared(0.0010)), 1 * Years, 2, + calendar, Following, false, baseIndex, otherIndex, + Handle(), false, 0, Annual, RateAveraging::Simple, + RateAveraging::Compound, telescopicValueDates); + }; + + auto fullScheduleHelper = makeHelper(false); + auto telescopicHelper = makeHelper(true); + + auto fullSimpleCoupon = ext::dynamic_pointer_cast( + fullScheduleHelper->swap()->leg(0).front()); + auto telescopicSimpleCoupon = ext::dynamic_pointer_cast( + telescopicHelper->swap()->leg(0).front()); + auto fullCompoundedCoupon = ext::dynamic_pointer_cast( + fullScheduleHelper->swap()->leg(1).front()); + auto telescopicCompoundedCoupon = ext::dynamic_pointer_cast( + telescopicHelper->swap()->leg(1).front()); + + BOOST_REQUIRE(fullSimpleCoupon); + BOOST_REQUIRE(telescopicSimpleCoupon); + BOOST_REQUIRE(fullCompoundedCoupon); + BOOST_REQUIRE(telescopicCompoundedCoupon); + BOOST_CHECK_EQUAL(telescopicSimpleCoupon->valueDates().size(), + fullSimpleCoupon->valueDates().size()); + BOOST_CHECK_LT(telescopicCompoundedCoupon->valueDates().size(), + fullCompoundedCoupon->valueDates().size()); + BOOST_CHECK_SMALL(telescopicSimpleCoupon->rate() - fullSimpleCoupon->rate(), 1.0e-14); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()