From 93e3a241cededb251d81574cc7163fead74f37ed Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Thu, 7 Aug 2025 12:18:03 +0200 Subject: [PATCH 01/32] First alignment ORE/QL ON coupon and ON coupon pricer --- ql/cashflows/overnightindexedcoupon.cpp | 337 +++++++++++++++++- ql/cashflows/overnightindexedcoupon.hpp | 110 +++++- ql/cashflows/overnightindexedcouponpricer.cpp | 92 ++++- ql/cashflows/overnightindexedcouponpricer.hpp | 23 ++ 4 files changed, 538 insertions(+), 24 deletions(-) diff --git a/ql/cashflows/overnightindexedcoupon.cpp b/ql/cashflows/overnightindexedcoupon.cpp index 0382671ad1d..74e1d8152d2 100644 --- a/ql/cashflows/overnightindexedcoupon.cpp +++ b/ql/cashflows/overnightindexedcoupon.cpp @@ -22,9 +22,12 @@ */ #include +#include #include #include +#include #include +#include #include #include #include @@ -57,7 +60,10 @@ namespace QuantLib { RateAveraging::Type averagingMethod, Natural lookbackDays, Natural lockoutDays, - bool applyObservationShift) + bool applyObservationShift, + bool includeSpread, + const Date& rateComputationStartDate, + const Date& rateComputationEndDate) : FloatingRateCoupon(paymentDate, nominal, startDate, endDate, lookbackDays, overnightIndex, @@ -65,7 +71,10 @@ namespace QuantLib { refPeriodStart, refPeriodEnd, dayCounter, false), averagingMethod_(averagingMethod), lockoutDays_(lockoutDays), - applyObservationShift_(applyObservationShift) { + applyObservationShift_(applyObservationShift), + includeSpread_(includeSpread), + rateComputationStartDate_(rateComputationStartDate), + rateComputationEndDate_(rateComputationEndDate) { // value dates Date tmpEndDate = endDate; @@ -222,7 +231,170 @@ namespace QuantLib { } } - OvernightLeg::OvernightLeg(Schedule schedule, ext::shared_ptr i) + Real OvernightIndexedCoupon::effectiveSpread() const { + if (!includeSpread_) + return spread(); + //FIXME: handle two pricers case + auto p = ext::dynamic_pointer_cast(pricer()); + QL_REQUIRE(p, "OvernightIndexedCoupon::effectiveSpread(): expected OvernightIndexedCouponPricer"); + p->initialize(*this); + return p->effectiveSpread(); + } + + Real OvernightIndexedCoupon::effectiveIndexFixing() const { + auto p = ext::dynamic_pointer_cast(pricer()); + //FIXME: handle two pricers case + QL_REQUIRE(p, "OvernightIndexedCoupon::effectiveSpread(): expected OvernightIndexedCouponPricer"); + p->initialize(*this); + return p->effectiveIndexFixing(); + } + + // CappedFlooredOvernightIndexedCoupon implementation + + CappedFlooredOvernightIndexedCoupon::CappedFlooredOvernightIndexedCoupon( + const ext::shared_ptr& underlying, Real cap, Real floor, bool nakedOption, + bool localCapFloor) + : FloatingRateCoupon(underlying->date(), underlying->nominal(), underlying->accrualStartDate(), + underlying->accrualEndDate(), underlying->fixingDays(), underlying->index(), + underlying->gearing(), underlying->spread(), underlying->referencePeriodStart(), + underlying->referencePeriodEnd(), underlying->dayCounter(), false), + underlying_(underlying), nakedOption_(nakedOption), localCapFloor_(localCapFloor) { + + QL_REQUIRE(!underlying_->includeSpread() || close_enough(underlying_->gearing(), 1.0), + "CappedFlooredOvernightIndexedCoupon: if include spread = true, only a gearing 1.0 is allowed - scale " + "the notional in this case instead."); + + if (!localCapFloor) { + if (gearing_ > 0.0) { + cap_ = cap; + floor_ = floor; + } else { + cap_ = floor; + floor_ = cap; + } + } else { + cap_ = cap; + floor_ = floor; + } + if (cap_ != Null() && floor_ != Null()) { + QL_REQUIRE(cap_ >= floor, "cap level (" << cap_ << ") less than floor level (" << floor_ << ")"); + } + registerWith(underlying_); + if (nakedOption_) + underlying_->alwaysForwardNotifications(); + } + + void CappedFlooredOvernightIndexedCoupon::alwaysForwardNotifications() { + LazyObject::alwaysForwardNotifications(); + underlying_->alwaysForwardNotifications(); + } + + void CappedFlooredOvernightIndexedCoupon::deepUpdate() { + update(); + underlying_->deepUpdate(); + } + + void CappedFlooredOvernightIndexedCoupon::performCalculations() const { + QL_REQUIRE(underlying_->pricer(), "pricer not set"); + Rate swapletRate = nakedOption_ ? 0.0 : underlying_->rate(); + if (floor_ != Null() || cap_ != Null()) + pricer()->initialize(*this); + Rate floorletRate = 0.; + if (floor_ != Null()) + floorletRate = pricer()->floorletRate(effectiveFloor()); + Rate capletRate = 0.; + if (cap_ != Null()) + capletRate = (nakedOption_ && floor_ == Null() ? -1.0 : 1.0) * pricer()->capletRate(effectiveCap()); + rate_ = swapletRate + floorletRate - capletRate; + auto p = QuantLib::ext::dynamic_pointer_cast(pricer()); + QL_REQUIRE(p, "CappedFlooredOvernightIndexedCoupon::performCalculations(): internal error, could not cast to " + "CappedFlooredOvernightIndexedCouponPricer"); + effectiveCapletVolatility_ = p->effectiveCapletVolatility(); + effectiveFloorletVolatility_ = p->effectiveFloorletVolatility(); + } + + Rate CappedFlooredOvernightIndexedCoupon::cap() const { return gearing_ > 0.0 ? cap_ : floor_; } + + Rate CappedFlooredOvernightIndexedCoupon::floor() const { return gearing_ > 0.0 ? floor_ : cap_; } + + Rate CappedFlooredOvernightIndexedCoupon::rate() const { + calculate(); + return rate_; + } + + Rate CappedFlooredOvernightIndexedCoupon::convexityAdjustment() const { return underlying_->convexityAdjustment(); } + + Rate CappedFlooredOvernightIndexedCoupon::effectiveCap() const { + if (cap_ == Null()) + return Null(); + /* We have four cases dependent on localCapFloor_ and includeSpread. Notation in the formulas: + g gearing, + s spread, + A coupon amount, + f_i daily fixings, + \tau_i daily accrual fractions, + \tau coupon accrual fraction, + C cap rate + F floor rate + */ + if (localCapFloor_) { + if (underlying_->includeSpread()) { + // A = g \cdot \frac{\prod (1 + \tau_i \min ( \max ( f_i + s , F), C)) - 1}{\tau} + return cap_ - underlying_->spread(); + } else { + // A = g \cdot \frac{\prod (1 + \tau_i \min ( \max ( f_i , F), C)) - 1}{\tau} + s + return cap_; + } + } else { + if (underlying_->includeSpread()) { + // A = \min \left( \max \left( g \cdot \frac{\prod (1 + \tau_i(f_i + s)) - 1}{\tau}, F \right), C \right) + return (cap_ / gearing() - underlying_->effectiveSpread()); + } else { + // A = \min \left( \max \left( g \cdot \frac{\prod (1 + \tau_i f_i) - 1}{\tau} + s, F \right), C \right) + return (cap_ - underlying_->effectiveSpread()) / gearing(); + } + } + } + + Rate CappedFlooredOvernightIndexedCoupon::effectiveFloor() const { + if (floor_ == Null()) + return Null(); + if (localCapFloor_) { + if (underlying_->includeSpread()) { + return floor_ - underlying_->spread(); + } else { + return floor_; + } + } else { + if (underlying_->includeSpread()) { + return (floor_ - underlying_->effectiveSpread()); + } else { + return (floor_ - underlying_->effectiveSpread()) / gearing(); + } + } + } + + Real CappedFlooredOvernightIndexedCoupon::effectiveCapletVolatility() const { + calculate(); + return effectiveCapletVolatility_; + } + + Real CappedFlooredOvernightIndexedCoupon::effectiveFloorletVolatility() const { + calculate(); + return effectiveFloorletVolatility_; + } + + void CappedFlooredOvernightIndexedCoupon::accept(AcyclicVisitor& v) { + Visitor* v1 = dynamic_cast*>(&v); + if (v1 != 0) + v1->visit(*this); + else + FloatingRateCoupon::accept(v); + } + + // OvernightLeg implementation + + OvernightLeg::OvernightLeg(const Schedule& schedule, const ext::shared_ptr& i) : schedule_(std::move(schedule)), overnightIndex_(std::move(i)), paymentCalendar_(schedule_.calendar()) { QL_REQUIRE(overnightIndex_, "no index provided"); } @@ -301,6 +473,73 @@ namespace QuantLib { return *this; } + OvernightLeg& OvernightLeg::includeSpread(bool includeSpread) { + includeSpread_ = includeSpread; + return *this; + } + + OvernightLeg& OvernightLeg::withCaps(Rate cap) { + caps_ = std::vector(1, cap); + return *this; + } + + OvernightLeg& OvernightLeg::withCaps(const std::vector& caps) { + caps_ = caps; + return *this; + } + + OvernightLeg& OvernightLeg::withFloors(Rate floor) { + floors_ = std::vector(1, floor); + return *this; + } + + OvernightLeg& OvernightLeg::withFloors(const std::vector& floors) { + floors_ = floors; + return *this; + } + + OvernightLeg& OvernightLeg::withNakedOption(const bool nakedOption) { + nakedOption_ = nakedOption; + return *this; + } + + OvernightLeg& OvernightLeg::withLocalCapFloor(const bool localCapFloor) { + localCapFloor_ = localCapFloor; + return *this; + } + + OvernightLeg& OvernightLeg::withInArrears(const bool inArrears) { + inArrears_ = inArrears; + return *this; + } + + OvernightLeg& OvernightLeg::withLastRecentPeriod(const ext::optional& lastRecentPeriod) { + lastRecentPeriod_ = lastRecentPeriod; + return *this; + } + + OvernightLeg& OvernightLeg::withLastRecentPeriodCalendar(const Calendar& lastRecentPeriodCalendar) { + lastRecentPeriodCalendar_ = lastRecentPeriodCalendar; + return *this; + } + + OvernightLeg& OvernightLeg::withPaymentDates(const std::vector& paymentDates) { + paymentDates_ = paymentDates; + return *this; + } + + /* + OvernightLeg& OvernightLeg::withOvernightIndexedCouponPricer(const ext::shared_ptr& couponPricer) { + couponPricer_ = couponPricer; + return *this; + } + + OvernightLeg& OvernightLeg::withCapFlooredOvernightIndexedCouponPricer( + const QuantLib::ext::shared_ptr& couponPricer) { + capFlooredCouponPricer_ = couponPricer; + return *this; + }*/ + OvernightLeg::operator Leg() const { QL_REQUIRE(!notionals_.empty(), "no notional given"); @@ -309,16 +548,40 @@ namespace QuantLib { // the following is not always correct Calendar calendar = schedule_.calendar(); + Calendar paymentCalendar = paymentCalendar_; + + if (calendar.empty()) + calendar = paymentCalendar; + if (calendar.empty()) + calendar = WeekendsOnly(); + if (paymentCalendar.empty()) + paymentCalendar = calendar; Date refStart, start, refEnd, end; Date paymentDate; Size n = schedule_.size()-1; + + // Initial consistency checks + if (!paymentDates_.empty()) { + QL_REQUIRE(paymentDates_.size() == n, "Expected the number of explicit payment dates (" + << paymentDates_.size() + << ") to equal the number of calculation periods (" + << n << ")"); + } + for (Size i=0; i( - paymentDate, detail::get(notionals_, i, notionals_.back()), start, end, - overnightIndex_, detail::get(gearings_, i, 1.0), detail::get(spreads_, i, 0.0), - refStart, refEnd, paymentDayCounter_, telescopicValueDates_, averagingMethod_, - lookbackDays_, lockoutDays_, applyObservationShift_); + // Determine the rate computation start and end date as + // - the coupon start and end date, if in arrears, and + // - the previous coupon start and end date, if in advance. + // In addition, adjust the start date, if a last recent period is given. + + Date rateComputationStartDate, rateComputationEndDate; + if (inArrears_) { + // in arrears fixing (i.e. the "classic" case) + rateComputationStartDate = start; + rateComputationEndDate = end; + } else { + // handle in advance fixing + if (i > 0) { + // if there is a previous period, we take that + rateComputationStartDate = schedule_.date(i - 1); + rateComputationEndDate = schedule_.date(i); + } else { + // otherwise we construct the previous period + rateComputationEndDate = start; + if (schedule_.hasTenor() && schedule_.tenor() != 0 * Days) + rateComputationStartDate = calendar.adjust(start - schedule_.tenor(), Preceding); + else + rateComputationStartDate = calendar.adjust(start - (end - start), Preceding); + } + } - cashflows.push_back(overnightIndexedCoupon); + if (lastRecentPeriod_) { + rateComputationStartDate = (lastRecentPeriodCalendar_.empty() ? calendar : lastRecentPeriodCalendar_) + .advance(rateComputationEndDate, -*lastRecentPeriod_); + } + + // build coupon + + if (close_enough(detail::get(gearings_, i, 1.0), 0.0)) { + // fixed coupon + cashflows.push_back(QuantLib::ext::make_shared( + paymentDate, detail::get(notionals_, i, 1.0), detail::effectiveFixedRate(spreads_, caps_, floors_, i), + paymentDayCounter_, start, end, refStart, refEnd)); + } else { + // floating coupon + auto cpn = ext::make_shared( + paymentDate, detail::get(notionals_, i, 1.0), start, end, overnightIndex_, + detail::get(gearings_, i, 1.0), detail::get(spreads_, i, 0.0), refStart, refEnd, paymentDayCounter_, + telescopicValueDates_, averagingMethod_, lookbackDays_, lockoutDays_, applyObservationShift_, + includeSpread_, rateComputationStartDate, rateComputationEndDate); + if (couponPricer_) { + cpn->setPricer(couponPricer_); + } + Real cap = detail::get(caps_, i, Null()); + Real floor = detail::get(floors_, i, Null()); + if (cap == Null() && floor == Null()) { + cashflows.push_back(cpn); + } else { + auto cfCpn = ext::make_shared(cpn, cap, floor, nakedOption_, + localCapFloor_); + if (capFlooredCouponPricer_) { + cfCpn->setPricer(capFlooredCouponPricer_); + } + cashflows.push_back(cfCpn); + } + } } return cashflows; } diff --git a/ql/cashflows/overnightindexedcoupon.hpp b/ql/cashflows/overnightindexedcoupon.hpp index 3adb50262d1..e04ec46af6e 100644 --- a/ql/cashflows/overnightindexedcoupon.hpp +++ b/ql/cashflows/overnightindexedcoupon.hpp @@ -6,6 +6,7 @@ Copyright (C) 2014 Peter Caspers Copyright (C) 2017 Joseph Jeisman Copyright (C) 2017 Fabrice Lecuyer + Copyright (C) 2025 Paolo D'Elia This file is part of QuantLib, a free-software/open-source library for financial quantitative analysts and developers - http://quantlib.org/ @@ -34,8 +35,12 @@ #include #include + namespace QuantLib { + class CompoundingOvernightIndexedCouponPricer; + class CappedFlooredOvernightIndexedCouponPricer; + //! overnight coupon /*! %Coupon paying the interest, depending on the averaging convention, due to daily overnight fixings. @@ -64,7 +69,10 @@ namespace QuantLib { RateAveraging::Type averagingMethod = RateAveraging::Compound, Natural lookbackDays = Null(), Natural lockoutDays = 0, - bool applyObservationShift = false); + bool applyObservationShift = false, + bool includeSpread = false, + const Date& rateComputationStartDate = Date(), + const Date& rateComputationEndDate = Date()); //! \name Inspectors //@{ //! fixing dates for the rates to be compounded @@ -83,6 +91,19 @@ namespace QuantLib { Natural lockoutDays() const { return lockoutDays_; } //! apply observation shift bool applyObservationShift() const { return applyObservationShift_; } + //! include spread in compounding? + bool includeSpread() const { return includeSpread_; } + /*! effectiveSpread and effectiveIndexFixing are set such that + coupon amount = notional * accrualPeriod * ( gearing * effectiveIndexFixing + effectiveSpread ) + notice that + - gearing = 1 is required if includeSpread = true + - effectiveSpread = spread() if includeSpread = false */ + Real effectiveSpread() const; + Real effectiveIndexFixing() const; + //! rate computation start date + const Date& rateComputationStartDate() const { return rateComputationStartDate_; } + //! rate computation end date + const Date& rateComputationEndDate() const { return rateComputationEndDate_; } //@} //! \name FloatingRateCoupon interface //@{ @@ -113,14 +134,76 @@ namespace QuantLib { RateAveraging::Type averagingMethod_; Natural lockoutDays_; bool applyObservationShift_; + bool includeSpread_; + Date rateComputationStartDate_, rateComputationEndDate_; Rate averageRate(const Date& date) const; }; + //! capped floored overnight indexed coupon + class CappedFlooredOvernightIndexedCoupon : public FloatingRateCoupon { + public: + /*! capped / floored compounded, backward-looking on coupon, local means that the daily rates are capped / floored + while a global cap / floor is applied to the effective period rate */ + CappedFlooredOvernightIndexedCoupon(const ext::shared_ptr& underlying, + Real cap = Null(), Real floor = Null(), bool nakedOption = false, + bool localCapFloor = false); + + //! \name Observer interface + //@{ + void deepUpdate() override; + //@} + //! \name LazyObject interface + //@{ + void performCalculations() const override; + void alwaysForwardNotifications(); + //@} + //! \name Coupon interface + //@{ + Rate rate() const override; + Rate convexityAdjustment() const override; + //@} + //! \name FloatingRateCoupon interface + //@{ + Date fixingDate() const override { return underlying_->fixingDate(); } + //@} + //! cap + Rate cap() const; + //! floor + Rate floor() const; + //! effective cap of fixing + Rate effectiveCap() const; + //! effective floor of fixing + Rate effectiveFloor() const; + //! effective caplet volatility + Real effectiveCapletVolatility() const; + //! effective floorlet volatility + Real effectiveFloorletVolatility() const; + //@} + //! \name Visitability + //@{ + virtual void accept(AcyclicVisitor&) override; + + bool isCapped() const { return cap_ != Null(); } + bool isFloored() const { return floor_ != Null(); } + + ext::shared_ptr underlying() const { return underlying_; } + bool nakedOption() const { return nakedOption_; } + bool localCapFloor() const { return localCapFloor_; } + + protected: + ext::shared_ptr underlying_; + Rate cap_, floor_; + bool nakedOption_; + bool localCapFloor_; + mutable Real effectiveCapletVolatility_; + mutable Real effectiveFloorletVolatility_; + }; + //! helper class building a sequence of overnight coupons class OvernightLeg { public: - OvernightLeg(Schedule schedule, ext::shared_ptr overnightIndex); + OvernightLeg(const Schedule& schedule, const ext::shared_ptr& overnightIndex); OvernightLeg& withNotionals(Real notional); OvernightLeg& withNotionals(const std::vector& notionals); OvernightLeg& withPaymentDayCounter(const DayCounter&); @@ -136,6 +219,19 @@ namespace QuantLib { OvernightLeg& withLookbackDays(Natural lookbackDays); OvernightLeg& withLockoutDays(Natural lockoutDays); OvernightLeg& withObservationShift(bool applyObservationShift = true); + OvernightLeg& includeSpread(bool includeSpread); + OvernightLeg& withLookback(const Period& lookback); + OvernightLeg& withCaps(Rate cap); + OvernightLeg& withCaps(const std::vector& caps); + OvernightLeg& withFloors(Rate floor); + OvernightLeg& withFloors(const std::vector& floors); + OvernightLeg& withNakedOption(const bool nakedOption); + OvernightLeg& withLocalCapFloor(const bool localCapFloor); + OvernightLeg& withInArrears(const bool inArrears); + OvernightLeg& withLastRecentPeriod(const ext::optional& lastRecentPeriod); + OvernightLeg& withLastRecentPeriodCalendar(const Calendar& lastRecentPeriodCalendar); + OvernightLeg& withPaymentDates(const std::vector& paymentDates); + operator Leg() const; private: Schedule schedule_; @@ -152,6 +248,16 @@ namespace QuantLib { Natural lookbackDays_ = Null(); Natural lockoutDays_ = 0; bool applyObservationShift_ = false; + bool includeSpread_ = false; + std::vector caps_, floors_; + bool nakedOption_; + bool localCapFloor_; + bool inArrears_ = true; + ext::optional lastRecentPeriod_; + Calendar lastRecentPeriodCalendar_; + std::vector paymentDates_; + ext::shared_ptr couponPricer_; //FIXME: make it flexible for both pricers + ext::shared_ptr capFlooredCouponPricer_; }; } diff --git a/ql/cashflows/overnightindexedcouponpricer.cpp b/ql/cashflows/overnightindexedcouponpricer.cpp index 81f98b775f8..5603c4a13b5 100644 --- a/ql/cashflows/overnightindexedcouponpricer.cpp +++ b/ql/cashflows/overnightindexedcouponpricer.cpp @@ -48,14 +48,34 @@ namespace QuantLib { } Rate CompoundingOvernightIndexedCouponPricer::swapletRate() const { - return averageRate(coupon_->accrualEndDate()); + auto [swapletRate, effectiveSpread, effectiveIndexFixing] = compute(coupon_->accrualEndDate()); + swapletRate_ = swapletRate; + effectiveSpread_ = effectiveSpread; + effectiveIndexFixing_ = effectiveIndexFixing; + return swapletRate; } Rate CompoundingOvernightIndexedCouponPricer::averageRate(const Date& date) const { - const Date today = Settings::instance().evaluationDate(); + auto [rate, effectiveSpread, effectiveIndexFixing] = compute(date); + return rate; + } - const ext::shared_ptr index = - ext::dynamic_pointer_cast(coupon_->index()); + Rate CompoundingOvernightIndexedCouponPricer::effectiveSpread() const { + auto [r, effectiveSpread, rest] = compute(coupon_->accrualEndDate()); + effectiveSpread_ = effectiveSpread; + return effectiveSpread_; + } + + Rate CompoundingOvernightIndexedCouponPricer::effectiveIndexFixing() const { + auto [r, effectiveSpread, effectiveIndexFixing] = compute(coupon_->accrualEndDate()); + effectiveIndexFixing_ = effectiveIndexFixing; + return effectiveIndexFixing_; + } + + std::tuple CompoundingOvernightIndexedCouponPricer::compute(const Date& date) const { + const Date today = Settings::instance().evaluationDate(); + + const ext::shared_ptr index = ext::dynamic_pointer_cast(coupon_->index()); const auto& pastFixings = index->timeSeries(); const auto& fixingDates = coupon_->fixingDates(); @@ -63,21 +83,26 @@ namespace QuantLib { const auto& interestDates = coupon_->interestDates(); const auto& dt = coupon_->dt(); const bool applyObservationShift = coupon_->applyObservationShift(); + Real couponSpread = coupon_->spread(); Size i = 0; const Size n = determineNumberOfFixings(interestDates, date, applyObservationShift); - Real compoundFactor = 1.0; + Real compoundFactor = 1.0, compoundFactorWithoutSpread = 1.0; // already fixed part while (i < n && fixingDates[i] < today) { // rate must have been fixed - const Rate fixing = pastFixings[fixingDates[i]]; + Rate fixing = pastFixings[fixingDates[i]]; QL_REQUIRE(fixing != Null(), "Missing " << index->name() << " fixing for " << fixingDates[i]); Time span = (date >= interestDates[i + 1] ? dt[i] : index->dayCounter().yearFraction(interestDates[i], date)); + if (coupon_->includeSpread()) { + compoundFactorWithoutSpread *= (1.0 + fixing * span); + fixing += coupon_->spread(); + } compoundFactor *= (1.0 + fixing * span); ++i; } @@ -91,6 +116,10 @@ namespace QuantLib { Time span = (date >= interestDates[i + 1] ? dt[i] : index->dayCounter().yearFraction(interestDates[i], date)); + if (coupon_->includeSpread()) { + compoundFactorWithoutSpread *= (1.0 + fixing * span); + fixing += coupon_->spread(); + } compoundFactor *= (1.0 + fixing * span); ++i; } else { @@ -110,12 +139,13 @@ namespace QuantLib { "null term structure set to this instance of " << index->name()); const auto effectiveRate = [&index, &fixingDates, &date, &interestDates, - &dt](Size position) { + &dt, &couponSpread](Size position, bool includeSpread) { Rate fixing = index->fixing(fixingDates[position]); Time span = (date >= interestDates[position + 1] ? dt[position] : index->dayCounter().yearFraction(interestDates[position], date)); - return span * fixing; + Spread spreadToAdd = includeSpread ? couponSpread : 0.0; + return span * (fixing + spreadToAdd); }; if (!coupon_->canApplyTelescopicFormula()) { @@ -130,7 +160,8 @@ namespace QuantLib { // Same applies to a case when accrual calculation date does or // does not occur on an interest date. while (i < n) { - compoundFactor *= (1.0 + effectiveRate(i)); + compoundFactorWithoutSpread *= (1.0 + effectiveRate(i, false)); + compoundFactor *= (1.0 + effectiveRate(i, coupon_->includeSpread())); ++i; } } else { @@ -148,6 +179,7 @@ namespace QuantLib { const DiscountFactor endDiscount = curve->discount(valueDates[std::min(nLockout, n)]); compoundFactor *= startDiscount / endDiscount; + compoundFactorWithoutSpread *= startDiscount / endDiscount; // For the lockout periods the telescopic formula does not apply. // The value dates (at which the projection is calculated) correspond // to the locked-out fixing, while the interest dates (at which the @@ -157,7 +189,8 @@ namespace QuantLib { // With no lockout, the loop is skipped because i = n. while (i < n) { - compoundFactor *= (1.0 + effectiveRate(i)); + compoundFactorWithoutSpread *= (1.0 + effectiveRate(i, false)); + compoundFactor *= (1.0 + effectiveRate(i, coupon_->includeSpread())); ++i; } } else { @@ -167,15 +200,30 @@ namespace QuantLib { // previous date, then we'll add the missing bit. const DiscountFactor endDiscount = curve->discount(valueDates[n - 1]); compoundFactor *= startDiscount / endDiscount; - compoundFactor *= (1.0 + effectiveRate(n - 1)); + compoundFactorWithoutSpread *= startDiscount / endDiscount; + compoundFactor *= (1.0 + effectiveRate(n - 1, coupon_->includeSpread())); + compoundFactorWithoutSpread *= (1.0 + effectiveRate(n - 1, false)); } } } + const Rate tau = index->dayCounter().yearFraction(valueDates.front(), valueDates.back()); const Rate rate = (compoundFactor - 1.0) / coupon_->accruedPeriod(date); - return coupon_->gearing() * rate + coupon_->spread(); + Rate finalRate = coupon_->gearing() * rate; + Spread effectiveSpread; + Rate effectiveIndexFixing; + if (!coupon_->includeSpread()) { + finalRate += coupon_->spread(); + effectiveSpread = coupon_->spread(); + effectiveIndexFixing = finalRate; + } else { + effectiveSpread = finalRate - (compoundFactorWithoutSpread - 1.0) / tau; + effectiveIndexFixing = finalRate - effectiveSpread; + } + return std::make_tuple(finalRate, effectiveSpread, effectiveIndexFixing); } + void ArithmeticAveragedOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) { coupon_ = dynamic_cast(&coupon); @@ -277,4 +325,24 @@ namespace QuantLib { ((te - ts) - pow(1.0 - exp(-mrs_ * (te - ts)), 2.0) / mrs_ - (1.0 - exp(-2.0 * mrs_ * (te - ts))) / (2.0 * mrs_)); } + + // CappedFlooredOvernightIndexedCouponPricer implementation (this is the base class only) + + CappedFlooredOvernightIndexedCouponPricer::CappedFlooredOvernightIndexedCouponPricer( + const Handle& v, const bool effectiveVolatilityInput) + : capletVol_(v), effectiveVolatilityInput_(effectiveVolatilityInput) { + registerWith(capletVol_); + } + + bool CappedFlooredOvernightIndexedCouponPricer::effectiveVolatilityInput() const { return effectiveVolatilityInput_; } + + Real CappedFlooredOvernightIndexedCouponPricer::effectiveCapletVolatility() const { return effectiveCapletVolatility_; } + + Real CappedFlooredOvernightIndexedCouponPricer::effectiveFloorletVolatility() const { + return effectiveFloorletVolatility_; + } + + Handle CappedFlooredOvernightIndexedCouponPricer::capletVolatility() const { + return capletVol_; + } } diff --git a/ql/cashflows/overnightindexedcouponpricer.hpp b/ql/cashflows/overnightindexedcouponpricer.hpp index 984af3cf2ac..027f6039cfa 100644 --- a/ql/cashflows/overnightindexedcouponpricer.hpp +++ b/ql/cashflows/overnightindexedcouponpricer.hpp @@ -35,6 +35,8 @@ namespace QuantLib { + class OptionletVolatilityStructure; + //! CompoudAveragedOvernightIndexedCouponPricer pricer class CompoundingOvernightIndexedCouponPricer : public FloatingRateCouponPricer { public: @@ -49,9 +51,13 @@ namespace QuantLib { Rate floorletRate(Rate) const override { QL_FAIL("floorletRate not available"); } //@} Rate averageRate(const Date& date) const; + Rate effectiveSpread() const; + Rate effectiveIndexFixing() const; protected: const OvernightIndexedCoupon* coupon_ = nullptr; + std::tuple compute(const Date& date) const; + mutable Real swapletRate_, effectiveSpread_, effectiveIndexFixing_; }; /*! pricer for arithmetically averaged overnight indexed coupons @@ -86,6 +92,23 @@ namespace QuantLib { Real mrs_; Real vol_; }; + + //! capped floored overnight indexed coupon pricer base class + class CappedFlooredOvernightIndexedCouponPricer : public FloatingRateCouponPricer { + public: + CappedFlooredOvernightIndexedCouponPricer(const Handle& v, + const bool effectiveVolatilityInput = false); + Handle capletVolatility() const; + bool effectiveVolatilityInput() const; + Real effectiveCapletVolatility() const; // only available after capletRate() was called + Real effectiveFloorletVolatility() const; // only available after floorletRate() was called + + protected: + Handle capletVol_; + bool effectiveVolatilityInput_; + mutable Real effectiveCapletVolatility_ = Null(); + mutable Real effectiveFloorletVolatility_ = Null(); + }; } #endif From 85216a2c0dc1eda7aeede1c2ff9dd8dff722a72e Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Thu, 7 Aug 2025 18:05:43 +0200 Subject: [PATCH 02/32] Add test for the overnightLeg --- ql/cashflows/overnightindexedcoupon.cpp | 18 +- ql/cashflows/overnightindexedcoupon.hpp | 5 +- ql/cashflows/overnightindexedcouponpricer.cpp | 3 +- test-suite/overnightindexedcoupon.cpp | 349 ++++++++++++++++++ 4 files changed, 367 insertions(+), 8 deletions(-) diff --git a/ql/cashflows/overnightindexedcoupon.cpp b/ql/cashflows/overnightindexedcoupon.cpp index 74e1d8152d2..a4e8d22d4c4 100644 --- a/ql/cashflows/overnightindexedcoupon.cpp +++ b/ql/cashflows/overnightindexedcoupon.cpp @@ -31,6 +31,7 @@ #include #include #include +#include using std::vector; @@ -528,17 +529,22 @@ namespace QuantLib { return *this; } - /* - OvernightLeg& OvernightLeg::withOvernightIndexedCouponPricer(const ext::shared_ptr& couponPricer) { + OvernightLeg& OvernightLeg::withOvernightIndexedCouponPricer(const ext::shared_ptr& couponPricer) { + if (averagingMethod_ == RateAveraging::Compound) + QL_REQUIRE((std::is_same_v), + "Wrong coupon pricer provided, provide a CompoundingOvernightIndexedCouponPricer"); + else + QL_REQUIRE((std::is_same_v) , + "Wrong coupon pricer provided, provide a ArithmeticAveragedOvernightIndexedCouponPricer"); couponPricer_ = couponPricer; return *this; } OvernightLeg& OvernightLeg::withCapFlooredOvernightIndexedCouponPricer( - const QuantLib::ext::shared_ptr& couponPricer) { - capFlooredCouponPricer_ = couponPricer; - return *this; - }*/ + const QuantLib::ext::shared_ptr& couponPricer) { + capFlooredCouponPricer_ = couponPricer; + return *this; + } OvernightLeg::operator Leg() const { diff --git a/ql/cashflows/overnightindexedcoupon.hpp b/ql/cashflows/overnightindexedcoupon.hpp index e04ec46af6e..60c7e12aada 100644 --- a/ql/cashflows/overnightindexedcoupon.hpp +++ b/ql/cashflows/overnightindexedcoupon.hpp @@ -231,6 +231,9 @@ namespace QuantLib { OvernightLeg& withLastRecentPeriod(const ext::optional& lastRecentPeriod); OvernightLeg& withLastRecentPeriodCalendar(const Calendar& lastRecentPeriodCalendar); OvernightLeg& withPaymentDates(const std::vector& paymentDates); + OvernightLeg& withOvernightIndexedCouponPricer(const ext::shared_ptr& couponPricer); + OvernightLeg& withCapFlooredOvernightIndexedCouponPricer( + const QuantLib::ext::shared_ptr& couponPricer); operator Leg() const; private: @@ -256,7 +259,7 @@ namespace QuantLib { ext::optional lastRecentPeriod_; Calendar lastRecentPeriodCalendar_; std::vector paymentDates_; - ext::shared_ptr couponPricer_; //FIXME: make it flexible for both pricers + ext::shared_ptr couponPricer_; ext::shared_ptr capFlooredCouponPricer_; }; diff --git a/ql/cashflows/overnightindexedcouponpricer.cpp b/ql/cashflows/overnightindexedcouponpricer.cpp index 5603c4a13b5..7dcb0883a93 100644 --- a/ql/cashflows/overnightindexedcouponpricer.cpp +++ b/ql/cashflows/overnightindexedcouponpricer.cpp @@ -61,7 +61,7 @@ namespace QuantLib { } Rate CompoundingOvernightIndexedCouponPricer::effectiveSpread() const { - auto [r, effectiveSpread, rest] = compute(coupon_->accrualEndDate()); + auto [r, effectiveSpread, effectiveIndexFixing] = compute(coupon_->accrualEndDate()); effectiveSpread_ = effectiveSpread; return effectiveSpread_; } @@ -212,6 +212,7 @@ namespace QuantLib { Rate finalRate = coupon_->gearing() * rate; Spread effectiveSpread; Rate effectiveIndexFixing; + if (!coupon_->includeSpread()) { finalRate += coupon_->spread(); effectiveSpread = coupon_->spread(); diff --git a/test-suite/overnightindexedcoupon.cpp b/test-suite/overnightindexedcoupon.cpp index 6ca1265149b..6f4f42dc490 100644 --- a/test-suite/overnightindexedcoupon.cpp +++ b/test-suite/overnightindexedcoupon.cpp @@ -19,10 +19,13 @@ #include "toplevelfixture.hpp" #include "utilities.hpp" +#include #include #include #include #include +#include +#include #include using namespace QuantLib; @@ -50,6 +53,20 @@ struct CommonVars { telescopicValueDates, averaging, fixingDays, lockoutDays, applyObservationShift); } + ext::shared_ptr makeSpreadedCoupon(Date startDate, + Date endDate, + Spread spread = 0.0001, + bool includeSpread = true, + Natural fixingDays = Null(), + Natural lockoutDays = 0, + bool applyObservationShift = false, + bool telescopicValueDates = false, + RateAveraging::Type averaging = RateAveraging::Compound) { + return ext::make_shared( + endDate, notional, startDate, endDate, sofr, 1.0, spread, Date(), Date(), DayCounter(), + telescopicValueDates, averaging, fixingDays, lockoutDays, applyObservationShift, includeSpread); + } + CommonVars(const Date& evaluationDate) { today = evaluationDate; @@ -110,6 +127,129 @@ struct CommonVars { CommonVars() : CommonVars(Date(23, November, 2021)) {} }; +struct CommonVarsONLeg { + Date today; + Real notional = 1000000.0; + ext::shared_ptr sofr; + RelinkableHandle forecastCurve; + Schedule legSchedule; + DayCounter dc; + + Leg makeLeg(Natural fixingDays = Null(), + Natural lockoutDays = 0, + bool applyObservationShift = false, + bool telescopicValueDates = false, + RateAveraging::Type averaging = RateAveraging::Compound, + const std::vector& gearings = std::vector(), + const std::vector& spreads = std::vector(), + const std::vector& caps = std::vector(), + const std::vector& floors = std::vector()) { + + OvernightLeg leg(legSchedule, sofr); + leg.withNotionals(notional) + .withPaymentDayCounter(dc) + .withAveragingMethod(averaging) + .withLockoutDays(lockoutDays) + .withObservationShift(applyObservationShift) + .withTelescopicValueDates(telescopicValueDates); + + if (fixingDays != Null()) { + leg.withLookbackDays(fixingDays); + } + + if (!gearings.empty()) { + leg.withGearings(gearings); + } + + if (!spreads.empty()) { + leg.withSpreads(spreads); + } + + if (!caps.empty()) { + leg.withCaps(caps); + } + + if (!floors.empty()) { + leg.withFloors(floors); + } + + return leg; + } + + CommonVarsONLeg(const Date& evaluationDate) { + today = evaluationDate; + dc = Actual360(); + + Settings::instance().evaluationDate() = today; + + sofr = ext::make_shared(forecastCurve); + + // Create a quarterly schedule for testing + legSchedule = Schedule(Date(1, July, 2025), Date(1, July, 2026), + Period(1, Months), + UnitedStates(UnitedStates::GovernmentBond), + ModifiedFollowing, ModifiedFollowing, + DateGeneration::Forward, false); + + std::vector pastDates = { + Date(2, June, 2025), Date(3, June, 2025), Date(4, June, 2025), Date(5, June, 2025), + Date(6, June, 2025), Date(9, June, 2025), Date(10, June, 2025), Date(11, June, 2025), + Date(12, June, 2025), Date(13, June, 2025), Date(16, June, 2025), Date(17, June, 2025), + Date(18, June, 2025), Date(20, June, 2025), Date(23, June, 2025), Date(24, June, 2025), + Date(25, June, 2025), Date(26, June, 2025), Date(27, June, 2025), Date(30, June, 2025), + Date(1, July, 2025), Date(2, July, 2025), Date(3, July, 2025), Date(7, July, 2025), + Date(8, July, 2025), Date(9, July, 2025), Date(10, July, 2025), Date(11, July, 2025), + Date(14, July, 2025), Date(15, July, 2025), Date(16, July, 2025), Date(17, July, 2025), + Date(18, July, 2025), Date(21, July, 2025), Date(22, July, 2025), Date(23, July, 2025), + Date(24, July, 2025), Date(25, July, 2025), Date(28, July, 2025), Date(29, July, 2025), + Date(30, July, 2025), Date(31, July, 2025), Date(1, August, 2025) + }; + + std::vector pastRates = { + 0.0435, 0.0432, 0.0428, 0.0429, 0.0429, 0.0429, 0.0428, 0.0428, 0.0428, 0.0428, + 0.0432, 0.0431, 0.0428, 0.0429, 0.0429, 0.0430, 0.0436, 0.0440, 0.0439, 0.0445, + 0.0444, 0.0440, 0.0435, 0.0433, 0.0434, 0.0432, 0.0431, 0.0431, 0.0433, 0.0437, + 0.0434, 0.0434, 0.0430, 0.0428, 0.0428, 0.0428, 0.0430, 0.0436, 0.0436, 0.0436, + 0.0432, 0.0439, 0.0434 + }; + + sofr->addFixings(pastDates.begin(), pastDates.end(), pastRates.begin()); + } + + void setupForecastCurve() { + std::vector curveDates = { + today, + Date(30, July, 2025), + Date(29, August, 2025), + Date(30, September, 2025), + Date(30, December, 2025), + Date(30, March, 2026), + Date(30, June, 2026) + }; + + std::vector zeroRates = { + 0.0434, + 0.0436, + 0.0431, + 0.0413, + 0.0390, + 0.0370, + 0.0348 + }; + + ext::shared_ptr> zeroCurve( + new InterpolatedZeroCurve(curveDates, zeroRates, + dc, UnitedStates(UnitedStates::SOFR)) + ); + + zeroCurve->enableExtrapolation(); + + forecastCurve.linkTo(zeroCurve); + } + + CommonVarsONLeg() : CommonVarsONLeg(Date(1, June, 2025)) {} +}; + #define CHECK_OIS_COUPON_RESULT(what, calculated, expected, tolerance) \ if (std::fabs(calculated-expected) > tolerance) { \ BOOST_ERROR("Failed to reproduce " what ":" \ @@ -135,6 +275,28 @@ BOOST_AUTO_TEST_CASE(testPastCouponRate) { CHECK_OIS_COUPON_RESULT("coupon amount", pastCoupon->amount(), expectedAmount, 1e-8); } +BOOST_AUTO_TEST_CASE(testPastSpreadedCouponRate) { + BOOST_TEST_MESSAGE("Testing rate for past overnight-indexed coupon with spread included..."); + + CommonVars vars; + + // coupon entirely in the past + auto pastCoupon = vars.makeSpreadedCoupon(Date(18, October, 2021), + Date(18, November, 2021), + 0.0001); + auto pastCouponNotSpreadedIncluded = vars.makeSpreadedCoupon(Date(18, October, 2021), + Date(18, November, 2021), + 0.0001, false); + + // expected values here and below come from manual calculations based on past dates and rates + Rate expectedRateSpreadInlcuded = 0.0010871445057780704; + Rate expectedRateSpreadNotInlcuded = 0.0010871361040194164; + Real expectedAmount = vars.notional * expectedRateSpreadInlcuded * 31.0/360; + CHECK_OIS_COUPON_RESULT("coupon rate", pastCoupon->rate(), expectedRateSpreadInlcuded, 1e-12); + CHECK_OIS_COUPON_RESULT("coupon rate", pastCouponNotSpreadedIncluded->rate(), expectedRateSpreadNotInlcuded, 1e-12); + CHECK_OIS_COUPON_RESULT("coupon amount", pastCoupon->amount(), expectedAmount, 1e-8); +} + BOOST_AUTO_TEST_CASE(testCurrentCouponRate) { BOOST_TEST_MESSAGE("Testing rate for current overnight-indexed coupon..."); @@ -534,6 +696,193 @@ BOOST_AUTO_TEST_CASE(testErrorWhenLookbackOrLockoutAppliedForSimpleAveraging) { Error); } +BOOST_AUTO_TEST_CASE(testOvernightLegBasicFunctionality) { + BOOST_TEST_MESSAGE("Testing basic functionality of OvernightLeg..."); + + CommonVarsONLeg vars; + vars.forecastCurve.linkTo(flatRate(0.0010, Actual360())); + + Leg leg = vars.makeLeg(); + + // Check that we have the expected number of coupons (monthly over 1 year = 12 coupons) + BOOST_CHECK_EQUAL(leg.size(), 12); + + // Check that all cash flows are OvernightIndexedCoupons + for (const auto& cf : leg) { + auto oisCoupon = ext::dynamic_pointer_cast(cf); + BOOST_CHECK(oisCoupon != nullptr); + if (oisCoupon) { + BOOST_CHECK_EQUAL(oisCoupon->nominal(), vars.notional); + BOOST_CHECK_EQUAL(oisCoupon->averagingMethod(), RateAveraging::Compound); + BOOST_CHECK_EQUAL(oisCoupon->lockoutDays(), 0); + BOOST_CHECK_EQUAL(oisCoupon->applyObservationShift(), false); + } + } +} + +BOOST_AUTO_TEST_CASE(testOvernightLegWithLookback) { + BOOST_TEST_MESSAGE("Testing OvernightLeg with lookback days..."); + + CommonVarsONLeg vars; + vars.forecastCurve.linkTo(flatRate(0.0010, Actual360())); + + Natural lookbackDays = 5; + Leg leg = vars.makeLeg(lookbackDays); + + for (const auto& cf : leg) { + auto oisCoupon = ext::dynamic_pointer_cast(cf); + BOOST_CHECK(oisCoupon != nullptr); + if (oisCoupon) { + // The coupon should have lookback configured + BOOST_CHECK(oisCoupon->fixingDays() == lookbackDays || + oisCoupon->fixingDays() == oisCoupon->index()->fixingDays()); + } + } +} + +BOOST_AUTO_TEST_CASE(testOvernightLegWithLockout) { + BOOST_TEST_MESSAGE("Testing OvernightLeg with lockout days..."); + + CommonVarsONLeg vars; + vars.forecastCurve.linkTo(flatRate(0.0010, Actual360())); + + Natural lockoutDays = 3; + Leg leg = vars.makeLeg(Null(), lockoutDays); + + for (const auto& cf : leg) { + auto oisCoupon = ext::dynamic_pointer_cast(cf); + BOOST_CHECK(oisCoupon != nullptr); + if (oisCoupon) { + BOOST_CHECK_EQUAL(oisCoupon->lockoutDays(), lockoutDays); + } + } +} + +BOOST_AUTO_TEST_CASE(testOvernightLegWithObservationShift) { + BOOST_TEST_MESSAGE("Testing OvernightLeg with observation shift..."); + + CommonVarsONLeg vars; + vars.forecastCurve.linkTo(flatRate(0.0010, Actual360())); + + Leg leg = vars.makeLeg(Null(), 0, true); + + for (const auto& cf : leg) { + auto oisCoupon = ext::dynamic_pointer_cast(cf); + BOOST_CHECK(oisCoupon != nullptr); + if (oisCoupon) { + BOOST_CHECK_EQUAL(oisCoupon->applyObservationShift(), true); + } + } +} + +BOOST_AUTO_TEST_CASE(testOvernightLegWithGearingsAndSpreads) { + BOOST_TEST_MESSAGE("Testing OvernightLeg with gearings and spreads..."); + + CommonVarsONLeg vars; + vars.setupForecastCurve(); + + std::vector gearings = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.25, 1.25, 1.25, 1.5, 2.0, 0.5}; + std::vector spreads = {0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, + 0.0001, 0.0001, 0.0001, 0.0002, 0.0003, 0.0004}; + + Leg leg = vars.makeLeg(Null(), 0, false, false, + RateAveraging::Compound, gearings, spreads); + + BOOST_CHECK_EQUAL(leg.size(), 12); + + for (Size i = 0; i < leg.size(); ++i) { + auto oisCoupon = ext::dynamic_pointer_cast(leg[i]); + BOOST_CHECK(oisCoupon != nullptr); + if (oisCoupon) { + BOOST_CHECK_CLOSE(oisCoupon->gearing(), gearings[i], 1e-12); + BOOST_CHECK_CLOSE(oisCoupon->spread(), spreads[i], 1e-12); + } + } +} + +BOOST_AUTO_TEST_CASE(testOvernightLegNPV) { + BOOST_TEST_MESSAGE("Testing Coupon Leg NPVs..."); + + CommonVarsONLeg vars; + vars.setupForecastCurve(); + + Leg leg = vars.makeLeg(Null(), 3, false, true, RateAveraging::Compound); + + Handle discountCurve(flatRate(0.0015, Actual360())); + + // Calculate NPV + Real expectedNpv = 34662.920418887923; + Real npv = 0.0; + for (const auto& cf : leg) { + npv += cf->amount() * discountCurve->discount(cf->date()); + } + + CHECK_OIS_COUPON_RESULT("OvernightLeg NPV", npv, expectedNpv, 1e-12); +} + +/* +BOOST_AUTO_TEST_CASE(testOvernightLegWithCapsAndFloors) { + BOOST_TEST_MESSAGE("Testing OvernightLeg with caps and floors..."); + + CommonVarsONLeg vars; + vars.forecastCurve.linkTo(flatRate(0.0010, Actual360())); + + std::vector caps = {0.05, 0.06, 0.07, 0.08}; + std::vector floors = {0.0, 0.001, 0.002, 0.003}; + + Leg leg = vars.makeLeg(Null(), 0, false, false, + RateAveraging::Compound, + std::vector(), std::vector(), + caps, floors); + + BOOST_CHECK_EQUAL(leg.size(), 4); + + for (Size i = 0; i < leg.size(); ++i) { + auto cappedFlooredCoupon = ext::dynamic_pointer_cast(leg[i]); + BOOST_CHECK(cappedFlooredCoupon != nullptr); + if (cappedFlooredCoupon) { + BOOST_CHECK_CLOSE(cappedFlooredCoupon->cap(), caps[i], 1e-12); + BOOST_CHECK_CLOSE(cappedFlooredCoupon->floor(), floors[i], 1e-12); + BOOST_CHECK(cappedFlooredCoupon->isCapped()); + BOOST_CHECK(cappedFlooredCoupon->isFloored()); + } + } +}*/ + +BOOST_AUTO_TEST_CASE(testOvernightLegSimpleAveraging) { + BOOST_TEST_MESSAGE("Testing OvernightLeg with simple averaging..."); + + CommonVarsONLeg vars; + vars.forecastCurve.linkTo(flatRate(0.0010, Actual360())); + + Leg leg = vars.makeLeg(Null(), 0, false, false, RateAveraging::Simple); + + for (const auto& cf : leg) { + auto oisCoupon = ext::dynamic_pointer_cast(cf); + BOOST_CHECK(oisCoupon != nullptr); + if (oisCoupon) { + BOOST_CHECK_EQUAL(oisCoupon->averagingMethod(), RateAveraging::Simple); + } + } +} + +BOOST_AUTO_TEST_CASE(testOvernightLegErrorConditions) { + BOOST_TEST_MESSAGE("Testing error conditions for OvernightLeg..."); + + CommonVarsONLeg vars; + vars.forecastCurve.linkTo(flatRate(0.0010, Actual360())); + + // Test that lookback with simple averaging throws an error + BOOST_CHECK_THROW(vars.makeLeg(5, 0, false, false, RateAveraging::Simple), Error); + + // Test that lockout with simple averaging throws an error + BOOST_CHECK_THROW(vars.makeLeg(Null(), 3, false, false, RateAveraging::Simple), Error); + + // Test that observation shift with simple averaging throws an error + BOOST_CHECK_THROW(vars.makeLeg(Null(), 0, true, false, RateAveraging::Simple), Error); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() From 633167fc20e93cc18ddf1b85d93419f5eade21a5 Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Sun, 10 Aug 2025 12:03:50 +0200 Subject: [PATCH 03/32] Fix tolerance in overnightleg test --- test-suite/overnightindexedcoupon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite/overnightindexedcoupon.cpp b/test-suite/overnightindexedcoupon.cpp index 6f4f42dc490..2373572ba4d 100644 --- a/test-suite/overnightindexedcoupon.cpp +++ b/test-suite/overnightindexedcoupon.cpp @@ -818,7 +818,7 @@ BOOST_AUTO_TEST_CASE(testOvernightLegNPV) { npv += cf->amount() * discountCurve->discount(cf->date()); } - CHECK_OIS_COUPON_RESULT("OvernightLeg NPV", npv, expectedNpv, 1e-12); + CHECK_OIS_COUPON_RESULT("OvernightLeg NPV", npv, expectedNpv, 1e-8); } /* From 121b3f38bce0ce7643f11350fa744b48e212764e Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Mon, 18 Aug 2025 23:29:53 +0200 Subject: [PATCH 04/32] Added blackovernigthindexedcouponpricer and updated pricesetter methods --- QuantLib.vcxproj | 2 + QuantLib.vcxproj.filters | 6 + ql/CMakeLists.txt | 2 + ql/cashflows/Makefile.am | 2 + .../blackovernightindexedcouponpricer.cpp | 511 ++++++++++++++++++ .../blackovernightindexedcouponpricer.hpp | 85 +++ ql/cashflows/couponpricer.cpp | 33 ++ ql/cashflows/overnightindexedcoupon.cpp | 15 +- ql/cashflows/overnightindexedcoupon.hpp | 13 +- 9 files changed, 663 insertions(+), 6 deletions(-) create mode 100644 ql/cashflows/blackovernightindexedcouponpricer.cpp create mode 100644 ql/cashflows/blackovernightindexedcouponpricer.hpp diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index d0b34e980ab..e8afbf0e563 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -510,6 +510,7 @@ + @@ -1924,6 +1925,7 @@ + diff --git a/QuantLib.vcxproj.filters b/QuantLib.vcxproj.filters index 4e3f928bd69..1c095387389 100644 --- a/QuantLib.vcxproj.filters +++ b/QuantLib.vcxproj.filters @@ -495,6 +495,9 @@ cashflows + + cashflows + cashflows @@ -4529,6 +4532,9 @@ cashflows + + cashflows + cashflows diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 063ce84db86..31fc37aee31 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -1,6 +1,7 @@ set(QL_SOURCES cashflow.cpp cashflows/averagebmacoupon.cpp + cashflows/blackovernightindexedcouponpricer.cpp cashflows/capflooredcoupon.cpp cashflows/capflooredinflationcoupon.cpp cashflows/cashflows.cpp @@ -951,6 +952,7 @@ set(QL_HEADERS auto_link.hpp cashflow.hpp cashflows/averagebmacoupon.hpp + cashflows/blackovernightindexedcouponpricer.hpp cashflows/capflooredcoupon.hpp cashflows/capflooredinflationcoupon.hpp cashflows/cashflows.hpp diff --git a/ql/cashflows/Makefile.am b/ql/cashflows/Makefile.am index cdb593471d2..af7320de996 100644 --- a/ql/cashflows/Makefile.am +++ b/ql/cashflows/Makefile.am @@ -5,6 +5,7 @@ this_includedir=${includedir}/${subdir} this_include_HEADERS = \ all.hpp \ averagebmacoupon.hpp \ + blackovernightindexedcouponpricer.hpp \ capflooredcoupon.hpp \ capflooredinflationcoupon.hpp \ cashflows.hpp \ @@ -42,6 +43,7 @@ this_include_HEADERS = \ cpp_files = \ averagebmacoupon.cpp \ + blackovernightindexedcouponpricer.cpp \ capflooredcoupon.cpp \ capflooredinflationcoupon.cpp \ cashflows.cpp \ diff --git a/ql/cashflows/blackovernightindexedcouponpricer.cpp b/ql/cashflows/blackovernightindexedcouponpricer.cpp new file mode 100644 index 00000000000..7703d2972f0 --- /dev/null +++ b/ql/cashflows/blackovernightindexedcouponpricer.cpp @@ -0,0 +1,511 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2020 Quaternion Risk Management Ltd + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. +*/ +#include + +#include +#include + +namespace QuantLib { + +void BlackOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) { + coupon_ = dynamic_cast(&coupon); + QL_REQUIRE(coupon_, "BlackOvernightIndexedCouponPricer: CappedFlooredOvernightIndexedCoupon required"); + gearing_ = coupon.gearing(); + index_ = ext::dynamic_pointer_cast(coupon.index()); + if (!index_) { + // check if the coupon was right + const CappedFlooredOvernightIndexedCoupon* c = + dynamic_cast(&coupon); + QL_REQUIRE(c, "BlackOvernightIndexedCouponPricer: CappedFlooredOvernightIndexedCoupon required"); + // coupon was right, index is not + QL_FAIL("BlackOvernightIndexedCouponPricer: CappedFlooredOvernightIndexedCoupon required"); + } + swapletRate_ = coupon_->underlying()->rate(); + effectiveIndexFixing_ = coupon_->underlying()->effectiveIndexFixing(); + effectiveCapletVolatility_ = effectiveFloorletVolatility_ = Null(); +} + +Real BlackOvernightIndexedCouponPricer::optionletRateGlobal(Option::Type optionType, Real effStrike) const { + Date lastRelevantFixingDate = coupon_->underlying()->fixingDate(); + if (lastRelevantFixingDate <= Settings::instance().evaluationDate()) { + // the amount is determined + Real a, b; + if (optionType == Option::Call) { + a = effectiveIndexFixing_; + b = effStrike; + } else { + a = effStrike; + b = effectiveIndexFixing_; + } + return gearing_ * std::max(a - b, 0.0); + } else { + // not yet determined, use Black model + QL_REQUIRE(!capletVolatility().empty(), "BlackOvernightIndexedCouponPricer: missing optionlet volatility"); + std::vector fixingDates = coupon_->underlying()->fixingDates(); + QL_REQUIRE(!fixingDates.empty(), "BlackOvernightIndexedCouponPricer: empty fixing dates"); + bool shiftedLn = capletVolatility()->volatilityType() == ShiftedLognormal; + Real shift = capletVolatility()->displacement(); + Real stdDev; + Real effectiveTime = capletVolatility()->timeFromReference(fixingDates.back()); + if (effectiveVolatilityInput()) { + // vol input is effective, i.e. we use a plain black model + stdDev = capletVolatility()->volatility(fixingDates.back(), effStrike) * std::sqrt(effectiveTime); + } else { + // vol input is not effective: + // for the standard deviation see Lyashenko, Mercurio, Looking forward to backward looking rates, + // section 6.3. the idea is to dampen the average volatility sigma between the fixing start and fixing end + // date by a linear function going from (fixing start, 1) to (fixing end, 0) + Real fixingStartTime = capletVolatility()->timeFromReference(fixingDates.front()); + Real fixingEndTime = capletVolatility()->timeFromReference(fixingDates.back()); + Real sigma = capletVolatility()->volatility( + std::max(fixingDates.front(), capletVolatility()->referenceDate() + 1), effStrike); + Real T = std::max(fixingStartTime, 0.0); + if (!close_enough(fixingEndTime, T)) + T += std::pow(fixingEndTime - T, 3.0) / std::pow(fixingEndTime - fixingStartTime, 2.0) / 3.0; + stdDev = sigma * std::sqrt(T); + } + if (optionType == Option::Type::Call) + effectiveCapletVolatility_ = stdDev / std::sqrt(effectiveTime); + else + effectiveFloorletVolatility_ = stdDev / std::sqrt(effectiveTime); + Real fixing = shiftedLn ? blackFormula(optionType, effStrike, effectiveIndexFixing_, stdDev, 1.0, shift) + : bachelierBlackFormula(optionType, effStrike, effectiveIndexFixing_, stdDev, 1.0); + return gearing_ * fixing; + } +} + +namespace { +Real cappedFlooredRate(Real r, Option::Type optionType, Real k) { + if (optionType == Option::Call) { + return std::min(r, k); + } else { + return std::max(r, k); + } +} +} // namespace + +Real BlackOvernightIndexedCouponPricer::optionletRateLocal(Option::Type optionType, Real effStrike) const { + + QL_REQUIRE(!effectiveVolatilityInput(), + "BlackAverageONIndexedCouponPricer::optionletRateLocal() does not support effective volatility input."); + + // We compute a rate and a rawRate such that + // rate * tau * nominal is the amount of the coupon with locally (i.e. daily) capped / floored rates + // rawRate * tau * nominal is the amount of the coupon without capping / flooring the rate + // We will then return the difference between rate and rawRate (with the correct sign, see below) + // as the option component of the coupon. + + // See CappedFlooredOvernightIndexedCoupon::effectiveCap(), Floor() for what is passed in as effStrike. + // From this we back out the absolute strike at which the + // - daily rate + spread (spread included) or the + // - daily rate (spread excluded) + // is capped / floored. + + Real absStrike = coupon_->underlying()->includeSpread() ? effStrike + coupon_->underlying()->spread() : effStrike; + + // This following code is inevitably quite similar to the plain ON coupon pricer code, possibly we can refactor + // this, but as a first step it seems safer to add the full modified code explicitly here and leave the original + // code alone. + + ext::shared_ptr index = ext::dynamic_pointer_cast(coupon_->index()); + + const std::vector& fixingDates = coupon_->underlying()->fixingDates(); + const std::vector