From 4182cd09994476eddfcb271ac8da3cba8a7952d9 Mon Sep 17 00:00:00 2001 From: Luigi Ballabio Date: Thu, 21 Nov 2024 12:25:38 +0100 Subject: [PATCH] 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) {