From 9536a5aab9c567d49cc3ec7e1b3d1205bdc20dfe Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Mon, 8 Jul 2024 13:20:39 +0200 Subject: [PATCH 1/2] Allow for holidays at start or end of SOFR future periods --- ql/instruments/overnightindexfuture.cpp | 53 +++++++++++++------ .../yield/overnightindexfutureratehelper.cpp | 23 ++++---- test-suite/sofrfutures.cpp | 14 +++-- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/ql/instruments/overnightindexfuture.cpp b/ql/instruments/overnightindexfuture.cpp index 7f226c6efe1..784e4811b04 100644 --- a/ql/instruments/overnightindexfuture.cpp +++ b/ql/instruments/overnightindexfuture.cpp @@ -44,20 +44,28 @@ namespace QuantLib { Handle forwardCurve = overnightIndex_->forwardingTermStructure(); Real avg = 0; Date d1 = valueDate_; + // d1 could be a holiday + Date fixingDate = calendar.adjust(d1, Preceding); const TimeSeries& history = IndexManager::instance() .getHistory(overnightIndex_->name()); Real fwd; while (d1 < maturityDate_) { Date d2 = calendar.advance(d1, 1, Days); - if (d1 < today) { - fwd = history[d1]; - QL_REQUIRE(fwd != Null(), "missing rate on " << - d1 << " for index " << overnightIndex_->name()); + if (fixingDate < today) { + fwd = history[fixingDate]; + QL_REQUIRE(fwd != Null(), + "missing rate on " << fixingDate << " for index " << overnightIndex_->name()); + } else if (fixingDate == today) { + fwd = history[fixingDate]; + if (fwd == Null()) + fwd = forwardCurve->forwardRate(fixingDate, d2, dayCounter, Simple).rate(); } else { - fwd = forwardCurve->forwardRate(d1, d2, dayCounter, Simple).rate(); + fwd = forwardCurve->forwardRate(fixingDate, d2, dayCounter, Simple).rate(); } - avg += fwd * dayCounter.yearFraction(d1, d2); - d1 = d2; + // The rate is accrued starting from d1 even when the fixing date is earlier. + // d2 might be beyond the maturity date if the latter is a holiday. + avg += fwd * dayCounter.yearFraction(d1, std::min(d2, maturityDate_)); + fixingDate = d1 = d2; } return avg / dayCounter.yearFraction(valueDate_, maturityDate_); @@ -69,29 +77,44 @@ namespace QuantLib { DayCounter dayCounter = overnightIndex_->dayCounter(); Handle forwardCurve = overnightIndex_->forwardingTermStructure(); Real prod = 1; + Date forwardDiscountStart = valueDate_; if (today > valueDate_) { // can't value on a weekend inside reference period because we // won't know the reset rate until start of next business day. // user can supply an estimate if they really want to do this today = calendar.adjust(today); + forwardDiscountStart = today; // for valuations inside the reference period, index quotes // must have been populated in the history const TimeSeries& history = IndexManager::instance() .getHistory(overnightIndex_->name()); Date d1 = valueDate_; + // d1 could be a holiday + Date fixingDate = calendar.adjust(d1, Preceding); while (d1 < today) { - Real r = history[d1]; - QL_REQUIRE(r != Null(), "missing rate on " << - d1 << " for index " << overnightIndex_->name()); + Real r = history[fixingDate]; + QL_REQUIRE(r != Null(), + "missing rate on " << fixingDate << " for index " << overnightIndex_->name()); Date d2 = calendar.advance(d1, 1, Days); + // The rate is accrued starting from d1 even when the fixing date is earlier. + // We can't get to the maturity date inside this loop, + // so we don't need to cap d2 like we do in averagedRate above. prod *= 1 + r * dayCounter.yearFraction(d1, d2); - d1 = d2; + fixingDate = d1 = d2; + } + // here d1 == today, and we might have today's fixing already + if (today < maturityDate_) { + Real r = history[today]; + if (r != Null()) { + Date tomorrow = calendar.advance(today, 1, Days); + prod *= 1 + r * dayCounter.yearFraction(today, tomorrow); + forwardDiscountStart = tomorrow; + } } } - DiscountFactor forwardDiscount = forwardCurve->discount(maturityDate_); - if (valueDate_ > today) { - forwardDiscount /= forwardCurve->discount(valueDate_); - } + // the telescopic part goes from the end of the last known fixing to the maturity + DiscountFactor forwardDiscount = + forwardCurve->discount(maturityDate_) / forwardCurve->discount(forwardDiscountStart); prod /= forwardDiscount; return (prod - 1) / dayCounter.yearFraction(valueDate_, maturityDate_); diff --git a/ql/termstructures/yield/overnightindexfutureratehelper.cpp b/ql/termstructures/yield/overnightindexfutureratehelper.cpp index 0c652e36ce9..2b7a7ed62dc 100644 --- a/ql/termstructures/yield/overnightindexfutureratehelper.cpp +++ b/ql/termstructures/yield/overnightindexfutureratehelper.cpp @@ -27,20 +27,19 @@ namespace QuantLib { namespace { - Date getValidSofrStart(Month month, Year year, Frequency freq) { + Date getSofrStart(Month month, Year year, Frequency freq) { static auto calendar = UnitedStates(UnitedStates::SOFR); - return calendar.adjust(freq == Monthly ? Date(1, month, year) : - Date::nthWeekday(3, Wednesday, month, year)); + return freq == Monthly ? Date(1, month, year) : + Date::nthWeekday(3, Wednesday, month, year); } - Date getValidSofrEnd(Month month, Year year, Frequency freq) { + Date getSofrEnd(Month month, Year year, Frequency freq) { static auto calendar = UnitedStates(UnitedStates::SOFR); if (freq == Monthly) { - Date d = calendar.endOfMonth(Date(1, month, year)); - return calendar.advance(d, 1*Days); + return Date::endOfMonth(Date(1, month, year)) + 1; } else { - Date d = getValidSofrStart(month, year, freq) + Period(freq); - return calendar.adjust(Date::nthWeekday(3, Wednesday, d.month(), d.year())); + Date d = getSofrStart(month, year, freq) + Period(freq); + return Date::nthWeekday(3, Wednesday, d.month(), d.year()); } } @@ -102,8 +101,8 @@ namespace QuantLib { Frequency referenceFreq, const Handle& convexityAdjustment) : OvernightIndexFutureRateHelper(price, - getValidSofrStart(referenceMonth, referenceYear, referenceFreq), - getValidSofrEnd(referenceMonth, referenceYear, referenceFreq), + getSofrStart(referenceMonth, referenceYear, referenceFreq), + getSofrEnd(referenceMonth, referenceYear, referenceFreq), ext::make_shared(), convexityAdjustment, referenceFreq == Quarterly ? RateAveraging::Compound : RateAveraging::Simple) { @@ -119,8 +118,8 @@ namespace QuantLib { Real convexityAdjustment) : OvernightIndexFutureRateHelper( Handle(ext::make_shared(price)), - getValidSofrStart(referenceMonth, referenceYear, referenceFreq), - getValidSofrEnd(referenceMonth, referenceYear, referenceFreq), + getSofrStart(referenceMonth, referenceYear, referenceFreq), + getSofrEnd(referenceMonth, referenceYear, referenceFreq), ext::make_shared(), Handle(ext::make_shared(convexityAdjustment)), referenceFreq == Quarterly ? RateAveraging::Compound : RateAveraging::Simple) { diff --git a/test-suite/sofrfutures.cpp b/test-suite/sofrfutures.cpp index c04e3158870..18d9001ad8c 100644 --- a/test-suite/sofrfutures.cpp +++ b/test-suite/sofrfutures.cpp @@ -117,11 +117,10 @@ BOOST_AUTO_TEST_CASE(testBootstrapWithJuneteenth) { BOOST_TEST_MESSAGE( "Testing bootstrap over SOFR futures when third Wednesday falls on Juneteenth..."); - Date today = Date(27, February, 2024); + Date today = Date(27, June, 2024); Settings::instance().evaluationDate() = today; const SofrQuotes sofrQuotes[] = { - {Quarterly, Mar, 2024, 97.295}, {Quarterly, Jun, 2024, 97.220}, {Quarterly, Sep, 2024, 97.170}, {Quarterly, Dec, 2024, 97.160}, @@ -130,6 +129,13 @@ BOOST_AUTO_TEST_CASE(testBootstrapWithJuneteenth) { }; ext::shared_ptr index = ext::make_shared(); + index->addFixing(Date(18, June, 2024), 0.02); + index->addFixing(Date(20, June, 2024), 0.02); + index->addFixing(Date(21, June, 2024), 0.02); + index->addFixing(Date(24, June, 2024), 0.02); + index->addFixing(Date(25, June, 2024), 0.02); + index->addFixing(Date(26, June, 2024), 0.02); + index->addFixing(Date(27, June, 2024), 0.02); std::vector > helpers; for (const auto& sofrQuote : sofrQuotes) { @@ -143,9 +149,9 @@ BOOST_AUTO_TEST_CASE(testBootstrapWithJuneteenth) { ext::shared_ptr sofr = ext::make_shared(Handle(curve)); - OvernightIndexFuture sf(sofr, Date(20, March, 2024), Date(20, June, 2024)); + OvernightIndexFuture sf(sofr, Date(19, June, 2024), Date(18, September, 2024)); - Real expected_price = 97.295; + Real expected_price = 97.220; Real tolerance = 1.0e-9; Real error = std::fabs(sf.NPV() - expected_price); From 4ded3b0be598db893f423ef996fecf129e9f5f67 Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Mon, 8 Jul 2024 21:29:55 +0200 Subject: [PATCH 2/2] Remove unused variables and include --- ql/termstructures/yield/overnightindexfutureratehelper.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/ql/termstructures/yield/overnightindexfutureratehelper.cpp b/ql/termstructures/yield/overnightindexfutureratehelper.cpp index 2b7a7ed62dc..ac9c4b51109 100644 --- a/ql/termstructures/yield/overnightindexfutureratehelper.cpp +++ b/ql/termstructures/yield/overnightindexfutureratehelper.cpp @@ -20,7 +20,6 @@ #include #include -#include #include namespace QuantLib { @@ -28,13 +27,11 @@ namespace QuantLib { namespace { Date getSofrStart(Month month, Year year, Frequency freq) { - static auto calendar = UnitedStates(UnitedStates::SOFR); return freq == Monthly ? Date(1, month, year) : Date::nthWeekday(3, Wednesday, month, year); } Date getSofrEnd(Month month, Year year, Frequency freq) { - static auto calendar = UnitedStates(UnitedStates::SOFR); if (freq == Monthly) { return Date::endOfMonth(Date(1, month, year)) + 1; } else {