diff --git a/LICENSE.TXT b/LICENSE.TXT
index 028efcbf89d..283e1a8a67a 100644
--- a/LICENSE.TXT
+++ b/LICENSE.TXT
@@ -127,7 +127,7 @@ Copyright (C) 2016 Nicholas Bertocchi
Copyright (C) 2016 Stefano Fondi
Copyright (C) 2016, 2017 Fabrice Lecuyer
Copyright (C) 2016, 2019, 2020 Eisuke Tani
-Copyright (C) 2016, 2022 Quaternion Risk Management Ltd
+Copyright (C) 2016, 2020, 2022 Quaternion Risk Management Ltd
Copyright (C) 2017 BN Algorithms Ltd
Copyright (C) 2017 Joseph Jeisman
diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj
index d0b34e980ab..d16673a0f06 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/all.hpp b/ql/cashflows/all.hpp
index 6929ae3526e..61cc799efd7 100644
--- a/ql/cashflows/all.hpp
+++ b/ql/cashflows/all.hpp
@@ -2,6 +2,7 @@
/* Add the files to be included into Makefile.am instead. */
#include
+#include
#include
#include
#include
diff --git a/ql/cashflows/blackovernightindexedcouponpricer.cpp b/ql/cashflows/blackovernightindexedcouponpricer.cpp
new file mode 100644
index 00000000000..1b5569d9997
--- /dev/null
+++ b/ql/cashflows/blackovernightindexedcouponpricer.cpp
@@ -0,0 +1,521 @@
+/* -*- 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 {
+
+ BlackCompoundingOvernightIndexedCouponPricer::BlackCompoundingOvernightIndexedCouponPricer(
+ Handle v,
+ const bool effectiveVolatilityInput)
+ : CompoundingOvernightIndexedCouponPricer(v, effectiveVolatilityInput) {}
+
+ void BlackCompoundingOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) {
+ OvernightIndexedCouponPricer::initialize(coupon);
+
+ gearing_ = coupon.gearing();
+ std::tie(swapletRate_, effectiveSpread_, effectiveIndexFixing_) = CompoundingOvernightIndexedCouponPricer::compute(coupon_->accrualEndDate());
+ effectiveCapletVolatility_ = effectiveFloorletVolatility_ = Null();
+ }
+
+ Real BlackCompoundingOvernightIndexedCouponPricer::optionletRateGlobal(Option::Type optionType, Real effStrike) const {
+ Date lastRelevantFixingDate = coupon_->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(), "BlackCompoundingOvernightIndexedCouponPricer: missing optionlet volatility");
+ std::vector fixingDates = coupon_->fixingDates();
+ QL_REQUIRE(!fixingDates.empty(), "BlackCompoundingOvernightIndexedCouponPricer: 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 BlackCompoundingOvernightIndexedCouponPricer::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 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_->compoundSpreadDaily() ? effStrike + coupon_->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_->fixingDates();
+ const std::vector& dt = coupon_->dt();
+
+ Size n = dt.size();
+ Size i = 0;
+ QL_REQUIRE(coupon_->lockoutDays() < n,
+ "rate cutoff (" << coupon_->lockoutDays()
+ << ") must be less than number of fixings in period (" << n << ")");
+ Size nCutoff = n - coupon_->lockoutDays();
+
+ Real compoundFactor = 1.0, compoundFactorRaw = 1.0;
+
+ // already fixed part
+ Date today = Settings::instance().evaluationDate();
+ while (i < n && fixingDates[std::min(i, nCutoff)] < today) {
+ // rate must have been fixed
+ Rate pastFixing = index->pastFixing(fixingDates[std::min(i, nCutoff)]);
+ QL_REQUIRE(pastFixing != Null(),
+ "Missing " << index->name() << " fixing for " << fixingDates[std::min(i, nCutoff)]);
+ if (coupon_->compoundSpreadDaily()) {
+ pastFixing += coupon_->spread();
+ }
+ compoundFactor *= 1.0 + cappedFlooredRate(pastFixing, optionType, absStrike) * dt[i];
+ compoundFactorRaw *= 1.0 + pastFixing * dt[i];
+ ++i;
+ }
+
+ // today is a border case
+ if (i < n && fixingDates[std::min(i, nCutoff)] == today) {
+ // might have been fixed
+ try {
+ Rate pastFixing = index->pastFixing(today);
+ if (pastFixing != Null()) {
+ if (coupon_->compoundSpreadDaily()) {
+ pastFixing += coupon_->spread();
+ }
+ compoundFactor *= 1.0 + cappedFlooredRate(pastFixing, optionType, absStrike) * dt[i];
+ compoundFactorRaw *= 1.0 + pastFixing * dt[i];
+ ++i;
+ } else {
+ ; // fall through and forecast
+ }
+ } catch (Error&) {
+ ; // fall through and forecast
+ }
+ }
+
+ // forward part, approximation by pricing a cap / floor in the middle of the future period
+ const std::vector& dates = coupon_->valueDates();
+ if (i < n) {
+ Handle curve = index->forwardingTermStructure();
+ QL_REQUIRE(!curve.empty(), "null term structure set to this instance of " << index->name());
+
+ DiscountFactor startDiscount = curve->discount(dates[i]);
+ DiscountFactor endDiscount = curve->discount(dates[std::max(nCutoff, i)]);
+
+ // handle the rate cutoff period (if there is any, i.e. if nCutoff < n)
+ if (nCutoff < n) {
+ // forward discount factor for one calendar day on the cutoff date
+ DiscountFactor discountCutoffDate = curve->discount(dates[nCutoff] + 1) / curve->discount(dates[nCutoff]);
+ // keep the above forward discount factor constant during the cutoff period
+ endDiscount *= std::pow(discountCutoffDate, dates[n] - dates[nCutoff]);
+ }
+
+ // estimate the average daily rate over the future period (approximate the continuously compounded rate)
+ Real tau = coupon_->dayCounter().yearFraction(dates[i], dates.back());
+ Real averageRate = -std::log(endDiscount / startDiscount) / tau;
+
+ // compute the value of a cap or floor with fixing in the middle of the future period
+ // (but accounting for the rate cutoff here)
+ Time midPoint =
+ (capletVolatility()->timeFromReference(dates[i]) + capletVolatility()->timeFromReference(dates[nCutoff])) /
+ 2.0;
+ Real stdDev = capletVolatility()->volatility(midPoint, effStrike) * std::sqrt(midPoint);
+ Real shift = capletVolatility()->displacement();
+ bool shiftedLn = capletVolatility()->volatilityType() == ShiftedLognormal;
+ Rate cfValue = shiftedLn ? blackFormula(optionType, effStrike, averageRate, stdDev, 1.0, shift)
+ : bachelierBlackFormula(optionType, effStrike, averageRate, stdDev, 1.0);
+ Real effectiveTime = capletVolatility()->timeFromReference(fixingDates.back());
+ if (optionType == Option::Type::Call)
+ effectiveCapletVolatility_ = stdDev / std::sqrt(effectiveTime);
+ else
+ effectiveFloorletVolatility_ = stdDev / std::sqrt(effectiveTime);
+
+ // add spread to average rate
+ if (coupon_->compoundSpreadDaily()) {
+ averageRate += coupon_->spread();
+ }
+
+ // incorporate cap/floor into average rate
+ Real averageRateRaw = averageRate;
+ averageRate += optionType == Option::Call ? (-cfValue) : cfValue;
+
+ // now assume the averageRate is the effective rate over the future period and update the compoundFactor
+ // this is an approximation, see "Ester / Daily Spread Curve Setup in ORE": set tau to avg value
+ Real dailyTau =
+ coupon_->dayCounter().yearFraction(dates[i], dates.back()) / (dates.back() - dates[i]);
+ // now use formula (4) from the paper
+ compoundFactor *= std::pow(1.0 + dailyTau * averageRate, static_cast(dates.back() - dates[i]));
+ compoundFactorRaw *= std::pow(1.0 + dailyTau * averageRateRaw, static_cast(dates.back() - dates[i]));
+ }
+
+ Real tau = coupon_->lockoutDays() == 0
+ ? coupon_->accrualPeriod()
+ : coupon_->dayCounter().yearFraction(dates.front(), dates.back());
+ Rate rate = (compoundFactor - 1.0) / tau;
+ Rate rawRate = (compoundFactorRaw - 1.0) / tau;
+
+ rate *= coupon_->gearing();
+ rawRate *= coupon_->gearing();
+
+ if (!coupon_->compoundSpreadDaily()) {
+ rate += coupon_->spread();
+ rawRate += coupon_->spread();
+ }
+
+ // return optionletRate := r - rawRate, i.e. the option component only
+ // (see CappedFlooredOvernightIndexedCoupon::rate() for the signs of the capletRate / flooredRate)
+
+ return (optionType == Option::Call ? -1.0 : 1.0) * (rate - rawRate);
+ }
+
+ Rate BlackCompoundingOvernightIndexedCouponPricer::swapletRate() const { return swapletRate_; }
+
+ Rate BlackCompoundingOvernightIndexedCouponPricer::capletRate(Rate effectiveCap) const {
+ return capletRate(effectiveCap, false);
+ }
+
+ Rate BlackCompoundingOvernightIndexedCouponPricer::floorletRate(Rate effectiveFloor) const {
+ return floorletRate(effectiveFloor, false);
+ }
+
+ Rate BlackCompoundingOvernightIndexedCouponPricer::capletRate(Rate effectiveCap, bool dailyCapFloor) const {
+ return dailyCapFloor ? optionletRateLocal(Option::Call, effectiveCap)
+ : optionletRateGlobal(Option::Call, effectiveCap);
+ }
+
+ Rate BlackCompoundingOvernightIndexedCouponPricer::floorletRate(Rate effectiveFloor, bool dailyCapFloor) const {
+ return dailyCapFloor ? optionletRateLocal(Option::Put, effectiveFloor)
+ : optionletRateGlobal(Option::Put, effectiveFloor);
+ }
+
+ Real BlackCompoundingOvernightIndexedCouponPricer::swapletPrice() const {
+ QL_FAIL("BlackCompoundingOvernightIndexedCouponPricer::swapletPrice() not provided");
+ }
+ Real BlackCompoundingOvernightIndexedCouponPricer::capletPrice(Rate effectiveCap) const {
+ QL_FAIL("BlackCompoundingOvernightIndexedCouponPricer::capletPrice() not provided");
+ }
+ Real BlackCompoundingOvernightIndexedCouponPricer::floorletPrice(Rate effectiveFloor) const {
+ QL_FAIL("BlackCompoundingOvernightIndexedCouponPricer::floorletPrice() not provided");
+ }
+
+ BlackAveragingOvernightIndexedCouponPricer::BlackAveragingOvernightIndexedCouponPricer(
+ Handle v,
+ const bool effectiveVolatilityInput)
+ : ArithmeticAveragedOvernightIndexedCouponPricer(0.03, 0.0, false, v, effectiveVolatilityInput) {}
+
+ void BlackAveragingOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) {
+ OvernightIndexedCouponPricer::initialize(coupon);
+
+ if (coupon_->averagingMethod() == RateAveraging::Compound)
+ QL_FAIL("Averaging method required to be simple for BlackAveragingOvernightIndexedCouponPricer");
+
+ gearing_ = coupon.gearing();
+ swapletRate_ = ArithmeticAveragedOvernightIndexedCouponPricer::swapletRate();
+ forwardRate_ = (swapletRate_ - coupon_->spread()) / coupon_->gearing();
+ effectiveCapletVolatility_ = effectiveFloorletVolatility_ = Null();
+ }
+
+ Real BlackAveragingOvernightIndexedCouponPricer::optionletRateGlobal(Option::Type optionType, Real effStrike) const {
+ Date lastRelevantFixingDate = coupon_->fixingDate();
+ if (lastRelevantFixingDate <= Settings::instance().evaluationDate()) {
+ // the amount is determined
+ Real a, b;
+ if (optionType == Option::Call) {
+ a = forwardRate_;
+ b = effStrike;
+ } else {
+ a = effStrike;
+ b = forwardRate_;
+ }
+ return gearing_ * std::max(a - b, 0.0);
+ } else {
+ // not yet determined, use Black model
+ QL_REQUIRE(!capletVolatility().empty(), "BlackAveragingOvernightIndexedCouponPricer: missing optionlet volatility");
+ std::vector fixingDates = coupon_->fixingDates();
+ QL_REQUIRE(!fixingDates.empty(), "BlackAveragingOvernightIndexedCouponPricer: 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, forwardRate_, stdDev, 1.0, shift)
+ : bachelierBlackFormula(optionType, effStrike, forwardRate_, stdDev, 1.0);
+ return gearing_ * fixing;
+ }
+ }
+
+ Real BlackAveragingOvernightIndexedCouponPricer::optionletRateLocal(Option::Type optionType, Real effStrike) const {
+
+ QL_REQUIRE(!effectiveVolatilityInput(),
+ "BlackAveragingOvernightIndexedCouponPricer::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 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_->compoundSpreadDaily() ? effStrike + coupon_->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_->fixingDates();
+ const std::vector& dt = coupon_->dt();
+
+ Size n = dt.size();
+ Size i = 0;
+ QL_REQUIRE(coupon_->lockoutDays() < n,
+ "rate cutoff (" << coupon_->lockoutDays()
+ << ") must be less than number of fixings in period (" << n << ")");
+ Size nCutoff = n - coupon_->lockoutDays();
+
+ Real accumulatedRate = 0.0, accumulatedRateRaw = 0.0;
+
+ // already fixed part
+ Date today = Settings::instance().evaluationDate();
+ while (i < n && fixingDates[std::min(i, nCutoff)] < today) {
+ // rate must have been fixed
+ Rate pastFixing = index->pastFixing(fixingDates[std::min(i, nCutoff)]);
+ QL_REQUIRE(pastFixing != Null(),
+ "Missing " << index->name() << " fixing for " << fixingDates[std::min(i, nCutoff)]);
+ if (coupon_->compoundSpreadDaily()) {
+ pastFixing += coupon_->spread();
+ }
+ accumulatedRate += cappedFlooredRate(pastFixing, optionType, absStrike) * dt[i];
+ accumulatedRateRaw += pastFixing * dt[i];
+ ++i;
+ }
+
+ // today is a border case
+ if (i < n && fixingDates[std::min(i, nCutoff)] == today) {
+ // might have been fixed
+ try {
+ Rate pastFixing = index->pastFixing(today);
+ if (pastFixing != Null()) {
+ if (coupon_->compoundSpreadDaily()) {
+ pastFixing += coupon_->spread();
+ }
+ accumulatedRate += cappedFlooredRate(pastFixing, optionType, absStrike) * dt[i];
+ accumulatedRateRaw += pastFixing * dt[i];
+ ++i;
+ } else {
+ ; // fall through and forecast
+ }
+ } catch (Error&) {
+ ; // fall through and forecast
+ }
+ }
+
+ // forward part, approximation by pricing a cap / floor in the middle of the future period
+ const std::vector& dates = coupon_->valueDates();
+ if (i < n) {
+ Handle curve = index->forwardingTermStructure();
+ QL_REQUIRE(!curve.empty(), "null term structure set to this instance of " << index->name());
+
+ DiscountFactor startDiscount = curve->discount(dates[i]);
+ DiscountFactor endDiscount = curve->discount(dates[std::max(nCutoff, i)]);
+
+ // handle the rate cutoff period (if there is any, i.e. if nCutoff < n)
+ if (nCutoff < n) {
+ // forward discount factor for one calendar day on the cutoff date
+ DiscountFactor discountCutoffDate = curve->discount(dates[nCutoff] + 1) / curve->discount(dates[nCutoff]);
+ // keep the above forward discount factor constant during the cutoff period
+ endDiscount *= std::pow(discountCutoffDate, dates[n] - dates[nCutoff]);
+ }
+
+ // estimate the average daily rate over the future period (approximate the continuously compounded rate)
+ Real tau = coupon_->dayCounter().yearFraction(dates[i], dates.back());
+ Real averageRate = -std::log(endDiscount / startDiscount) / tau;
+
+ // compute the value of a cap or floor with fixing in the middle of the future period
+ // (but accounting for the rate cutoff here)
+ Time midPoint =
+ (capletVolatility()->timeFromReference(dates[i]) + capletVolatility()->timeFromReference(dates[nCutoff])) /
+ 2.0;
+ Real stdDev = capletVolatility()->volatility(midPoint, effStrike) * std::sqrt(midPoint);
+ Real shift = capletVolatility()->displacement();
+ bool shiftedLn = capletVolatility()->volatilityType() == ShiftedLognormal;
+ Rate cfValue = shiftedLn ? blackFormula(optionType, effStrike, averageRate, stdDev, 1.0, shift)
+ : bachelierBlackFormula(optionType, effStrike, averageRate, stdDev, 1.0);
+
+ Real effectiveTime = capletVolatility()->timeFromReference(fixingDates.back());
+ if (optionType == Option::Type::Call)
+ effectiveCapletVolatility_ = stdDev / std::sqrt(effectiveTime);
+ else
+ effectiveFloorletVolatility_ = stdDev / std::sqrt(effectiveTime);
+
+ // add spread to average rate
+ if (coupon_->compoundSpreadDaily()) {
+ averageRate += coupon_->spread();
+ }
+
+ // incorporate cap/floor into average rate
+ Real averageRateRaw = averageRate;
+ averageRate += optionType == Option::Call ? (-cfValue) : cfValue;
+
+ // now assume the averageRate is the effective rate over the future period and update the average rate
+ // this is an approximation, see "Ester / Daily Spread Curve Setup in ORE": set tau to avg value
+ Real dailyTau =
+ coupon_->dayCounter().yearFraction(dates[i], dates.back()) / (dates.back() - dates[i]);
+ accumulatedRate += dailyTau * averageRate * static_cast(dates.back() - dates[i]);
+ accumulatedRateRaw += dailyTau * averageRateRaw * static_cast(dates.back() - dates[i]);
+ }
+
+ Rate tau = coupon_->fixingDays() == 0
+ ? coupon_->accrualPeriod()
+ : coupon_->dayCounter().yearFraction(dates.front(), dates.back());
+ Rate rate = accumulatedRate / tau;
+ Rate rawRate = accumulatedRateRaw / tau;
+
+ rate *= coupon_->gearing();
+ rawRate *= coupon_->gearing();
+
+ if (!coupon_->compoundSpreadDaily()) {
+ rate += coupon_->spread();
+ rawRate += coupon_->spread();
+ }
+
+ // return optionletRate := r - rawRate, i.e. the option component only
+ // (see CappedFlooredAverageONIndexedCoupon::rate() for the signs of the capletRate / flooredRate)
+
+ return (optionType == Option::Call ? -1.0 : 1.0) * (rate - rawRate);
+ }
+
+ Rate BlackAveragingOvernightIndexedCouponPricer::swapletRate() const { return swapletRate_; }
+
+ Rate BlackAveragingOvernightIndexedCouponPricer::capletRate(Rate effectiveCap) const {
+ return OvernightIndexedCouponPricer::capletRate(effectiveCap, false);
+ }
+
+ Rate BlackAveragingOvernightIndexedCouponPricer::floorletRate(Rate effectiveFloor) const {
+ return OvernightIndexedCouponPricer::floorletRate(effectiveFloor, false);
+ }
+
+ Rate BlackAveragingOvernightIndexedCouponPricer::capletRate(Rate effectiveCap, bool dailyCapFloor) const {
+ return dailyCapFloor ? optionletRateLocal(Option::Call, effectiveCap)
+ : optionletRateGlobal(Option::Call, effectiveCap);
+ }
+
+ Rate BlackAveragingOvernightIndexedCouponPricer::floorletRate(Rate effectiveFloor, bool dailyCapFloor) const {
+ return dailyCapFloor ? optionletRateLocal(Option::Put, effectiveFloor)
+ : optionletRateGlobal(Option::Put, effectiveFloor);
+ }
+
+ Real BlackAveragingOvernightIndexedCouponPricer::swapletPrice() const {
+ QL_FAIL("BlackAveragingOvernightIndexedCouponPricer::swapletPrice() not provided");
+ }
+
+ Real BlackAveragingOvernightIndexedCouponPricer::capletPrice(Rate effectiveCap) const {
+ QL_FAIL("BlackAveragingOvernightIndexedCouponPricer::capletPrice() not provided");
+ }
+
+ Real BlackAveragingOvernightIndexedCouponPricer::floorletPrice(Rate effectiveFloor) const {
+ QL_FAIL("BlackAveragingOvernightIndexedCouponPricer::floorletPrice() not provided");
+ }
+
+}
diff --git a/ql/cashflows/blackovernightindexedcouponpricer.hpp b/ql/cashflows/blackovernightindexedcouponpricer.hpp
new file mode 100644
index 00000000000..e80c689d6c4
--- /dev/null
+++ b/ql/cashflows/blackovernightindexedcouponpricer.hpp
@@ -0,0 +1,97 @@
+/* -*- 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.
+*/
+
+/*! \file blackovernightindexedcouponpricer.hpp
+ \brief black coupon pricer for capped / floored ON indexed coupons
+*/
+
+#ifndef quantlib_black_overnight_indexed_coupon_pricer_hpp
+#define quantlib_black_overnight_indexed_coupon_pricer_hpp
+
+#include
+#include
+
+#include
+
+namespace QuantLib {
+
+ //! Black compounded overnight coupon pricer
+ /* The methods that are implemented here to price capped / floored compounded ON coupons are
+ highly experimental and ad-hoc. As soon as a market best practice has evolved, the pricer
+ should be revised. */
+ class BlackCompoundingOvernightIndexedCouponPricer : public CompoundingOvernightIndexedCouponPricer {
+ public:
+ explicit BlackCompoundingOvernightIndexedCouponPricer(
+ Handle v = Handle(),
+ const bool effectiveVolatilityInput = false);
+ //! \name FloatingRateCoupon interface
+ //@{
+ void initialize(const FloatingRateCoupon& coupon) override;
+ Real swapletPrice() const override;
+ Rate swapletRate() const override;
+ Real capletPrice(Rate effectiveCap) const override;
+ Rate capletRate(Rate effectiveCap) const override;
+ Real floorletPrice(Rate effectiveFloor) const override;
+ Rate floorletRate(Rate effectiveFloor) const override;
+ //@}
+ Rate capletRate(Rate effectiveCap, bool dailyCapFloor) const override;
+ Rate floorletRate(Rate effectiveCap, bool dailyCapFloor) const override;
+ private:
+ Real optionletRateGlobal(Option::Type optionType, Real effStrike) const;
+ Real optionletRateLocal(Option::Type optionType, Real effStrike) const;
+
+ Real gearing_;
+ ext::shared_ptr index_;
+ Real effectiveIndexFixing_, swapletRate_;
+ };
+
+ //! Black averaged overnight coupon pricer
+ /* The methods that are implemented here to price capped / floored average ON coupons are
+ highly experimental and ad-hoc. As soon as a market best practice has evolved, the pricer
+ should be revised. */
+ class BlackAveragingOvernightIndexedCouponPricer : public ArithmeticAveragedOvernightIndexedCouponPricer {
+ public:
+ explicit BlackAveragingOvernightIndexedCouponPricer(
+ Handle v = Handle(),
+ const bool effectiveVolatilityInput = false);
+ //! \name FloatingRateCoupon interface
+ //@{
+ void initialize(const FloatingRateCoupon& coupon) override;
+ Real swapletPrice() const override;
+ Rate swapletRate() const override;
+ Real capletPrice(Rate effectiveCap) const override;
+ Rate capletRate(Rate effectiveCap) const override;
+ Real floorletPrice(Rate effectiveFloor) const override;
+ Rate floorletRate(Rate effectiveFloor) const override;
+ //@}
+ Rate capletRate(Rate effectiveCap, bool dailyCapFloor) const override;
+ Rate floorletRate(Rate effectiveCap, bool dailyCapFloor) const override;
+ private:
+ Real optionletRateGlobal(Option::Type optionType, Real effStrike) const;
+ Real optionletRateLocal(Option::Type optionType, Real effStrike) const;
+
+ Real gearing_;
+ ext::shared_ptr index_;
+ Real swapletRate_, forwardRate_;
+ };
+
+}
+
+#endif
diff --git a/ql/cashflows/couponpricer.cpp b/ql/cashflows/couponpricer.cpp
index 44b5577b00f..1b82a085fea 100644
--- a/ql/cashflows/couponpricer.cpp
+++ b/ql/cashflows/couponpricer.cpp
@@ -27,6 +27,9 @@
#include
#include
#include
+#include
+#include
+#include
#include /* internal */
#include /* internal */
#include
@@ -249,6 +252,8 @@ namespace QuantLib {
public Visitor,
public Visitor,
public Visitor,
+ public Visitor,
+ public Visitor,
public Visitor,
public Visitor,
public Visitor,
@@ -266,6 +271,8 @@ namespace QuantLib {
void visit(CappedFlooredCoupon& c) override;
void visit(IborCoupon& c) override;
void visit(CappedFlooredIborCoupon& c) override;
+ void visit(OvernightIndexedCoupon& c) override;
+ void visit(CappedFlooredOvernightIndexedCoupon& c) override;
void visit(DigitalIborCoupon& c) override;
void visit(CmsCoupon& c) override;
void visit(CmsSpreadCoupon& c) override;
@@ -330,6 +337,42 @@ namespace QuantLib {
c.setPricer(iborCouponPricer);
}
+ void PricerSetter::visit(OvernightIndexedCoupon& c) {
+ if (c.averagingMethod() == RateAveraging::Compound) {
+ const ext::shared_ptr overnightCouponPricer =
+ ext::dynamic_pointer_cast(pricer_);
+ QL_REQUIRE(overnightCouponPricer,
+ "pricer not compatible with overnight indexed coupon");
+ c.setPricer(overnightCouponPricer);
+ } else {
+ const ext::shared_ptr overnightCouponPricer =
+ ext::dynamic_pointer_cast(pricer_);
+ QL_REQUIRE(overnightCouponPricer,
+ "pricer not compatible with arithmetic averaged overnight indexed coupon");
+ c.setPricer(overnightCouponPricer);
+ }
+ }
+
+ void PricerSetter::visit(CappedFlooredOvernightIndexedCoupon& c) {
+ auto overnightCouponPricer = ext::dynamic_pointer_cast(pricer_);
+ QL_REQUIRE(overnightCouponPricer, "pricer not compatible with capped-floored overnight indexed coupon");
+
+ if (c.averagingMethod() == RateAveraging::Compound) {
+ auto p = ext::dynamic_pointer_cast(overnightCouponPricer);
+ QL_REQUIRE(p,
+ "pricer not compatible with capped-floored overnight indexed coupon");
+ c.setPricer(p);
+ c.underlying()->accept(*this);
+ } else {
+ auto p =
+ ext::dynamic_pointer_cast(overnightCouponPricer);
+ QL_REQUIRE(p,
+ "pricer not compatible with arithmetic averaged capped-floored overnight indexed coupon");
+ c.setPricer(p);
+ c.underlying()->accept(*this);
+ }
+ }
+
void PricerSetter::visit(CmsCoupon& c) {
const ext::shared_ptr cmsCouponPricer =
ext::dynamic_pointer_cast(pricer_);
diff --git a/ql/cashflows/overnightindexedcoupon.cpp b/ql/cashflows/overnightindexedcoupon.cpp
index 0382671ad1d..ad6abb16fce 100644
--- a/ql/cashflows/overnightindexedcoupon.cpp
+++ b/ql/cashflows/overnightindexedcoupon.cpp
@@ -22,12 +22,17 @@
*/
#include
+#include
#include
+#include
#include
+#include
#include
+#include
#include
#include
#include
+#include
using std::vector;
@@ -57,7 +62,10 @@ namespace QuantLib {
RateAveraging::Type averagingMethod,
Natural lookbackDays,
Natural lockoutDays,
- bool applyObservationShift)
+ bool applyObservationShift,
+ bool compoundSpreadDaily,
+ const Date& rateComputationStartDate,
+ const Date& rateComputationEndDate)
: FloatingRateCoupon(paymentDate, nominal, startDate, endDate,
lookbackDays,
overnightIndex,
@@ -65,8 +73,18 @@ namespace QuantLib {
refPeriodStart, refPeriodEnd,
dayCounter, false),
averagingMethod_(averagingMethod), lockoutDays_(lockoutDays),
- applyObservationShift_(applyObservationShift) {
-
+ applyObservationShift_(applyObservationShift),
+ compoundSpreadDaily_(compoundSpreadDaily),
+ rateComputationStartDate_(rateComputationStartDate),
+ rateComputationEndDate_(rateComputationEndDate) {
+ Date valueStart = rateComputationStartDate_ == Null() ? startDate : rateComputationStartDate_;
+ Date valueEnd = rateComputationEndDate_ == Null() ? endDate : rateComputationEndDate_;
+ if (lookbackDays != Null()) {
+ BusinessDayConvention bdc = lookbackDays > 0 ? Preceding : Following;
+ valueStart = overnightIndex->fixingCalendar().advance(valueStart, -static_cast(lookbackDays), Days, bdc);
+ valueEnd = overnightIndex->fixingCalendar().advance(valueEnd, -static_cast(lookbackDays), Days, bdc);
+ }
+
// value dates
Date tmpEndDate = endDate;
@@ -222,7 +240,183 @@ namespace QuantLib {
}
}
- OvernightLeg::OvernightLeg(Schedule schedule, ext::shared_ptr i)
+ Real OvernightIndexedCoupon::effectiveSpread() const {
+ if (!compoundSpreadDaily_)
+ return spread();
+
+ if (averagingMethod_ == RateAveraging::Simple)
+ return spread();
+
+ 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());
+
+ if (averagingMethod_ == RateAveraging::Simple)
+ QL_FAIL("Average OIS Coupon does not have an effectiveIndexFixing"); // FIXME: better error message
+
+ 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 dailyCapFloor)
+ : 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), dailyCapFloor_(dailyCapFloor) {
+
+ QL_REQUIRE(!underlying_->compoundSpreadDaily() || 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 (!dailyCapFloor) {
+ 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(), "underlying coupon pricer not set");
+ Rate swapletRate = nakedOption_ ? 0.0 : underlying_->rate();
+ auto cfONPricer = ext::dynamic_pointer_cast(pricer());
+ QL_REQUIRE(cfONPricer, "coupon pricer not an instance of OvernightIndexedCouponPricer");
+
+ if (floor_ != Null() || cap_ != Null())
+ cfONPricer->initialize(*this);
+ Rate floorletRate = 0.;
+ if (floor_ != Null())
+ floorletRate = cfONPricer->floorletRate(effectiveFloor(), dailyCapFloor());
+ Rate capletRate = 0.;
+ if (cap_ != Null())
+ capletRate = (nakedOption_ && floor_ == Null() ? -1.0 : 1.0) * cfONPricer->capletRate(effectiveCap(), dailyCapFloor());
+ rate_ = swapletRate + floorletRate - capletRate;
+
+ effectiveCapletVolatility_ = cfONPricer->effectiveCapletVolatility();
+ effectiveFloorletVolatility_ = cfONPricer->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 dailyCapFloor_ and compoundSpreadDaily. 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 (dailyCapFloor_) {
+ if (underlying_->compoundSpreadDaily()) {
+ // 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_->compoundSpreadDaily()) {
+ // 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 (dailyCapFloor_) {
+ if (underlying_->compoundSpreadDaily()) {
+ return floor_ - underlying_->spread();
+ } else {
+ return floor_;
+ }
+ } else {
+ if (underlying_->compoundSpreadDaily()) {
+ 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);
+ }
+
+ void CappedFlooredOvernightIndexedCoupon::setPricer(const ext::shared_ptr& pricer){
+ auto p = ext::dynamic_pointer_cast(pricer);
+ QL_REQUIRE(p, "The pricer is required to be an instance of OvernightIndexedCouponPricer");
+ FloatingRateCoupon::setPricer(p);
+ }
+
+ // 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,24 +495,117 @@ namespace QuantLib {
return *this;
}
+ OvernightLeg& OvernightLeg::compoundingSpreadDaily(bool compoundSpreadDaily) {
+ compoundSpreadDaily_ = compoundSpreadDaily;
+ 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::withDailyCapFloor(const bool dailyCapFloor) {
+ dailyCapFloor_ = dailyCapFloor;
+ 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::withCouponPricer(const ext::shared_ptr& couponPricer) {
+ couponPricer_ = couponPricer;
+ return *this;
+ }
+
OvernightLeg::operator Leg() const {
QL_REQUIRE(!notionals_.empty(), "no notional given");
+ if (couponPricer_ != nullptr) {
+ if (averagingMethod_ == RateAveraging::Compound)
+ QL_REQUIRE(ext::dynamic_pointer_cast(couponPricer_),
+ "Wrong coupon pricer provided, provide a CompoundingOvernightIndexedCouponPricer");
+ else
+ QL_REQUIRE(ext::dynamic_pointer_cast(couponPricer_),
+ "Wrong coupon pricer provided, provide a ArithmeticAveragedOvernightIndexedCouponPricer");
+ }
+
Leg cashflows;
// 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_,
+ compoundSpreadDaily_, 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_,
+ dailyCapFloor_);
+ if (couponPricer_) {
+ cfCpn->setPricer(couponPricer_);
+ }
+ cashflows.push_back(cfCpn);
+ }
+ }
}
return cashflows;
}
diff --git a/ql/cashflows/overnightindexedcoupon.hpp b/ql/cashflows/overnightindexedcoupon.hpp
index 3adb50262d1..8153ad3366d 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 OvernightIndexedCouponPricer;
+ class CompoundingOvernightIndexedCouponPricer;
+
//! 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 compoundSpread = 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_; }
+ //! is the spread compounded daily or added after compounding?
+ bool compoundSpreadDaily() const { return compoundSpreadDaily_; }
+ /*! effectiveSpread and effectiveIndexFixing are set such that
+ coupon amount = notional * accrualPeriod * ( gearing * effectiveIndexFixing + effectiveSpread )
+ notice that
+ - gearing = 1 is required if compoundSpreadDaily = true
+ - effectiveSpread = spread() if compoundSpreadDaily = 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,83 @@ namespace QuantLib {
RateAveraging::Type averagingMethod_;
Natural lockoutDays_;
bool applyObservationShift_;
+ bool compoundSpreadDaily_;
+ 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. The cap can be applied to the
+ effective period rate (the default) or to the daily rates. */
+ explicit CappedFlooredOvernightIndexedCoupon(const ext::shared_ptr& underlying,
+ Real cap = Null(),
+ Real floor = Null(),
+ bool nakedOption = false,
+ bool dailyCapFloor = 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(); }
+
+ void setPricer(const ext::shared_ptr& pricer) override;
+
+ ext::shared_ptr underlying() const { return underlying_; }
+ bool nakedOption() const { return nakedOption_; }
+ bool dailyCapFloor() const { return dailyCapFloor_; }
+ bool compoundSpreadDaily() const { return underlying_->compoundSpreadDaily(); }
+ //! averaging method
+ RateAveraging::Type averagingMethod() const { return underlying_->averagingMethod(); }
+
+ protected:
+ ext::shared_ptr underlying_;
+ Rate cap_, floor_;
+ bool nakedOption_;
+ bool dailyCapFloor_;
+ 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 +226,20 @@ namespace QuantLib {
OvernightLeg& withLookbackDays(Natural lookbackDays);
OvernightLeg& withLockoutDays(Natural lockoutDays);
OvernightLeg& withObservationShift(bool applyObservationShift = true);
+ OvernightLeg& compoundingSpreadDaily(bool compoundSpreadDaily = true);
+ 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& withDailyCapFloor(const bool dailyCapFloor = true);
+ OvernightLeg& withInArrears(const bool inArrears);
+ OvernightLeg& withLastRecentPeriod(const ext::optional& lastRecentPeriod);
+ OvernightLeg& withLastRecentPeriodCalendar(const Calendar& lastRecentPeriodCalendar);
+ OvernightLeg& withPaymentDates(const std::vector& paymentDates);
+ OvernightLeg& withCouponPricer(const ext::shared_ptr& couponPricer);
+
operator Leg() const;
private:
Schedule schedule_;
@@ -152,6 +256,15 @@ namespace QuantLib {
Natural lookbackDays_ = Null();
Natural lockoutDays_ = 0;
bool applyObservationShift_ = false;
+ bool compoundSpreadDaily_ = false;
+ std::vector caps_, floors_;
+ bool nakedOption_ = false;
+ bool dailyCapFloor_ = false;
+ bool inArrears_ = true;
+ ext::optional lastRecentPeriod_;
+ Calendar lastRecentPeriodCalendar_;
+ std::vector paymentDates_;
+ ext::shared_ptr couponPricer_;
};
}
diff --git a/ql/cashflows/overnightindexedcouponpricer.cpp b/ql/cashflows/overnightindexedcouponpricer.cpp
index 81f98b775f8..ab5fb95a825 100644
--- a/ql/cashflows/overnightindexedcouponpricer.cpp
+++ b/ql/cashflows/overnightindexedcouponpricer.cpp
@@ -42,20 +42,80 @@ namespace QuantLib {
}
}
- void CompoundingOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) {
- coupon_ = dynamic_cast(&coupon);
- QL_ENSURE(coupon_, "wrong coupon type");
+ OvernightIndexedCouponPricer::OvernightIndexedCouponPricer(
+ Handle v,
+ const bool effectiveVolatilityInput)
+ : capletVol_(std::move(v)),
+ effectiveVolatilityInput_(effectiveVolatilityInput) {
+ registerWith(capletVol_);
}
+ void OvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) {
+ if (auto cfCoupon = dynamic_cast(&coupon)) {
+ auto underlying = cfCoupon->underlying().get();
+ QL_REQUIRE(underlying, "OvernightIndexedCouponPricer: CappedFlooredOvernightIndexedCoupon underlying coupon not defined");
+ coupon_ = cfCoupon->underlying().get();
+ } else if (auto onCoupon = dynamic_cast(&coupon)) {
+ coupon_ = onCoupon;
+ } else {
+ QL_FAIL("OvernightIndexedCouponPricer: unsupported coupon type");
+ }
+ }
+
+ bool OvernightIndexedCouponPricer::effectiveVolatilityInput() const {
+ return effectiveVolatilityInput_;
+ }
+
+ Real OvernightIndexedCouponPricer::effectiveCapletVolatility() const {
+ return effectiveCapletVolatility_;
+ }
+
+ Real OvernightIndexedCouponPricer::effectiveFloorletVolatility() const {
+ return effectiveFloorletVolatility_;
+ }
+
+ Rate OvernightIndexedCouponPricer::capletRate(Rate effectiveCap, bool dailyCapFloor) const {
+ QL_FAIL("OvernightIndexedCouponPricer::capletRate(Rate, bool) not implemented");
+ }
+
+ Rate OvernightIndexedCouponPricer::floorletRate(Rate effectiveFloor, bool dailyCapFloor) const {
+ QL_FAIL("OvernightIndexedCouponPricer::capletRate(Rate, bool) not implemented");
+ }
+
+ CompoundingOvernightIndexedCouponPricer::CompoundingOvernightIndexedCouponPricer(
+ Handle v,
+ const bool effectiveVolatilityInput)
+ : OvernightIndexedCouponPricer(v, effectiveVolatilityInput) {}
+
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, effectiveIndexFixing] = 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 +123,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_->compoundSpreadDaily()) {
+ compoundFactorWithoutSpread *= (1.0 + fixing * span);
+ fixing += coupon_->spread();
+ }
compoundFactor *= (1.0 + fixing * span);
++i;
}
@@ -91,6 +156,10 @@ namespace QuantLib {
Time span = (date >= interestDates[i + 1] ?
dt[i] :
index->dayCounter().yearFraction(interestDates[i], date));
+ if (coupon_->compoundSpreadDaily()) {
+ compoundFactorWithoutSpread *= (1.0 + fixing * span);
+ fixing += coupon_->spread();
+ }
compoundFactor *= (1.0 + fixing * span);
++i;
} else {
@@ -110,12 +179,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 compoundSpreadDaily) {
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 = compoundSpreadDaily ? couponSpread : 0.0;
+ return span * (fixing + spreadToAdd);
};
if (!coupon_->canApplyTelescopicFormula()) {
@@ -130,7 +200,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_->compoundSpreadDaily()));
++i;
}
} else {
@@ -148,6 +219,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 +229,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_->compoundSpreadDaily()));
++i;
}
} else {
@@ -167,19 +240,29 @@ 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_->compoundSpreadDaily()));
+ 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 swapletRate = coupon_->gearing() * rate;
+ Spread effectiveSpread;
+ Rate effectiveIndexFixing;
+
+ if (!coupon_->compoundSpreadDaily()) {
+ swapletRate += coupon_->spread();
+ effectiveSpread = coupon_->spread();
+ effectiveIndexFixing = rate;
+ } else {
+ effectiveSpread = rate - (compoundFactorWithoutSpread - 1.0) / tau;
+ effectiveIndexFixing = rate - effectiveSpread;
+ }
- void
- ArithmeticAveragedOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) {
- coupon_ = dynamic_cast(&coupon);
- QL_ENSURE(coupon_, "wrong coupon type");
+ return std::make_tuple(swapletRate, effectiveSpread, effectiveIndexFixing);
}
Rate ArithmeticAveragedOvernightIndexedCouponPricer::swapletRate() const {
diff --git a/ql/cashflows/overnightindexedcouponpricer.hpp b/ql/cashflows/overnightindexedcouponpricer.hpp
index 984af3cf2ac..f408130271b 100644
--- a/ql/cashflows/overnightindexedcouponpricer.hpp
+++ b/ql/cashflows/overnightindexedcouponpricer.hpp
@@ -35,12 +35,79 @@
namespace QuantLib {
+ class OptionletVolatilityStructure;
+
+ //! Pricer for overnight-indexed floating coupons
+ /*!
+ This is the base pricer class for coupons indexed to an overnight rate.
+ It defines the common pricing interface and provides the foundation for
+ more specialized overnight coupon pricers (e.g., compounded, averaged,
+ capped/floored variants).
+
+ Derived classes should implement the specific logic for computing the
+ rate and optional adjustments, depending on the compounding or
+ averaging convention used.
+ */
+ class OvernightIndexedCouponPricer : public FloatingRateCouponPricer {
+ using FloatingRateCouponPricer::capletRate;
+ using FloatingRateCouponPricer::floorletRate;
+ public:
+
+ explicit OvernightIndexedCouponPricer(
+ Handle v = Handle(),
+ const bool effectiveVolatilityInput = false);
+
+ void initialize(const FloatingRateCoupon& coupon) override;
+
+ void setCapletVolatility(
+ const Handle& v =
+ Handle()) {
+ unregisterWith(capletVol_);
+ capletVol_ = v;
+ registerWith(capletVol_);
+ update();
+ }
+
+ /*! \brief Returns the handle to the optionlet volatility structure used for caplets/floorlets */
+ Handle capletVolatility() const {
+ return capletVol_;
+ }
+
+ void setEffectiveVolatilityInput(const bool effectiveVolatilityInput) {
+ effectiveVolatilityInput_ = effectiveVolatilityInput;
+ }
+
+ /*! \brief Returns true if the volatility input is interpreted as effective volatility */
+ bool effectiveVolatilityInput() const;
+ /*! \brief Returns the effective caplet volatility used in the last capletRate() calculation.
+ \note Only available after capletRate() was called.
+ */
+ virtual Real effectiveCapletVolatility() const;
+ /*! \brief Returns the effective floorlet volatility used in the last floorletRate() calculation.
+ \note Only available after floorletRate() was called.
+ */
+ virtual Real effectiveFloorletVolatility() const;
+
+ virtual Rate capletRate(Rate effectiveCap, bool dailyCapFloor) const;
+ virtual Rate floorletRate(Rate effectiveCap, bool dailyCapFloor) const;
+
+ protected:
+ const OvernightIndexedCoupon* coupon_ = nullptr;
+ Handle capletVol_;
+ bool effectiveVolatilityInput_ = false;
+ mutable Real effectiveCapletVolatility_ = Null();
+ mutable Real effectiveFloorletVolatility_ = Null();
+ };
+
//! CompoudAveragedOvernightIndexedCouponPricer pricer
- class CompoundingOvernightIndexedCouponPricer : public FloatingRateCouponPricer {
+ class CompoundingOvernightIndexedCouponPricer : public OvernightIndexedCouponPricer {
public:
+ explicit CompoundingOvernightIndexedCouponPricer(
+ Handle v = Handle(),
+ const bool effectiveVolatilityInput = false);
//! \name FloatingRateCoupon interface
//@{
- void initialize(const FloatingRateCoupon& coupon) override;
+ //void initialize(const FloatingRateCoupon& coupon) override;
Rate swapletRate() const override;
Real swapletPrice() const override { QL_FAIL("swapletPrice not available"); }
Real capletPrice(Rate) const override { QL_FAIL("capletPrice not available"); }
@@ -48,29 +115,41 @@ namespace QuantLib {
Real floorletPrice(Rate) const override { QL_FAIL("floorletPrice not available"); }
Rate floorletRate(Rate) const override { QL_FAIL("floorletRate not available"); }
//@}
+ Rate capletRate(Rate effectiveCap, bool dailyCapFloor) const override {
+ QL_FAIL("CompoundingOvernightIndexedCouponPricer::capletRate(Rate, bool) not implemented");
+ }
+ Rate floorletRate(Rate effectiveCap, bool dailyCapFloor) const override {
+ QL_FAIL("CompoundingOvernightIndexedCouponPricer::floorletRate(Rate, bool) not implemented");
+ }
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
Reference: Katsumi Takada 2011, Valuation of Arithmetically Average of
Fed Funds Rates and Construction of the US Dollar Swap Yield Curve
*/
- class ArithmeticAveragedOvernightIndexedCouponPricer : public FloatingRateCouponPricer {
+ class ArithmeticAveragedOvernightIndexedCouponPricer : public OvernightIndexedCouponPricer {
public:
explicit ArithmeticAveragedOvernightIndexedCouponPricer(
Real meanReversion = 0.03,
Real volatility = 0.00, // NO convexity adjustment by default
- bool byApprox = false) // TRUE to use Katsumi Takada approximation
- : byApprox_(byApprox), mrs_(meanReversion), vol_(volatility) {}
+ bool byApprox = false, // TRUE to use Katsumi Takada approximation
+ Handle v = Handle(),
+ const bool effectiveVolatilityInput = false)
+ : OvernightIndexedCouponPricer(v, effectiveVolatilityInput),
+ byApprox_(byApprox), mrs_(meanReversion), vol_(volatility) {}
explicit ArithmeticAveragedOvernightIndexedCouponPricer(
bool byApprox) // Simplified constructor assuming no convexity correction
: ArithmeticAveragedOvernightIndexedCouponPricer(0.03, 0.0, byApprox) {}
- void initialize(const FloatingRateCoupon& coupon) override;
+ //void initialize(const FloatingRateCoupon& coupon) override;
Rate swapletRate() const override;
Real swapletPrice() const override { QL_FAIL("swapletPrice not available"); }
Real capletPrice(Rate) const override { QL_FAIL("capletPrice not available"); }
@@ -78,10 +157,15 @@ namespace QuantLib {
Real floorletPrice(Rate) const override { QL_FAIL("floorletPrice not available"); }
Rate floorletRate(Rate) const override { QL_FAIL("floorletRate not available"); }
+ Rate capletRate(Rate effectiveCap, bool dailyCapFloor) const override {
+ QL_FAIL("ArithmeticAveragedOvernightIndexedCouponPricer::capletRate(Rate, bool) not implemented");
+ }
+ Rate floorletRate(Rate effectiveCap, bool dailyCapFloor) const override {
+ QL_FAIL("ArithmeticAveragedOvernightIndexedCouponPricer::floorletRate(Rate, bool) not implemented");
+ }
protected:
Real convAdj1(Time ts, Time te) const;
Real convAdj2(Time ts, Time te) const;
- const OvernightIndexedCoupon* coupon_;
bool byApprox_;
Real mrs_;
Real vol_;
diff --git a/test-suite/overnightindexedcoupon.cpp b/test-suite/overnightindexedcoupon.cpp
index 6ca1265149b..475bd13cffb 100644
--- a/test-suite/overnightindexedcoupon.cpp
+++ b/test-suite/overnightindexedcoupon.cpp
@@ -19,10 +19,18 @@
#include "toplevelfixture.hpp"
#include "utilities.hpp"
+#include
+#include
#include
+#include
+#include
#include
#include
#include
+#include
+#include
+#include
+#include
#include
using namespace QuantLib;
@@ -50,6 +58,20 @@ struct CommonVars {
telescopicValueDates, averaging, fixingDays, lockoutDays, applyObservationShift);
}
+ ext::shared_ptr makeSpreadedCoupon(Date startDate,
+ Date endDate,
+ Spread spread = 0.0001,
+ bool compoundSpreadDaily = 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, compoundSpreadDaily);
+ }
+
CommonVars(const Date& evaluationDate) {
today = evaluationDate;
@@ -110,6 +132,189 @@ struct CommonVars {
CommonVars() : CommonVars(Date(23, November, 2021)) {}
};
+struct BlackONPricerVars {
+ Date today;
+ Real notional = 1000000.0;
+ RelinkableHandle forecastCurve;
+ RelinkableHandle vol;
+ ext::shared_ptr sofr;
+ DayCounter dc;
+
+ BlackONPricerVars(const Date& evalDate = Date(1, July, 2025)) {
+ today = evalDate;
+ dc = Actual360();
+ Settings::instance().evaluationDate() = today;
+ auto optionletVol = makeQuoteHandle(0.1);
+
+ // Flat forward curve
+ forecastCurve.linkTo(flatRate(today, 0.04, dc));
+ sofr = ext::make_shared(forecastCurve);
+
+ // Flat volatility
+ vol.linkTo(ext::make_shared(today, TARGET(), Following, optionletVol, dc));
+ }
+
+ ext::shared_ptr makeBaseCoupon(Date start, Date end,
+ RateAveraging::Type avgMethod = RateAveraging::Compound) {
+ auto onCoupon = ext::make_shared(
+ end, notional, start, end, sofr, 1.0, 0.0, Date(), Date(), dc,
+ false, avgMethod, Null(), 0, false,
+ false);
+
+ if (avgMethod == RateAveraging::Compound)
+ onCoupon->setPricer(ext::make_shared());
+ else
+ onCoupon->setPricer(ext::make_shared());
+
+ return onCoupon;
+ }
+
+ ext::shared_ptr makeCoupon(Date start, Date end, Rate cap = Null(), Rate floor = Null(),
+ RateAveraging::Type avgMethod = RateAveraging::Compound) {
+ auto onCoupon = makeBaseCoupon(start, end, avgMethod);
+
+ return ext::make_shared(onCoupon, cap, floor);
+ }
+};
+
+
+struct CommonVarsONLeg {
+ Date today;
+ Real notional = 1000000.0;
+ ext::shared_ptr sofr;
+ RelinkableHandle forecastCurve;
+ Schedule legSchedule;
+ DayCounter dc;
+ RelinkableHandle rateVolTS;
+
+ ext::shared_ptr returnRateVolTS() {
+ auto optionletVol = makeQuoteHandle(0.05);
+ return ext::make_shared(today, TARGET(), Following, optionletVol, 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);
+ }
+
+ if (!caps.empty() || !floors.empty()) {
+ rateVolTS.linkTo(returnRateVolTS());
+ if (averaging == RateAveraging::Compound)
+ leg.withCouponPricer(ext::make_shared(rateVolTS));
+ else
+ leg.withCouponPricer(ext::make_shared(rateVolTS));
+ }
+
+ 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(3, 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 +340,29 @@ 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 compounded spread...");
+
+ CommonVars vars;
+
+ // coupon entirely in the past
+ auto pastCoupon = vars.makeSpreadedCoupon(Date(18, October, 2021),
+ Date(18, November, 2021),
+ 0.0001);
+ auto pastCouponCompoundingSpread = 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 expectedRate = 0.0010871445057780704;
+ Real expectedAmount = vars.notional * expectedRate * 31.0/360;
+ CHECK_OIS_COUPON_RESULT("coupon rate", pastCoupon->rate(), expectedRate, 1e-12);
+ CHECK_OIS_COUPON_RESULT("coupon amount", pastCoupon->amount(), expectedAmount, 1e-8);
+
+ expectedRate = 0.0010871361040194164;
+ CHECK_OIS_COUPON_RESULT("coupon rate", pastCouponCompoundingSpread->rate(), expectedRate, 1e-12);
+}
+
BOOST_AUTO_TEST_CASE(testCurrentCouponRate) {
BOOST_TEST_MESSAGE("Testing rate for current overnight-indexed coupon...");
@@ -534,6 +762,344 @@ BOOST_AUTO_TEST_CASE(testErrorWhenLookbackOrLockoutAppliedForSimpleAveraging) {
Error);
}
+BOOST_AUTO_TEST_CASE(testBlackOvernightIndexedCouponPricerCapletFloorlet) {
+ BOOST_TEST_MESSAGE("Testing Black compounding overnight-indexed coupon pricer...");
+
+ BlackONPricerVars vars;
+ Date start = Date(1, July, 2035);
+ Date end = Date(1, October, 2035);
+
+ // Vanilla
+ auto vanillaCoupon = vars.makeBaseCoupon(start, end);
+ Rate expectedRate = vanillaCoupon->rate();
+
+ auto pricer = ext::make_shared(vars.vol);
+ vanillaCoupon->setPricer(pricer);
+
+ Rate rate = vanillaCoupon->rate();
+ CHECK_OIS_COUPON_RESULT("Base Rate", rate, expectedRate, 1e-8);
+
+ // Caplet
+ Rate cap = 0.045;
+ auto cappedCoupon = vars.makeCoupon(start, end, cap, Null());
+ cappedCoupon->setPricer(pricer);
+
+ rate = cappedCoupon->rate();
+ expectedRate = 0.036604717;
+ BOOST_CHECK(rate <= cap + 1e-8); // Should not exceed cap
+ CHECK_OIS_COUPON_RESULT("Capped Rate", rate, expectedRate, 1e-8);
+
+ // Floorlet
+ Rate floor = 0.035;
+ auto flooredCoupon = vars.makeCoupon(start, end, Null(), floor);
+ flooredCoupon->setPricer(pricer);
+ BOOST_CHECK(!flooredCoupon->isCalculated());
+
+ rate = flooredCoupon->rate();
+ expectedRate = 0.042502070;
+ BOOST_CHECK(rate >= floor - 1e-8); // Should not be below floor
+ CHECK_OIS_COUPON_RESULT("Floored Rate", rate, expectedRate, 1e-8);
+
+ // Capped and Floored
+ auto cappedFlooredCoupon = vars.makeCoupon(start, end, cap, floor);
+ cappedFlooredCoupon->setPricer(pricer);
+
+ rate = cappedFlooredCoupon->rate();
+ expectedRate = 0.039340869;
+ BOOST_CHECK(rate <= cap + 1e-8 && rate >= floor - 1e-8);
+ CHECK_OIS_COUPON_RESULT("Capped and Floored Rate", rate, expectedRate, 1e-8);
+}
+
+BOOST_AUTO_TEST_CASE(testBlackAverageONIndexedCouponPricerCapletFloorlet) {
+ BOOST_TEST_MESSAGE("Testing Black averaging overnight-indexed coupon pricer...");
+
+ BlackONPricerVars vars;
+ Date start = Date(1, July, 2035);
+ Date end = Date(1, October, 2035);
+
+ // Vanilla
+ auto vanillaCoupon = vars.makeBaseCoupon(start, end, RateAveraging::Simple);
+ Rate expectedRate = vanillaCoupon->rate();
+
+ auto pricer = ext::make_shared(vars.vol);
+ vanillaCoupon->setPricer(pricer);
+
+ Rate rate = vanillaCoupon->rate();
+ CHECK_OIS_COUPON_RESULT("Base Rate", rate, expectedRate, 1e-8);
+
+ // Caplet
+ Rate cap = 0.045;
+ auto cappedCoupon = vars.makeCoupon(start, end, cap, Null(), RateAveraging::Simple);
+ cappedCoupon->setPricer(pricer);
+
+ rate = cappedCoupon->rate();
+ expectedRate = 0.036488300;
+ BOOST_CHECK(rate <= cap + 1e-8);
+ CHECK_OIS_COUPON_RESULT("Capped Rate", rate, expectedRate, 1e-8);
+
+ // Floorlet
+ Rate floor = 0.035;
+ auto flooredCoupon = vars.makeCoupon(start, end, Null(), floor, RateAveraging::Simple);
+ flooredCoupon->setPricer(pricer);
+
+ rate = flooredCoupon->rate();
+ expectedRate = 0.042362746;
+ BOOST_CHECK(rate >= floor - 1e-8);
+ CHECK_OIS_COUPON_RESULT("Capped Rate", rate, expectedRate, 1e-8);
+
+ // Capped and Floored
+ auto cappedFlooredCoupon = vars.makeCoupon(start, end, cap, floor, RateAveraging::Simple);
+ cappedFlooredCoupon->setPricer(pricer);
+
+ rate = cappedFlooredCoupon->rate();
+ expectedRate = 0.039281553;
+ BOOST_CHECK(rate <= cap + 1e-8 && rate >= floor - 1e-8);
+ CHECK_OIS_COUPON_RESULT("Capped and Floored Rate", rate, expectedRate, 1e-8);
+}
+
+BOOST_AUTO_TEST_CASE(testBlackONPricerConsistencyWithNoVol) {
+ BOOST_TEST_MESSAGE("Testing Black compounding pricer with zero volatility (should match vanilla pricer)...");
+
+ BlackONPricerVars vars;
+ auto optionletVol = makeQuoteHandle(0.0);
+ vars.vol.linkTo(ext::make_shared(vars.today, TARGET(), Following, optionletVol, vars.dc));
+ Date start = Date(1, July, 2035);
+ Date end = Date(1, October, 2035);
+
+ auto cappedFlooredCoupon = vars.makeCoupon(start, end, 0.045, 0.035);
+ auto blackPricer = ext::make_shared(vars.vol);
+ cappedFlooredCoupon->setPricer(blackPricer);
+ Rate blackRate = cappedFlooredCoupon->rate();
+
+ // Compare with standard compounding pricer
+ auto baseONCoupon = vars.makeBaseCoupon(start, end);
+ baseONCoupon->setPricer(ext::make_shared());
+ Rate vanillaRate = baseONCoupon->rate();
+
+ CHECK_OIS_COUPON_RESULT("Zero capped coupon rate", blackRate, vanillaRate, 1e-10);
+
+ baseONCoupon->setPricer(blackPricer);
+ vanillaRate = baseONCoupon->rate();
+ CHECK_OIS_COUPON_RESULT("Zero capped coupon rate (same pricer)", blackRate, vanillaRate, 1e-10);
+}
+
+BOOST_AUTO_TEST_CASE(testBlackONAveragingPricerConsistencyWithNoVol) {
+ BOOST_TEST_MESSAGE("Testing Black averaging pricer with zero volatility (should match vanilla pricer)...");
+
+ BlackONPricerVars vars;
+ auto optionletVol = makeQuoteHandle(0.0);
+ vars.vol.linkTo(ext::make_shared(vars.today, TARGET(), Following, optionletVol, vars.dc));
+ Date start = Date(1, July, 2035);
+ Date end = Date(1, October, 2035);
+
+ auto cappedFlooredCoupon = vars.makeCoupon(start, end, 0.045, 0.035, RateAveraging::Simple);
+ auto blackPricer = ext::make_shared(vars.vol);
+ cappedFlooredCoupon->setPricer(blackPricer);
+ Rate blackRate = cappedFlooredCoupon->rate();
+
+ // Compare with standard compounding pricer
+ auto baseONCoupon = vars.makeBaseCoupon(start, end, RateAveraging::Simple);
+ baseONCoupon->setPricer(ext::make_shared());
+ Rate vanillaRate = baseONCoupon->rate();
+
+ CHECK_OIS_COUPON_RESULT("Zero capped coupon rate", blackRate, vanillaRate, 1e-10);
+
+ baseONCoupon->setPricer(blackPricer);
+ vanillaRate = baseONCoupon->rate();
+ CHECK_OIS_COUPON_RESULT("Zero capped coupon rate (same pricer)", blackRate, vanillaRate, 1e-10);
+}
+
+BOOST_AUTO_TEST_CASE(testOvernightLegBasicFunctionality) {
+ BOOST_TEST_MESSAGE("Testing basic functionality of overnight leg...");
+
+ 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(), 4);
+
+ // 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 overnight leg construction 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 overnight leg construction 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 overnight leg construction 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 overnight leg construction with gearings and spreads...");
+
+ CommonVarsONLeg vars;
+ vars.setupForecastCurve();
+
+ std::vector gearings = {1.0, 1.25, 2.0, 0.5};
+ std::vector spreads = {0.0001, 0.0001, 0.0002, 0.0002};
+
+ Leg leg = vars.makeLeg(Null(), 0, false, false,
+ RateAveraging::Compound, gearings, spreads);
+
+ BOOST_CHECK_EQUAL(leg.size(), 4);
+
+ for (Size i = 0; i < leg.size(); ++i) {
+ auto oisCoupon = ext::dynamic_pointer_cast