From 2187fb2c478449bb925a7f23163c255259a5d101 Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Mon, 14 Oct 2024 13:37:28 +0200 Subject: [PATCH] Add new constructor and new leg builder to sub-periods coupon --- ql/cashflows/subperiodcoupon.cpp | 192 +++++++++++++++++++++++++++++++ ql/cashflows/subperiodcoupon.hpp | 98 +++++++++++++--- test-suite/subperiodcoupons.cpp | 26 ++++- 3 files changed, 299 insertions(+), 17 deletions(-) diff --git a/ql/cashflows/subperiodcoupon.cpp b/ql/cashflows/subperiodcoupon.cpp index 8efdcfca411..08ab52b5f4a 100644 --- a/ql/cashflows/subperiodcoupon.cpp +++ b/ql/cashflows/subperiodcoupon.cpp @@ -27,6 +27,43 @@ namespace QuantLib { + SubPeriodsCoupon::SubPeriodsCoupon(const Date& paymentDate, + Real nominal, + const Schedule& resetSchedule, + Natural fixingDays, + const ext::shared_ptr& index, + Real gearing, + Rate couponSpread, + Rate rateSpread, + const Date& refPeriodStart, + const Date& refPeriodEnd, + const DayCounter& dayCounter, + const Date& exCouponDate) + : FloatingRateCoupon(paymentDate, nominal, + resetSchedule.front(), resetSchedule.back(), + fixingDays, index, gearing, couponSpread, + refPeriodStart, refPeriodEnd, dayCounter, + false, exCouponDate), + rateSpread_(rateSpread) { + valueDates_ = resetSchedule.dates(); + + // fixing dates + n_ = valueDates_.size() - 1; + if (fixingDays_ == 0) { + fixingDates_ = std::vector(valueDates_.begin(), valueDates_.end() - 1); + } else { + fixingDates_.resize(n_); + for (Size i = 0; i < n_; ++i) + fixingDates_[i] = fixingDate(valueDates_[i]); + } + + // accrual times of sub-periods + dt_.resize(n_); + const DayCounter& dc = index->dayCounter(); + for (Size i = 0; i < n_; ++i) + dt_[i] = dc.yearFraction(valueDates_[i], valueDates_[i + 1]); + } + SubPeriodsCoupon::SubPeriodsCoupon(const Date& paymentDate, Real nominal, const Date& startDate, @@ -155,6 +192,160 @@ namespace QuantLib { return coupon_->gearing() * rate + coupon_->spread(); } + + + MultipleResetsLeg::MultipleResetsLeg(Schedule schedule, + ext::shared_ptr index, + Size resetsPerCoupon) + : schedule_(std::move(schedule)), index_(std::move(index)), resetsPerCoupon_(resetsPerCoupon), + paymentCalendar_(schedule_.calendar()) { + QL_REQUIRE(index_, "no index provided"); + QL_REQUIRE(!schedule_.empty(), "empty schedule provided"); + QL_REQUIRE((schedule_.size() - 1) % resetsPerCoupon_ == 0, + "number of resets per coupon does not divide exactly number of periods in schedule"); + } + + MultipleResetsLeg& MultipleResetsLeg::withNotionals(Real notional) { + notionals_ = std::vector(1, notional); + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withNotionals(const std::vector& notionals) { + notionals_ = notionals; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withPaymentDayCounter(const DayCounter& dc) { + paymentDayCounter_ = dc; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withPaymentAdjustment(BusinessDayConvention convention) { + paymentAdjustment_ = convention; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withPaymentCalendar(const Calendar& cal) { + paymentCalendar_ = cal; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withPaymentLag(Integer lag) { + paymentLag_ = lag; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withFixingDays(Natural fixingDays) { + fixingDays_ = std::vector(1, fixingDays); + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withFixingDays(const std::vector& fixingDays) { + fixingDays_ = fixingDays; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withGearings(Real gearing) { + gearings_ = std::vector(1, gearing); + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withGearings(const std::vector& gearings) { + gearings_ = gearings; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withCouponSpreads(Spread spread) { + couponSpreads_ = std::vector(1, spread); + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withCouponSpreads(const std::vector& spreads) { + couponSpreads_ = spreads; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withRateSpreads(Spread spread) { + rateSpreads_ = std::vector(1, spread); + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withRateSpreads(const std::vector& spreads) { + rateSpreads_ = spreads; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withAveragingMethod(RateAveraging::Type averagingMethod) { + averagingMethod_ = averagingMethod; + return *this; + } + + MultipleResetsLeg& MultipleResetsLeg::withExCouponPeriod(const Period& period, + const Calendar& cal, + BusinessDayConvention convention, + bool endOfMonth) { + exCouponPeriod_ = period; + exCouponCalendar_ = cal; + exCouponAdjustment_ = convention; + exCouponEndOfMonth_ = endOfMonth; + return *this; + } + + MultipleResetsLeg::operator Leg() const { + Leg cashflows; + Calendar calendar = schedule_.calendar(); + + Size n = (schedule_.size() - 1) / resetsPerCoupon_; + QL_REQUIRE(!notionals_.empty(), "no notional given"); + QL_REQUIRE(notionals_.size() <= n, + "too many nominals (" << notionals_.size() << "), only " << n << " required"); + QL_REQUIRE(gearings_.size() <= n, + "too many gearings (" << gearings_.size() << "), only " << n << " required"); + QL_REQUIRE(couponSpreads_.size() <= n, + "too many coupon spreads (" << couponSpreads_.size() << "), only " << n << " required"); + QL_REQUIRE(rateSpreads_.size() <= n, + "too many rate spreads (" << rateSpreads_.size() << "), only " << n << " required"); + QL_REQUIRE(fixingDays_.size() <= n, + "too many fixing days (" << fixingDays_.size() << "), only " << n << " required"); + + for (Size i = 0; i < n; ++i) { + Date start = schedule_.date(i * resetsPerCoupon_); + Date end = schedule_.date((i + 1) * resetsPerCoupon_); + auto subSchedule = schedule_.after(start).until(end); + Date paymentDate = paymentCalendar_.advance(end, paymentLag_, Days, paymentAdjustment_); + Date exCouponDate; + if (exCouponPeriod_ != Period()) { + if (exCouponCalendar_.empty()) { + exCouponDate = calendar.advance(paymentDate, -exCouponPeriod_, + exCouponAdjustment_, exCouponEndOfMonth_); + } else { + exCouponDate = exCouponCalendar_.advance( + paymentDate, -exCouponPeriod_, exCouponAdjustment_, exCouponEndOfMonth_); + } + } + + cashflows.push_back(ext::make_shared( + paymentDate, detail::get(notionals_, i, notionals_.back()), subSchedule, + detail::get(fixingDays_, i, index_->fixingDays()), index_, + detail::get(gearings_, i, 1.0), detail::get(couponSpreads_, i, 0.0), + detail::get(rateSpreads_, i, 0.0), start, end, paymentDayCounter_, + exCouponDate)); + } + + switch (averagingMethod_) { + case RateAveraging::Simple: + setCouponPricer(cashflows, ext::make_shared()); + break; + case RateAveraging::Compound: + setCouponPricer(cashflows, ext::make_shared()); + break; + default: + QL_FAIL("unknown compounding convention (" << Integer(averagingMethod_) << ")"); + } + return cashflows; + } + + SubPeriodsLeg::SubPeriodsLeg(Schedule schedule, ext::shared_ptr i) : schedule_(std::move(schedule)), index_(std::move(i)), paymentCalendar_(schedule_.calendar()) { QL_REQUIRE(index_, "no index provided"); @@ -305,4 +496,5 @@ namespace QuantLib { } return cashflows; } + } diff --git a/ql/cashflows/subperiodcoupon.hpp b/ql/cashflows/subperiodcoupon.hpp index 955cd14ac62..cc158a765cc 100644 --- a/ql/cashflows/subperiodcoupon.hpp +++ b/ql/cashflows/subperiodcoupon.hpp @@ -35,18 +35,35 @@ namespace QuantLib { class IborIndex; - //! sub-periods coupon - /*! %Coupon paying the interest, depending on the averaging convention, - due to possible multiple fixing resets in one accrual period. + //! multiple-reset coupon + /*! %Coupon paying a rate calculated by compounding or averaging + multiple fixings during its accrual period. */ class SubPeriodsCoupon: public FloatingRateCoupon { public: - // The index object passed in has a tenor significantly less than the - // start/end dates. - // Thus endDate-startDate may equal 3M - // The Tenor used within the index object should be 1M for - // averaging/compounding across three coupons within the - // coupon period. + /*! \param resetSchedule the schedule for the multiple resets. The first and last + dates are also the start and end dates of the coupon. + Each period specified by the schedule is the underlying + period for one fixing; the corresponding fixing date is + the passed number of fixing days before the start of + the period. + \param couponSpread an optional spread added to the final coupon rate. + \param rateSpread an optional spread added to each of the underlying fixings. + \param gearing an optional multiplier for the final coupon rate. + */ + SubPeriodsCoupon(const Date& paymentDate, + Real nominal, + const Schedule& resetSchedule, + Natural fixingDays, + const ext::shared_ptr& index, + Real gearing = 1.0, + Rate couponSpread = 0.0, + Rate rateSpread = 0.0, + const Date& refPeriodStart = Date(), + const Date& refPeriodEnd = Date(), + const DayCounter& dayCounter = DayCounter(), + const Date& exCouponDate = Date()); + SubPeriodsCoupon(const Date& paymentDate, Real nominal, const Date& startDate, @@ -54,11 +71,8 @@ namespace QuantLib { Natural fixingDays, const ext::shared_ptr& index, Real gearing = 1.0, - Rate couponSpread = 0.0, // Spread added to the computed - // averaging/compounding rate. - Rate rateSpread = 0.0, // Spread to be added onto each - // fixing within the - // averaging/compounding calculation + Rate couponSpread = 0.0, + Rate rateSpread = 0.0, const Date& refPeriodStart = Date(), const Date& refPeriodEnd = Date(), const DayCounter& dayCounter = DayCounter(), @@ -117,7 +131,61 @@ namespace QuantLib { Real swapletRate() const override; }; - //! helper class building a sequence of overnight coupons + + //! helper class building a sequence of multiple-reset coupons + class MultipleResetsLeg { + public: + /*! \param fullResetSchedule the full schedule specifying reset periods for all coupons. + \param index the index whose fixings will be used; it should have the + same tenor as the resets. + \param resetsPerCoupon the number of resets for each coupon; the number of periods + in the schedule should be divided exactly by this number. + */ + MultipleResetsLeg(Schedule fullResetSchedule, + ext::shared_ptr index, + Size resetsPerCoupon); + MultipleResetsLeg& withNotionals(Real notional); + MultipleResetsLeg& withNotionals(const std::vector& notionals); + MultipleResetsLeg& withPaymentDayCounter(const DayCounter&); + MultipleResetsLeg& withPaymentAdjustment(BusinessDayConvention); + MultipleResetsLeg& withPaymentCalendar(const Calendar&); + MultipleResetsLeg& withPaymentLag(Integer lag); + MultipleResetsLeg& withFixingDays(Natural fixingDays); + MultipleResetsLeg& withFixingDays(const std::vector& fixingDays); + MultipleResetsLeg& withGearings(Real gearing); + MultipleResetsLeg& withGearings(const std::vector& gearings); + MultipleResetsLeg& withCouponSpreads(Spread spread); + MultipleResetsLeg& withCouponSpreads(const std::vector& spreads); + MultipleResetsLeg& withRateSpreads(Spread spread); + MultipleResetsLeg& withRateSpreads(const std::vector& spreads); + MultipleResetsLeg& withExCouponPeriod(const Period&, + const Calendar&, + BusinessDayConvention, + bool endOfMonth = false); + MultipleResetsLeg& withAveragingMethod(RateAveraging::Type averagingMethod); + operator Leg() const; + + private: + Schedule schedule_; + ext::shared_ptr index_; + Size resetsPerCoupon_; + std::vector notionals_; + DayCounter paymentDayCounter_; + Calendar paymentCalendar_; + BusinessDayConvention paymentAdjustment_ = Following; + Integer paymentLag_ = 0; + std::vector fixingDays_; + std::vector gearings_; + std::vector couponSpreads_; + std::vector rateSpreads_; + RateAveraging::Type averagingMethod_ = RateAveraging::Compound; + Period exCouponPeriod_; + Calendar exCouponCalendar_; + BusinessDayConvention exCouponAdjustment_ = Unadjusted; + bool exCouponEndOfMonth_ = false; + }; + + class SubPeriodsLeg { public: SubPeriodsLeg(Schedule schedule, ext::shared_ptr index); diff --git a/test-suite/subperiodcoupons.cpp b/test-suite/subperiodcoupons.cpp index a75927b8eeb..2df0b6c36eb 100644 --- a/test-suite/subperiodcoupons.cpp +++ b/test-suite/subperiodcoupons.cpp @@ -341,8 +341,7 @@ BOOST_AUTO_TEST_CASE(testExCouponCashFlow) { } BOOST_AUTO_TEST_CASE(testSubPeriodsLegCashFlows) { - BOOST_TEST_MESSAGE( - "Testing sub-periods leg replication..."); + BOOST_TEST_MESSAGE("Testing sub-periods leg replication..."); testSubPeriodsLegReplication(RateAveraging::Compound); testSubPeriodsLegReplication(RateAveraging::Simple); @@ -381,6 +380,29 @@ BOOST_AUTO_TEST_CASE(testSubPeriodsLegConsistencyChecks) { Error); } +BOOST_AUTO_TEST_CASE(testSubPeriodsLegRegression) { + BOOST_TEST_MESSAGE("Testing number of fixing dates in sub-periods coupons..."); + + Schedule schedule = MakeSchedule() + .from({1, August, 2024}) + .to({1, August, 2025}) + .withFrequency(Monthly) + .withCalendar(TARGET()); + + Size resetsPerCoupon = 3; + Leg leg = MultipleResetsLeg(schedule, ext::make_shared(), resetsPerCoupon) + .withNotionals(100.0) + .withAveragingMethod(RateAveraging::Compound); + + for (const auto& cf : leg) { + auto c = ext::dynamic_pointer_cast(cf); + if (c->fixingDates().size() != 3) + BOOST_ERROR("Unexpected number of fixing dates (" << c->fixingDates().size() << ") " + "in coupon paying on " << c->date()); + } +} + + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()