Skip to content

Commit

Permalink
Allow for holidays at start or end of SOFR future periods (#2013)
Browse files Browse the repository at this point in the history
  • Loading branch information
lballabio authored Jul 8, 2024
2 parents 9e4ef24 + 4ded3b0 commit 1378d49
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 34 deletions.
53 changes: 38 additions & 15 deletions ql/instruments/overnightindexfuture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,28 @@ namespace QuantLib {
Handle<YieldTermStructure> forwardCurve = overnightIndex_->forwardingTermStructure();
Real avg = 0;
Date d1 = valueDate_;
// d1 could be a holiday
Date fixingDate = calendar.adjust(d1, Preceding);
const TimeSeries<Real>& 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<Real>(), "missing rate on " <<
d1 << " for index " << overnightIndex_->name());
if (fixingDate < today) {
fwd = history[fixingDate];
QL_REQUIRE(fwd != Null<Real>(),
"missing rate on " << fixingDate << " for index " << overnightIndex_->name());
} else if (fixingDate == today) {
fwd = history[fixingDate];
if (fwd == Null<Real>())
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_);
Expand All @@ -69,29 +77,44 @@ namespace QuantLib {
DayCounter dayCounter = overnightIndex_->dayCounter();
Handle<YieldTermStructure> 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<Real>& 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<Real>(), "missing rate on " <<
d1 << " for index " << overnightIndex_->name());
Real r = history[fixingDate];
QL_REQUIRE(r != Null<Real>(),
"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<Real>()) {
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_);
Expand Down
26 changes: 11 additions & 15 deletions ql/termstructures/yield/overnightindexfutureratehelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,23 @@

#include <ql/termstructures/yield/overnightindexfutureratehelper.hpp>
#include <ql/indexes/ibor/sofr.hpp>
#include <ql/time/calendars/unitedstates.hpp>
#include <ql/utilities/null_deleter.hpp>

namespace QuantLib {

namespace {

Date getValidSofrStart(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));
Date getSofrStart(Month month, Year year, Frequency freq) {
return freq == Monthly ? Date(1, month, year) :
Date::nthWeekday(3, Wednesday, month, year);
}

Date getValidSofrEnd(Month month, Year year, Frequency freq) {
static auto calendar = UnitedStates(UnitedStates::SOFR);
Date getSofrEnd(Month month, Year year, Frequency freq) {
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());
}

}
Expand Down Expand Up @@ -102,8 +98,8 @@ namespace QuantLib {
Frequency referenceFreq,
const Handle<Quote>& convexityAdjustment)
: OvernightIndexFutureRateHelper(price,
getValidSofrStart(referenceMonth, referenceYear, referenceFreq),
getValidSofrEnd(referenceMonth, referenceYear, referenceFreq),
getSofrStart(referenceMonth, referenceYear, referenceFreq),
getSofrEnd(referenceMonth, referenceYear, referenceFreq),
ext::make_shared<Sofr>(),
convexityAdjustment,
referenceFreq == Quarterly ? RateAveraging::Compound : RateAveraging::Simple) {
Expand All @@ -119,8 +115,8 @@ namespace QuantLib {
Real convexityAdjustment)
: OvernightIndexFutureRateHelper(
Handle<Quote>(ext::make_shared<SimpleQuote>(price)),
getValidSofrStart(referenceMonth, referenceYear, referenceFreq),
getValidSofrEnd(referenceMonth, referenceYear, referenceFreq),
getSofrStart(referenceMonth, referenceYear, referenceFreq),
getSofrEnd(referenceMonth, referenceYear, referenceFreq),
ext::make_shared<Sofr>(),
Handle<Quote>(ext::make_shared<SimpleQuote>(convexityAdjustment)),
referenceFreq == Quarterly ? RateAveraging::Compound : RateAveraging::Simple) {
Expand Down
14 changes: 10 additions & 4 deletions test-suite/sofrfutures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -130,6 +129,13 @@ BOOST_AUTO_TEST_CASE(testBootstrapWithJuneteenth) {
};

ext::shared_ptr<OvernightIndex> index = ext::make_shared<Sofr>();
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<ext::shared_ptr<RateHelper> > helpers;
for (const auto& sofrQuote : sofrQuotes) {
Expand All @@ -143,9 +149,9 @@ BOOST_AUTO_TEST_CASE(testBootstrapWithJuneteenth) {

ext::shared_ptr<OvernightIndex> sofr =
ext::make_shared<Sofr>(Handle<YieldTermStructure>(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);
Expand Down

0 comments on commit 1378d49

Please sign in to comment.