Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions ql/instruments/makeois.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ namespace QuantLib {
}

Date refDate = Settings::instance().evaluationDate();
// if the evaluation date is not a business day
// then move to the next business day
refDate = overnightCalendar_.adjust(refDate);
Date spotDate = overnightCalendar_.advance(refDate,
// settlement days are counted from the actual evaluation
// date, even when it is not a business day (see issue #753)
const Calendar& settlementCalendar =
settlementCalendar_.empty() ? overnightCalendar_ : settlementCalendar_;
Date spotDate = settlementCalendar.advance(refDate,
settlementDays*Days);
startDate = spotDate+forwardStart_;
if (forwardStart_.length()<0)
Expand Down Expand Up @@ -202,6 +203,11 @@ namespace QuantLib {
return *this;
}

MakeOIS& MakeOIS::withSettlementCalendar(const Calendar& cal) {
settlementCalendar_ = cal;
return *this;
}

MakeOIS& MakeOIS::withEffectiveDate(const Date& effectiveDate) {
effectiveDate_ = effectiveDate;
return *this;
Expand Down
2 changes: 2 additions & 0 deletions ql/instruments/makeois.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ namespace QuantLib {
MakeOIS& withNominal(Real n);

MakeOIS& withSettlementDays(Natural settlementDays);
MakeOIS& withSettlementCalendar(const Calendar& cal);
MakeOIS& withEffectiveDate(const Date&);
MakeOIS& withTerminationDate(const Date&);
MakeOIS& withRule(DateGeneration::Rule r);
Expand Down Expand Up @@ -104,6 +105,7 @@ namespace QuantLib {
Natural settlementDays_ = Null<Natural>();
Date effectiveDate_, terminationDate_;
Calendar fixedCalendar_, overnightCalendar_;
Calendar settlementCalendar_;

Frequency fixedPaymentFrequency_ = Annual;
Frequency overnightPaymentFrequency_ = Annual;
Expand Down
14 changes: 10 additions & 4 deletions ql/instruments/makevanillaswap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ namespace QuantLib {
refDate = iborIndex_->fixingCalendar().adjust(refDate);
spotDate = iborIndex_->valueDate(refDate);
} else {
// an explicit settlement-day count is advanced on the
// float/payment calendar, so adjust the reference date there.
refDate = floatCalendar_.adjust(refDate);
spotDate = floatCalendar_.advance(refDate, settlementDays_ * Days);
// settlement days are counted from the actual evaluation
// date, even when it is not a business day (see issue #753)
const Calendar& settlementCalendar =
settlementCalendar_.empty() ? floatCalendar_ : settlementCalendar_;
spotDate = settlementCalendar.advance(refDate, settlementDays_ * Days);
}
startDate = spotDate+forwardStart_;
if (forwardStart_.length()<0)
Expand Down Expand Up @@ -221,6 +222,11 @@ namespace QuantLib {
return *this;
}

MakeVanillaSwap& MakeVanillaSwap::withSettlementCalendar(const Calendar& cal) {
settlementCalendar_ = cal;
return *this;
}

MakeVanillaSwap&
MakeVanillaSwap::withEffectiveDate(const Date& effectiveDate) {
effectiveDate_ = effectiveDate;
Expand Down
2 changes: 2 additions & 0 deletions ql/instruments/makevanillaswap.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ namespace QuantLib {
MakeVanillaSwap& withNominal(Real n);

MakeVanillaSwap& withSettlementDays(Natural settlementDays);
MakeVanillaSwap& withSettlementCalendar(const Calendar& cal);
MakeVanillaSwap& withEffectiveDate(const Date&);
MakeVanillaSwap& withTerminationDate(const Date&);
MakeVanillaSwap& withRule(DateGeneration::Rule r);
Expand Down Expand Up @@ -95,6 +96,7 @@ namespace QuantLib {
Natural settlementDays_ = Null<Natural>();
Date effectiveDate_, terminationDate_;
Calendar fixedCalendar_, floatCalendar_;
Calendar settlementCalendar_;

Swap::Type type_ = Swap::Payer;
Real nominal_ = 1.0;
Expand Down
26 changes: 10 additions & 16 deletions ql/termstructures/yield/ratehelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,10 @@ namespace QuantLib {

void DepositRateHelper::initializeDates() {
if (updateDates_) {
// if the evaluation date is not a business day
// then move to the next business day
Date referenceDate =
iborIndex_->fixingCalendar().adjust(evaluationDate_);
earliestDate_ = iborIndex_->valueDate(referenceDate);
// fixing days are counted from the actual evaluation date,
// even when it is not a business day (see issue #753)
earliestDate_ = iborIndex_->fixingCalendar().advance(
evaluationDate_, iborIndex_->fixingDays() * Days);
fixingDate_ = iborIndex_->fixingDate(earliestDate_);
} else {
earliestDate_ = iborIndex_->valueDate(fixingDate_);
Expand Down Expand Up @@ -392,12 +391,10 @@ namespace QuantLib {

void FraRateHelper::initializeDates() {
if (updateDates_) {
// if the evaluation date is not a business day
// then move to the next business day
Date referenceDate =
iborIndex_->fixingCalendar().adjust(evaluationDate_);
// fixing days are counted from the actual evaluation date,
// even when it is not a business day (see issue #753)
Date spotDate = iborIndex_->fixingCalendar().advance(
referenceDate, iborIndex_->fixingDays()*Days);
evaluationDate_, iborIndex_->fixingDays()*Days);
if (periodToStart_) { // NOLINT(readability-implicit-bool-conversion)
earliestDate_ = iborIndex_->fixingCalendar().advance(
spotDate, *periodToStart_, iborIndex_->businessDayConvention(),
Expand Down Expand Up @@ -673,13 +670,10 @@ namespace QuantLib {
}

void BMASwapRateHelper::initializeDates() {
// if the evaluation date is not a business day
// then move to the next business day
JointCalendar jc(calendar_,
iborIndex_->fixingCalendar());
Date referenceDate = jc.adjust(evaluationDate_);
// settlement days are counted from the actual evaluation date,
// even when it is not a business day (see issue #753)
earliestDate_ =
calendar_.advance(referenceDate, settlementDays_ * Days, Following);
calendar_.advance(evaluationDate_, settlementDays_ * Days, Following);

Date maturity = earliestDate_ + tenor_;

Expand Down
65 changes: 63 additions & 2 deletions test-suite/overnightindexedswap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include <ql/cashflows/overnightindexedcouponpricer.hpp>
#include <ql/currencies/europe.hpp>
#include <ql/time/calendars/unitedstates.hpp>
#include <ql/time/calendars/jointcalendar.hpp>
#include <ql/utilities/dataformatters.hpp>
#include <ql/indexes/ibor/sonia.hpp>
#include <ql/indexes/ibor/eonia.hpp>
Expand Down Expand Up @@ -1012,14 +1013,14 @@ BOOST_AUTO_TEST_CASE(testMakeOISDefaultSettlementDays) {
// Test 1-day settlement index on weekend
{
OvernightIndexedSwap swap = MakeOIS(6 * Months, indices[1].second, 0.01); // CORRA
Date expected(13, May, 2025); // Tuesday
Date expected(12, May, 2025); // Monday: T+1 from the actual trade date
BOOST_CHECK_EQUAL(swap.startDate(), expected);
}

// Test 2-day settlement index on weekend
{
OvernightIndexedSwap swap = MakeOIS(6 * Months, indices[2].second, 0.01); // EONIA
Date expected(14, May, 2025); // Wednesday
Date expected(13, May, 2025); // Tuesday: T+2 from the actual trade date
BOOST_CHECK_EQUAL(swap.startDate(), expected);
}
}
Expand Down Expand Up @@ -1093,6 +1094,66 @@ BOOST_AUTO_TEST_CASE(testSettlementDaysEffectiveDateConflict) {
BOOST_CHECK(swap3->startDate() != Date());
}

BOOST_AUTO_TEST_CASE(testSpotDateFromNonBusinessEvaluationDate) {

BOOST_TEST_MESSAGE("Testing that the OIS spot date is calculated from "
"the actual evaluation date when the latter is not a "
"business day...");

// Saturday
Date today(20, June, 2026);
Settings::instance().evaluationDate() = today;

auto index = ext::make_shared<Estr>();
Calendar calendar = index->fixingCalendar();

// settlement days are counted from the actual trade date,
// not from the next business day
Date expectedStart = calendar.advance(today, 2 * Days);

ext::shared_ptr<OvernightIndexedSwap> swap =
MakeOIS(1 * Years, index, 0.03).withSettlementDays(2);

if (swap->startDate() != expectedStart)
BOOST_FAIL("OIS start date not calculated from the actual "
"evaluation date:\n"
" expected: " << expectedStart << "\n"
" obtained: " << swap->startDate());
}

BOOST_AUTO_TEST_CASE(testSettlementCalendar) {

BOOST_TEST_MESSAGE("Testing that the OIS spot date can be calculated "
"on an explicit settlement calendar...");

// 3 July 2026 is a TARGET business day, but a US holiday
// (Independence Day observed), so the settlement calendar and the
// index fixing calendar diverge between here and the spot date.
Date today(2, July, 2026);
Settings::instance().evaluationDate() = today;

auto index = ext::make_shared<Estr>();
Calendar settlementCalendar =
JointCalendar(TARGET(), UnitedStates(UnitedStates::Settlement));

Date expectedStart = settlementCalendar.advance(today, 2 * Days);

ext::shared_ptr<OvernightIndexedSwap> swap =
MakeOIS(1 * Years, index, 0.03)
.withSettlementDays(2)
.withSettlementCalendar(settlementCalendar);

if (swap->startDate() != expectedStart)
BOOST_FAIL("OIS start date not calculated on the settlement "
"calendar:\n"
" expected: " << expectedStart << "\n"
" obtained: " << swap->startDate());

// sanity check: the two calendars must actually diverge here
BOOST_CHECK(expectedStart !=
index->fixingCalendar().advance(today, 2 * Days));
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()
51 changes: 51 additions & 0 deletions test-suite/piecewiseyieldcurve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2337,6 +2337,57 @@ BOOST_AUTO_TEST_CASE(testSwapRateHelperWithCouponPricer) {
}
}

BOOST_AUTO_TEST_CASE(testHelperDatesFromNonBusinessEvaluationDate) {

BOOST_TEST_MESSAGE("Testing that rate-helper dates are calculated from "
"the actual evaluation date when the latter is not a "
"business day...");

// Saturday
Date today(20, June, 2026);
Settings::instance().evaluationDate() = today;

auto euribor6m = ext::make_shared<Euribor6M>();
Calendar calendar = euribor6m->fixingCalendar();
// fixing days are counted from the actual evaluation date,
// not from the next business day
Date expectedSpot = calendar.advance(today, euribor6m->fixingDays() * Days);

auto depo = ext::make_shared<DepositRateHelper>(
0.03, 6 * Months, euribor6m->fixingDays(), calendar,
euribor6m->businessDayConvention(), euribor6m->endOfMonth(),
euribor6m->dayCounter());
if (depo->earliestDate() != expectedSpot)
BOOST_ERROR("deposit helper earliest date not calculated from the "
"actual evaluation date:\n"
" expected: " << expectedSpot << "\n"
" obtained: " << depo->earliestDate());

auto fra = ext::make_shared<FraRateHelper>(0.03, 3, euribor6m);
Date expectedFraStart =
calendar.advance(expectedSpot, 3 * Months,
euribor6m->businessDayConvention(),
euribor6m->endOfMonth());
if (fra->earliestDate() != expectedFraStart)
BOOST_ERROR("FRA helper earliest date not calculated from the "
"actual evaluation date:\n"
" expected: " << expectedFraStart << "\n"
" obtained: " << fra->earliestDate());

Calendar bmaCalendar = JointCalendar(BMAIndex().fixingCalendar(),
USDLibor(3 * Months).fixingCalendar());
auto bmaHelper = ext::make_shared<BMASwapRateHelper>(
Handle<Quote>(ext::make_shared<SimpleQuote>(0.75)),
5 * Years, 3, bmaCalendar, Period(Quarterly), Following, Actual360(),
ext::make_shared<BMAIndex>(), ext::make_shared<USDLibor>(3 * Months));
Date expectedBmaStart = bmaCalendar.advance(today, 3 * Days, Following);
if (bmaHelper->earliestDate() != expectedBmaStart)
BOOST_ERROR("BMA helper earliest date not calculated from the "
"actual evaluation date:\n"
" expected: " << expectedBmaStart << "\n"
" obtained: " << bmaHelper->earliestDate());
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()
61 changes: 61 additions & 0 deletions test-suite/swap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include <ql/indexes/ibor/gbplibor.hpp>
#include <ql/indexes/iborindex.hpp>
#include <ql/time/calendars/taiwan.hpp>
#include <ql/time/calendars/target.hpp>
#include <ql/time/calendars/unitedstates.hpp>
#include <ql/time/calendars/jointcalendar.hpp>

Expand Down Expand Up @@ -600,6 +601,66 @@ BOOST_AUTO_TEST_CASE(testSpotDateUsesFixingCalendar) {
BOOST_CHECK(buggyStart != expectedStart);
}

BOOST_AUTO_TEST_CASE(testSpotDateFromNonBusinessEvaluationDate) {

BOOST_TEST_MESSAGE("Testing that the swap spot date is calculated from "
"the actual evaluation date when the latter is not a "
"business day...");

// Saturday
Date today(20, June, 2026);
Settings::instance().evaluationDate() = today;

auto index = ext::make_shared<Euribor6M>();
Calendar calendar = index->fixingCalendar();

// settlement days are counted from the actual trade date,
// not from the next business day
Date expectedStart = calendar.advance(today, 2 * Days);

ext::shared_ptr<VanillaSwap> swap =
MakeVanillaSwap(5 * Years, index, 0.03).withSettlementDays(2);

if (swap->startDate() != expectedStart)
BOOST_FAIL("swap start date not calculated from the actual "
"evaluation date:\n"
" expected: " << expectedStart << "\n"
" obtained: " << swap->startDate());
}

BOOST_AUTO_TEST_CASE(testSettlementCalendar) {

BOOST_TEST_MESSAGE("Testing that the swap spot date can be calculated "
"on an explicit settlement calendar...");

// 3 July 2026 is a TARGET business day, but a US holiday
// (Independence Day observed), so the settlement calendar and the
// index fixing calendar diverge between here and the spot date.
Date today(2, July, 2026);
Settings::instance().evaluationDate() = today;

auto index = ext::make_shared<Euribor6M>();
Calendar settlementCalendar =
JointCalendar(TARGET(), UnitedStates(UnitedStates::Settlement));

Date expectedStart = settlementCalendar.advance(today, 2 * Days);

ext::shared_ptr<VanillaSwap> swap =
MakeVanillaSwap(5 * Years, index, 0.03)
.withSettlementDays(2)
.withSettlementCalendar(settlementCalendar);

if (swap->startDate() != expectedStart)
BOOST_FAIL("swap start date not calculated on the settlement "
"calendar:\n"
" expected: " << expectedStart << "\n"
" obtained: " << swap->startDate());

// sanity check: the two calendars must actually diverge here
BOOST_CHECK(expectedStart !=
index->fixingCalendar().advance(today, 2 * Days));
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()
Loading