From c56549c813e46a6aed8a3cf9c397377d1e3c160e Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Tue, 19 Nov 2024 17:58:15 +0100 Subject: [PATCH 1/2] Unify asset swap constructors --- ql/instruments/assetswap.cpp | 251 ++++++++++++----------------------- ql/instruments/assetswap.hpp | 12 +- 2 files changed, 93 insertions(+), 170 deletions(-) diff --git a/ql/instruments/assetswap.cpp b/ql/instruments/assetswap.cpp index 47a05b030c4..e73c084ab4c 100644 --- a/ql/instruments/assetswap.cpp +++ b/ql/instruments/assetswap.cpp @@ -42,124 +42,9 @@ namespace QuantLib { const DayCounter& floatingDayCounter, Date dealMaturity, bool payBondCoupon) - : Swap(2), bond_(std::move(bond)), bondCleanPrice_(bondCleanPrice), - nonParRepayment_(nonParRepayment), spread_(spread), parSwap_(parSwap) { - Schedule tempSch(bond_->settlementDate(), - bond_->maturityDate(), - iborIndex->tenor(), - iborIndex->fixingCalendar(), - iborIndex->businessDayConvention(), - iborIndex->businessDayConvention(), - DateGeneration::Backward, - false); // endOfMonth - if (dealMaturity==Date()) - dealMaturity = bond_->maturityDate(); - QL_REQUIRE(dealMaturity <= tempSch.dates().back(), - "deal maturity " << dealMaturity << - " cannot be later than (adjusted) bond maturity " << - tempSch.dates().back()); - QL_REQUIRE(dealMaturity > tempSch.dates()[0], - "deal maturity " << dealMaturity << - " must be later than swap start date " << - tempSch.dates()[0]); - - // the following might become an input parameter - BusinessDayConvention paymentAdjustment = Following; - - Date finalDate = tempSch.calendar().adjust( - dealMaturity, paymentAdjustment); - Schedule schedule = tempSch.until(finalDate); - - // bondCleanPrice must be the (forward) clean price - // at the floating schedule start date - upfrontDate_ = schedule.startDate(); - Real dirtyPrice = bondCleanPrice_ + - bond_->accruedAmount(upfrontDate_); - - Real notional = bond_->notional(upfrontDate_); - /* In the market asset swap, the bond is purchased in return for - payment of the full price. The notional of the floating leg is - then scaled by the full price. */ - if (!parSwap_) - notional *= dirtyPrice/100.0; - - if (floatingDayCounter==DayCounter()) - legs_[1] = IborLeg(schedule, iborIndex) - .withNotionals(notional) - .withPaymentAdjustment(paymentAdjustment) - .withGearings(gearing) - .withSpreads(spread); - else - legs_[1] = IborLeg(schedule, iborIndex) - .withNotionals(notional) - .withPaymentDayCounter(floatingDayCounter) - .withPaymentAdjustment(paymentAdjustment) - .withGearings(gearing) - .withSpreads(spread); - - Leg::const_iterator i; - for (i=legs_[1].begin(); icashflows(); - // skip bond redemption - for (i = bondLeg.begin(); idate()<=dealMaturity; ++i) { - // whatever might be the choice for the discounting engine - // bond flows on upfrontDate_ must be discarded - bool upfrontDateBondFlows = false; - if (!(*i)->hasOccurred(upfrontDate_, upfrontDateBondFlows)) - legs_[0].push_back(*i); - } - // if the first skipped cashflow is not the redemption - // and it is a coupon then add the accrued coupon - if (i c = ext::dynamic_pointer_cast(*i); - if (c != nullptr) { - ext::shared_ptr accruedCoupon(new - SimpleCashFlow(c->accruedAmount(dealMaturity), finalDate)); - legs_[0].push_back(accruedCoupon); - } - } - // add the nonParRepayment_ - ext::shared_ptr nonParRepaymentFlow(new - SimpleCashFlow(nonParRepayment_, finalDate)); - legs_[0].push_back(nonParRepaymentFlow); - - QL_REQUIRE(!legs_[0].empty(), - "empty bond leg to start with"); - - // special flows - if (parSwap_) { - // upfront on the floating leg - Real upfront = (dirtyPrice-100.0)/100.0*notional; - ext::shared_ptr upfrontCashFlow(new - SimpleCashFlow(upfront, upfrontDate_)); - legs_[1].insert(legs_[1].begin(), upfrontCashFlow); - // backpayment on the floating leg - // (accounts for non-par redemption, if any) - Real backPayment = notional; - ext::shared_ptr backPaymentCashFlow(new - SimpleCashFlow(backPayment, finalDate)); - legs_[1].push_back(backPaymentCashFlow); - } else { - // final notional exchange - ext::shared_ptr finalCashFlow (new - SimpleCashFlow(notional, finalDate)); - legs_[1].push_back(finalCashFlow); - } - - QL_REQUIRE(!legs_[0].empty(), "empty bond leg"); - for (i=legs_[0].begin(); i bond, @@ -168,9 +53,13 @@ namespace QuantLib { Spread spread, Schedule floatSchedule, const DayCounter& floatingDayCounter, - bool parSwap) - : Swap(2), bond_(std::move(bond)), bondCleanPrice_(bondCleanPrice), nonParRepayment_(100), - spread_(spread), parSwap_(parSwap) { + bool parSwap, + Real gearing, + Real nonParRepayment, + Date dealMaturity) + : Swap(2), bond_(std::move(bond)), bondCleanPrice_(bondCleanPrice), + nonParRepayment_(nonParRepayment), spread_(spread), parSwap_(parSwap) { + Schedule schedule = floatSchedule.empty() ? Schedule(bond_->settlementDate(), bond_->maturityDate(), @@ -182,25 +71,28 @@ namespace QuantLib { false) // endOfMonth : std::move(floatSchedule); + if (dealMaturity == Date()) + dealMaturity = schedule.back(); + QL_REQUIRE(dealMaturity <= schedule.back(), + "deal maturity " << dealMaturity << + " cannot be later than (adjusted) bond maturity " << + schedule.back()); + QL_REQUIRE(dealMaturity > schedule.front(), + "deal maturity " << dealMaturity << + " must be later than swap start date " << + schedule.front()); + // the following might become an input parameter BusinessDayConvention paymentAdjustment = Following; Date finalDate = schedule.calendar().adjust( - schedule.endDate(), paymentAdjustment); - Date adjBondMaturityDate = schedule.calendar().adjust( - bond_->maturityDate(), paymentAdjustment); - - QL_REQUIRE(finalDate==adjBondMaturityDate, - "adjusted schedule end date (" << - finalDate << - ") must be equal to adjusted bond maturity date (" << - adjBondMaturityDate << ")"); + dealMaturity, paymentAdjustment); + schedule = schedule.until(finalDate); // bondCleanPrice must be the (forward) clean price // at the floating schedule start date upfrontDate_ = schedule.startDate(); - Real dirtyPrice = bondCleanPrice_ + - bond_->accruedAmount(upfrontDate_); + Real dirtyPrice = bondCleanPrice_ + bond_->accruedAmount(upfrontDate_); Real notional = bond_->notional(upfrontDate_); /* In the market asset swap, the bond is purchased in return for @@ -209,56 +101,79 @@ namespace QuantLib { if (!parSwap_) notional *= dirtyPrice/100.0; - if (floatingDayCounter==DayCounter()) - legs_[1] = IborLeg(std::move(schedule), iborIndex) - .withNotionals(notional) - .withPaymentAdjustment(paymentAdjustment) - .withSpreads(spread); - else - legs_[1] = IborLeg(std::move(schedule), iborIndex) - .withNotionals(notional) - .withPaymentDayCounter(floatingDayCounter) - .withPaymentAdjustment(paymentAdjustment) - .withSpreads(spread); - - for (auto i=legs_[1].begin(); icashflows(); - for (auto i = bondLeg.begin(); i < bondLeg.end(); ++i) { - // whatever might be the choice for the discounting engine - // bond flows on upfrontDate_ must be discarded - bool upfrontDateBondFlows = false; - if (!(*i)->hasOccurred(upfrontDate_, upfrontDateBondFlows)) + QL_REQUIRE(!bondLeg.empty(), "no cashflows from bond"); + + bool includeOnUpfrontDate = false; // a cash flow on the upfront + // date must be discarded + + // add coupons for the time being, not the redemption + Leg::const_iterator i; + for (i = bondLeg.begin(); i < bondLeg.end()-1 && (*i)->date()<=dealMaturity; ++i) { + if (!(*i)->hasOccurred(upfrontDate_, includeOnUpfrontDate)) legs_[0].push_back(*i); } - QL_REQUIRE(!legs_[0].empty(), - "empty bond leg to start with"); + // if we're skipping a cashflow before the redemption + // and it's a coupon, then add the accrued coupon. + if (i < bondLeg.end()-1) { + auto c = ext::dynamic_pointer_cast(*i); + if (c != nullptr) { + Real accruedAmount = c->accruedAmount(dealMaturity); + auto accruedCoupon = + ext::make_shared(accruedAmount, finalDate); + legs_[0].push_back(accruedCoupon); + } + } + + // add the redemption, or whatever the final payment is + if (nonParRepayment_ == Null()) { + auto redemption = bondLeg.back(); + auto finalFlow = + ext::make_shared(redemption->amount(), finalDate); + legs_[0].push_back(finalFlow); + nonParRepayment_ = 100.0; + } else { + auto finalFlow = + ext::make_shared(nonParRepayment_, finalDate); + legs_[0].push_back(finalFlow); + } + + /******** Floating leg ********/ + + legs_[1] = + IborLeg(std::move(schedule), iborIndex) + .withNotionals(notional) + .withPaymentAdjustment(paymentAdjustment) + .withGearings(gearing) + .withSpreads(spread) + .withPaymentDayCounter(floatingDayCounter); - // special flows if (parSwap_) { - // upfront on the floating leg - Real upfront = (dirtyPrice-100.0)/100.0*notional; - ext::shared_ptr upfrontCashFlow(new - SimpleCashFlow(upfront, upfrontDate_)); + // upfront + Real upfront = (dirtyPrice-100.0)/100.0 * notional; + auto upfrontCashFlow = + ext::make_shared(upfront, upfrontDate_); legs_[1].insert(legs_[1].begin(), upfrontCashFlow); - // backpayment on the floating leg - // (accounts for non-par redemption, if any) + // backpayment (accounts for non-par redemption, if any) Real backPayment = notional; - ext::shared_ptr backPaymentCashFlow(new - SimpleCashFlow(backPayment, finalDate)); + auto backPaymentCashFlow = + ext::make_shared(backPayment, finalDate); legs_[1].push_back(backPaymentCashFlow); } else { // final notional exchange - ext::shared_ptr finalCashFlow(new - SimpleCashFlow(notional, finalDate)); + auto finalCashFlow = + ext::make_shared(notional, finalDate); legs_[1].push_back(finalCashFlow); } - QL_REQUIRE(!legs_[0].empty(), "empty bond leg"); - for (auto i=legs_[0].begin(); i(), + Date dealMaturity = Date()); + + /*! \deprecated Use the other overload. + Deprecated in version 1.37. + */ + [[deprecated("Use the other overload")]] AssetSwap(bool parAssetSwap, ext::shared_ptr bond, Real bondCleanPrice, @@ -72,6 +79,7 @@ namespace QuantLib { const DayCounter& floatingDayCount = DayCounter(), Date dealMaturity = Date(), bool payBondCoupon = false); + // results Spread fairSpread() const; Real floatingLegBPS() const; From 4182cd09994476eddfcb271ac8da3cba8a7952d9 Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Thu, 21 Nov 2024 12:25:38 +0100 Subject: [PATCH 2/2] Enable using overnight rates in asset swap --- ql/instruments/assetswap.cpp | 35 +++++-- ql/instruments/assetswap.hpp | 5 + test-suite/assetswap.cpp | 178 +++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 9 deletions(-) diff --git a/ql/instruments/assetswap.cpp b/ql/instruments/assetswap.cpp index e73c084ab4c..b5fe15eb28e 100644 --- a/ql/instruments/assetswap.cpp +++ b/ql/instruments/assetswap.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,12 @@ namespace QuantLib { : Swap(2), bond_(std::move(bond)), bondCleanPrice_(bondCleanPrice), nonParRepayment_(nonParRepayment), spread_(spread), parSwap_(parSwap) { + auto overnight = ext::dynamic_pointer_cast(iborIndex); + if (overnight) { + QL_REQUIRE(!floatSchedule.empty(), + "floating schedule is needed when using an overnight index"); + } + Schedule schedule = floatSchedule.empty() ? Schedule(bond_->settlementDate(), bond_->maturityDate(), @@ -102,11 +109,11 @@ namespace QuantLib { notional *= dirtyPrice/100.0; /******** Bond leg ********/ - + const Leg& bondLeg = bond_->cashflows(); QL_REQUIRE(!bondLeg.empty(), "no cashflows from bond"); - bool includeOnUpfrontDate = false; // a cash flow on the upfront + bool includeOnUpfrontDate = false; // a cash flow on the upfront // date must be discarded // add coupons for the time being, not the redemption @@ -143,13 +150,23 @@ namespace QuantLib { /******** Floating leg ********/ - legs_[1] = - IborLeg(std::move(schedule), iborIndex) - .withNotionals(notional) - .withPaymentAdjustment(paymentAdjustment) - .withGearings(gearing) - .withSpreads(spread) - .withPaymentDayCounter(floatingDayCounter); + if (overnight) { + legs_[1] = + OvernightLeg(std::move(schedule), overnight) + .withNotionals(notional) + .withPaymentAdjustment(paymentAdjustment) + .withGearings(gearing) + .withSpreads(spread) + .withPaymentDayCounter(floatingDayCounter); + } else { + legs_[1] = + IborLeg(std::move(schedule), iborIndex) + .withNotionals(notional) + .withPaymentAdjustment(paymentAdjustment) + .withGearings(gearing) + .withSpreads(spread) + .withPaymentDayCounter(floatingDayCounter); + } if (parSwap_) { // upfront diff --git a/ql/instruments/assetswap.hpp b/ql/instruments/assetswap.hpp index f52b523d530..27daf366f22 100644 --- a/ql/instruments/assetswap.hpp +++ b/ql/instruments/assetswap.hpp @@ -53,6 +53,11 @@ namespace QuantLib { class arguments; class results; + /*! If the passed iborIndex is an overnight rate such as + SOFR, ESTR or SONIA, the floatSchedule argument is + required and will be used to build overnight-indexed + coupons. + */ AssetSwap(bool payBondCoupon, ext::shared_ptr bond, Real bondCleanPrice, diff --git a/test-suite/assetswap.cpp b/test-suite/assetswap.cpp index 3fb04306af1..240f8657ff7 100644 --- a/test-suite/assetswap.cpp +++ b/test-suite/assetswap.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -314,11 +315,104 @@ BOOST_AUTO_TEST_CASE(testConsistency) { "\n tolerance: " << tolerance); } + // using overnight index + + auto overnight = ext::make_shared(vars.termStructure); + Schedule overnightSchedule(bond->settlementDate(), + bond->maturityDate(), + 6 * Months, + overnight->fixingCalendar(), + overnight->businessDayConvention(), + overnight->businessDayConvention(), + DateGeneration::Backward, + false); + + parAssetSwap = AssetSwap(payFixedRate, + bond, bondPrice, + overnight, vars.spread, + overnightSchedule, + overnight->dayCounter(), + isPar); + + swapEngine = ext::make_shared( + vars.termStructure, + true, + bond->settlementDate(), + Settings::instance().evaluationDate()); + parAssetSwap.setPricingEngine(swapEngine); + fairCleanPrice = parAssetSwap.fairCleanPrice(); + fairSpread = parAssetSwap.fairSpread(); + assetSwap2 = AssetSwap(payFixedRate, + bond, fairCleanPrice, + overnight, vars.spread, + overnightSchedule, + overnight->dayCounter(), + isPar); + assetSwap2.setPricingEngine(swapEngine); + if (std::fabs(assetSwap2.NPV())>tolerance) { + BOOST_FAIL("\npar asset swap fair clean price doesn't zero the NPV: " << + std::fixed << std::setprecision(4) << + "\n clean price: " << bondPrice << + "\n fair clean price: " << fairCleanPrice << + "\n NPV: " << assetSwap2.NPV() << + "\n tolerance: " << tolerance); + } + if (std::fabs(assetSwap2.fairCleanPrice() - fairCleanPrice)>tolerance) { + BOOST_FAIL("\npar asset swap fair clean price doesn't equal input " + "clean price at zero NPV: " << + std::fixed << std::setprecision(4) << + "\n input clean price: " << fairCleanPrice << + "\n fair clean price: " << assetSwap2.fairCleanPrice() << + "\n NPV: " << assetSwap2.NPV() << + "\n tolerance: " << tolerance); + } + if (std::fabs(assetSwap2.fairSpread() - vars.spread)>tolerance) { + BOOST_FAIL("\npar asset swap fair spread doesn't equal input spread " + "at zero NPV: " << std::fixed << std::setprecision(4) << + "\n input spread: " << vars.spread << + "\n fair spread: " << assetSwap2.fairSpread() << + "\n NPV: " << assetSwap2.NPV() << + "\n tolerance: " << tolerance); + } + + assetSwap3 = AssetSwap(payFixedRate, + bond, bondPrice, + overnight, fairSpread, + overnightSchedule, + overnight->dayCounter(), + isPar); + assetSwap3.setPricingEngine(swapEngine); + if (std::fabs(assetSwap3.NPV())>tolerance) { + BOOST_FAIL("\npar asset swap fair spread doesn't zero the NPV: " << + std::fixed << std::setprecision(4) << + "\n spread: " << vars.spread << + "\n fair spread: " << fairSpread << + "\n NPV: " << assetSwap3.NPV() << + "\n tolerance: " << tolerance); + } + if (std::fabs(assetSwap3.fairCleanPrice() - bondPrice)>tolerance) { + BOOST_FAIL("\npar asset swap fair clean price doesn't equal input " + "clean price at zero NPV: " << + std::fixed << std::setprecision(4) << + "\n input clean price: " << bondPrice << + "\n fair clean price: " << assetSwap3.fairCleanPrice() << + "\n NPV: " << assetSwap3.NPV() << + "\n tolerance: " << tolerance); + } + if (std::fabs(assetSwap3.fairSpread() - fairSpread)>tolerance) { + BOOST_FAIL("\npar asset swap fair spread doesn't equal input spread at" + " zero NPV: " << std::fixed << std::setprecision(4) << + "\n input spread: " << fairSpread << + "\n fair spread: " << assetSwap3.fairSpread() << + "\n NPV: " << assetSwap3.NPV() << + "\n tolerance: " << tolerance); + } // now market asset swap + isPar = false; AssetSwap mktAssetSwap(payFixedRate, bond, bondPrice, @@ -493,6 +587,90 @@ BOOST_AUTO_TEST_CASE(testConsistency) { "\n tolerance: " << tolerance); } + // using overnight index + + mktAssetSwap = AssetSwap(payFixedRate, + bond, bondPrice, + overnight, vars.spread, + overnightSchedule, + overnight->dayCounter(), + isPar); + + swapEngine = ext::make_shared( + vars.termStructure, + true, + bond->settlementDate(), + Settings::instance().evaluationDate()); + + mktAssetSwap.setPricingEngine(swapEngine); + fairCleanPrice = mktAssetSwap.fairCleanPrice(); + fairSpread = mktAssetSwap.fairSpread(); + + assetSwap4 = AssetSwap(payFixedRate, + bond, fairCleanPrice, + overnight, vars.spread, + overnightSchedule, + overnight->dayCounter(), + isPar); + assetSwap4.setPricingEngine(swapEngine); + if (std::fabs(assetSwap4.NPV())>tolerance) { + BOOST_FAIL("\nmarket asset swap fair clean price doesn't zero the NPV: " << + std::fixed << std::setprecision(4) << + "\n clean price: " << bondPrice << + "\n fair clean price: " << fairCleanPrice << + "\n NPV: " << assetSwap4.NPV() << + "\n tolerance: " << tolerance); + } + if (std::fabs(assetSwap4.fairCleanPrice() - fairCleanPrice)>tolerance) { + BOOST_FAIL("\nmarket asset swap fair clean price doesn't equal input " + "clean price at zero NPV: " << + std::fixed << std::setprecision(4) << + "\n input clean price: " << fairCleanPrice << + "\n fair clean price: " << assetSwap4.fairCleanPrice() << + "\n NPV: " << assetSwap4.NPV() << + "\n tolerance: " << tolerance); + } + if (std::fabs(assetSwap4.fairSpread() - vars.spread)>tolerance) { + BOOST_FAIL("\nmarket asset swap fair spread doesn't equal input spread" + " at zero NPV: " << std::fixed << std::setprecision(4) << + "\n input spread: " << vars.spread << + "\n fair spread: " << assetSwap4.fairSpread() << + "\n NPV: " << assetSwap4.NPV() << + "\n tolerance: " << tolerance); + } + + assetSwap5 = AssetSwap(payFixedRate, + bond, bondPrice, + overnight, fairSpread, + overnightSchedule, + overnight->dayCounter(), + isPar); + assetSwap5.setPricingEngine(swapEngine); + if (std::fabs(assetSwap5.NPV())>tolerance) { + BOOST_FAIL("\nmarket asset swap fair spread doesn't zero the NPV: " << + std::fixed << std::setprecision(4) << + "\n spread: " << vars.spread << + "\n fair spread: " << fairSpread << + "\n NPV: " << assetSwap5.NPV() << + "\n tolerance: " << tolerance); + } + if (std::fabs(assetSwap5.fairCleanPrice() - bondPrice)>tolerance) { + BOOST_FAIL("\nmarket asset swap fair clean price doesn't equal input " + "clean price at zero NPV: " << + std::fixed << std::setprecision(4) << + "\n input clean price: " << bondPrice << + "\n fair clean price: " << assetSwap5.fairCleanPrice() << + "\n NPV: " << assetSwap5.NPV() << + "\n tolerance: " << tolerance); + } + if (std::fabs(assetSwap5.fairSpread() - fairSpread)>tolerance) { + BOOST_FAIL("\nmarket asset swap fair spread doesn't equal input spread at zero NPV: " << + std::fixed << std::setprecision(4) << + "\n input spread: " << fairSpread << + "\n fair spread: " << assetSwap5.fairSpread() << + "\n NPV: " << assetSwap5.NPV() << + "\n tolerance: " << tolerance); + } } BOOST_AUTO_TEST_CASE(testImpliedValue) {