diff --git a/ql/cashflows/couponpricer.cpp b/ql/cashflows/couponpricer.cpp index d77c5572c78..65e8239646a 100644 --- a/ql/cashflows/couponpricer.cpp +++ b/ql/cashflows/couponpricer.cpp @@ -119,18 +119,25 @@ namespace QuantLib { Handle rateCurve = index_->forwardingTermStructure(); - Date paymentDate = coupon_->date(); - if (paymentDate > rateCurve->referenceDate()) - discount_ = rateCurve->discount(paymentDate); - else - discount_ = 1.0; - - spreadLegValue_ = spread_ * accrualPeriod_ * discount_; + if (rateCurve.empty()) { + discount_ = Null(); // might not be needed, will be checked later + QL_DEPRECATED_DISABLE_WARNING + spreadLegValue_ = Null(); + QL_DEPRECATED_ENABLE_WARNING + } else { + Date paymentDate = coupon_->date(); + if (paymentDate > rateCurve->referenceDate()) + discount_ = rateCurve->discount(paymentDate); + else + discount_ = 1.0; + QL_DEPRECATED_DISABLE_WARNING + spreadLegValue_ = spread_ * accrualPeriod_ * discount_; + QL_DEPRECATED_ENABLE_WARNING + } } - Real BlackIborCouponPricer::optionletPrice(Option::Type optionType, - Real effStrike) const { + Real BlackIborCouponPricer::optionletRate(Option::Type optionType, Real effStrike) const { if (fixingDate_ <= Settings::instance().evaluationDate()) { // the amount is determined Real a, b; @@ -141,7 +148,7 @@ namespace QuantLib { a = effStrike; b = coupon_->indexFixing(); } - return std::max(a - b, 0.0)* accrualPeriod_*discount_; + return std::max(a - b, 0.0); } else { // not yet determined, use Black model QL_REQUIRE(!capletVolatility().empty(), @@ -158,10 +165,16 @@ namespace QuantLib { stdDev, 1.0, shift) : bachelierBlackFormula(optionType, effStrike, adjustedFixing(), stdDev, 1.0); - return fixing * accrualPeriod_ * discount_; + return fixing; } } + Real BlackIborCouponPricer::optionletPrice(Option::Type optionType, + Real effStrike) const { + QL_REQUIRE(discount_ != Null(), "no forecast curve provided"); + return optionletRate(optionType, effStrike) * accrualPeriod_ * discount_; + } + Rate BlackIborCouponPricer::adjustedFixing(Rate fixing) const { if (fixing == Null()) diff --git a/ql/cashflows/couponpricer.hpp b/ql/cashflows/couponpricer.hpp index 3d4b4b3368f..3ae46b1ef49 100644 --- a/ql/cashflows/couponpricer.hpp +++ b/ql/cashflows/couponpricer.hpp @@ -102,6 +102,8 @@ namespace QuantLib { bool useIndexedCoupon_; }; + QL_DEPRECATED_DISABLE_WARNING + /*! Black-formula pricer for capped/floored Ibor coupons References for timing adjustments Black76 Hull, Options, Futures and other @@ -132,12 +134,19 @@ namespace QuantLib { Rate floorletRate(Rate effectiveFloor) const override; protected: - Real optionletPrice(Option::Type optionType, - Real effStrike) const; + Real optionletPrice(Option::Type optionType, Real effStrike) const; + Real optionletRate(Option::Type optionType, Real effStrike) const; virtual Rate adjustedFixing(Rate fixing = Null()) const; Real discount_; + + /*! \deprecated don't use this data member. Use spread_ instead + and calculate it on the fly if needed. But you + probably won't. + Deprecated in version 1.25. + */ + QL_DEPRECATED Real spreadLegValue_; private: @@ -145,6 +154,8 @@ namespace QuantLib { const Handle correlation_; }; + QL_DEPRECATED_ENABLE_WARNING + //! base pricer for vanilla CMS coupons class CmsCouponPricer : public FloatingRateCouponPricer { public: @@ -208,9 +219,8 @@ namespace QuantLib { inline Real BlackIborCouponPricer::swapletPrice() const { // past or future fixing is managed in InterestRateIndex::fixing() - - Real swapletPrice = adjustedFixing() * accrualPeriod_ * discount_; - return gearing_ * swapletPrice + spreadLegValue_; + QL_REQUIRE(discount_ != Null(), "no forecast curve provided"); + return swapletRate() * accrualPeriod_ * discount_; } inline Rate BlackIborCouponPricer::swapletRate() const { @@ -218,23 +228,23 @@ namespace QuantLib { } inline Real BlackIborCouponPricer::capletPrice(Rate effectiveCap) const { - Real capletPrice = optionletPrice(Option::Call, effectiveCap); - return gearing_ * capletPrice; + QL_REQUIRE(discount_ != Null(), "no forecast curve provided"); + return capletRate(effectiveCap) * accrualPeriod_ * discount_; } inline Rate BlackIborCouponPricer::capletRate(Rate effectiveCap) const { - return capletPrice(effectiveCap) / (accrualPeriod_*discount_); + return gearing_ * optionletRate(Option::Call, effectiveCap); } inline Real BlackIborCouponPricer::floorletPrice(Rate effectiveFloor) const { - Real floorletPrice = optionletPrice(Option::Put, effectiveFloor); - return gearing_ * floorletPrice; + QL_REQUIRE(discount_ != Null(), "no forecast curve provided"); + return floorletRate(effectiveFloor) * accrualPeriod_ * discount_; } inline Rate BlackIborCouponPricer::floorletRate(Rate effectiveFloor) const { - return floorletPrice(effectiveFloor) / (accrualPeriod_*discount_); + return gearing_ * optionletRate(Option::Put, effectiveFloor); } } diff --git a/test-suite/cashflows.cpp b/test-suite/cashflows.cpp index 26d478e9f66..a7d0afc7832 100644 --- a/test-suite/cashflows.cpp +++ b/test-suite/cashflows.cpp @@ -505,7 +505,39 @@ void CashFlowsTest::testPartialScheduleLegConstruction() { BOOST_CHECK_EQUAL(lastCpnF3->referencePeriodStart(), Date(25, Sep, 2020)); BOOST_CHECK_EQUAL(lastCpnF3->referencePeriodEnd(), Date(30, Sep, 2020)); } - + +void CashFlowsTest::testFixedIborCouponWithoutForecastCurve() { + BOOST_TEST_MESSAGE("Testing past ibor coupon without forecast curve..."); + + IndexHistoryCleaner cleaner; + + Date today = Settings::instance().evaluationDate(); + + auto index = ext::make_shared(6*Months); + auto calendar = index->fixingCalendar(); + + Date fixingDate = calendar.advance(today, -2, Months); + Rate pastFixing = 0.01; + index->addFixing(fixingDate, pastFixing); + + Date startDate = index->valueDate(fixingDate); + Date endDate = index->maturityDate(fixingDate); + + IborCoupon coupon(endDate, 100.0, startDate, endDate, index->fixingDays(), index); + coupon.setPricer(ext::make_shared()); + + BOOST_CHECK_NO_THROW(coupon.amount()); + + // the main check is the one above, but let's check for consistency too: + Real amount = coupon.amount(); + Real expected = pastFixing * coupon.nominal() * coupon.accrualPeriod(); + if (std::fabs(amount - expected) > 1e-8) { + BOOST_ERROR("amount mismatch:" + << "\n calculated: " << amount + << "\n expected: " << expected); + } +} + test_suite* CashFlowsTest::suite() { auto* suite = BOOST_TEST_SUITE("Cash flows tests"); suite->add(QUANTLIB_TEST_CASE(&CashFlowsTest::testSettings)); @@ -519,6 +551,7 @@ test_suite* CashFlowsTest::suite() { suite->add(QUANTLIB_TEST_CASE(&CashFlowsTest::testIrregularFirstCouponReferenceDatesAtEndOfMonth)); suite->add(QUANTLIB_TEST_CASE(&CashFlowsTest::testIrregularLastCouponReferenceDatesAtEndOfMonth)); suite->add(QUANTLIB_TEST_CASE(&CashFlowsTest::testPartialScheduleLegConstruction)); + suite->add(QUANTLIB_TEST_CASE(&CashFlowsTest::testFixedIborCouponWithoutForecastCurve)); return suite; } diff --git a/test-suite/cashflows.hpp b/test-suite/cashflows.hpp index c5d4cc31aa0..345639a5fee 100644 --- a/test-suite/cashflows.hpp +++ b/test-suite/cashflows.hpp @@ -32,6 +32,7 @@ class CashFlowsTest { static void testIrregularFirstCouponReferenceDatesAtEndOfMonth(); static void testIrregularLastCouponReferenceDatesAtEndOfMonth(); static void testPartialScheduleLegConstruction(); + static void testFixedIborCouponWithoutForecastCurve(); static boost::unit_test_framework::test_suite* suite(); };