From af4d04446a3313501132ad696639b57bb71a7fc3 Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Fri, 13 Jun 2025 16:15:16 +0200 Subject: [PATCH 01/18] Added base CrossCcySwap, CrossCcySwapEngine and related tests --- QuantLib.vcxproj | 4 + QuantLib.vcxproj.filters | 12 + ql/CMakeLists.txt | 4 + ql/instruments/Makefile.am | 2 + ql/instruments/crossccyswap.cpp | 107 +++ ql/instruments/crossccyswap.hpp | 147 ++++ ql/pricingengines/swap/Makefile.am | 2 + ql/pricingengines/swap/crossccyswapengine.cpp | 165 +++++ ql/pricingengines/swap/crossccyswapengine.hpp | 107 +++ test-suite/CMakeLists.txt | 1 + test-suite/Makefile.am | 1 + test-suite/crossccyswap.cpp | 698 ++++++++++++++++++ test-suite/testsuite.vcxproj | 1 + test-suite/testsuite.vcxproj.filters | 3 + 14 files changed, 1254 insertions(+) create mode 100644 ql/instruments/crossccyswap.cpp create mode 100644 ql/instruments/crossccyswap.hpp create mode 100644 ql/pricingengines/swap/crossccyswapengine.cpp create mode 100644 ql/pricingengines/swap/crossccyswapengine.hpp create mode 100644 test-suite/crossccyswap.cpp diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index 89bb2028461..05daab44233 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -912,6 +912,7 @@ + @@ -1580,6 +1581,7 @@ + @@ -2187,6 +2189,7 @@ + @@ -2635,6 +2638,7 @@ + diff --git a/QuantLib.vcxproj.filters b/QuantLib.vcxproj.filters index 433f0d94d0a..86da7f1dcc5 100644 --- a/QuantLib.vcxproj.filters +++ b/QuantLib.vcxproj.filters @@ -822,6 +822,9 @@ instruments + + instruments + instruments @@ -2700,6 +2703,9 @@ pricingengines\swap + + pricingengines\swap + pricingengines\swap @@ -4745,6 +4751,9 @@ instruments + + instruments + instruments @@ -5975,6 +5984,9 @@ pricingengines\bond + + pricingengines\swap + pricingengines\swap diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index b27cc237424..048a0c48d56 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -271,6 +271,7 @@ set(QL_SOURCES instruments/cpicapfloor.cpp instruments/cpiswap.cpp instruments/creditdefaultswap.cpp + instruments/crossccyswap.cpp instruments/doublebarrieroption.cpp instruments/doublebarriertype.cpp instruments/equitytotalreturnswap.cpp @@ -723,6 +724,7 @@ set(QL_SOURCES pricingengines/lookback/analyticcontinuouspartialfixedlookback.cpp pricingengines/lookback/analyticcontinuouspartialfloatinglookback.cpp pricingengines/lookback/mclookbackengine.cpp + pricingengines/swap/crossccyswapengine.cpp pricingengines/swap/cvaswapengine.cpp pricingengines/swap/discountingswapengine.cpp pricingengines/swap/discretizedswap.cpp @@ -1336,6 +1338,7 @@ set(QL_HEADERS instruments/cpicapfloor.hpp instruments/cpiswap.hpp instruments/creditdefaultswap.hpp + instruments/crossccyswap.hpp instruments/dividendbarrieroption.hpp instruments/dividendschedule.hpp instruments/dividendvanillaoption.hpp @@ -1955,6 +1958,7 @@ set(QL_HEADERS pricingengines/mclongstaffschwartzengine.hpp pricingengines/mcsimulation.hpp pricingengines/quanto/quantoengine.hpp + pricingengines/swap/crossccyswapengine.hpp pricingengines/swap/cvaswapengine.hpp pricingengines/swap/discountingswapengine.hpp pricingengines/swap/discretizedswap.hpp diff --git a/ql/instruments/Makefile.am b/ql/instruments/Makefile.am index 4f0ee47ef2e..bc2d4062085 100644 --- a/ql/instruments/Makefile.am +++ b/ql/instruments/Makefile.am @@ -25,6 +25,7 @@ this_include_HEADERS = \ cpicapfloor.hpp \ cpiswap.hpp \ creditdefaultswap.hpp \ + crossccyswap.hpp \ dividendbarrieroption.hpp \ dividendschedule.hpp \ dividendvanillaoption.hpp \ @@ -100,6 +101,7 @@ cpp_files = \ cpicapfloor.cpp \ cpiswap.cpp \ creditdefaultswap.cpp \ + crossccyswap.cpp \ doublebarrieroption.cpp \ doublebarriertype.cpp \ equitytotalreturnswap.cpp \ diff --git a/ql/instruments/crossccyswap.cpp b/ql/instruments/crossccyswap.cpp new file mode 100644 index 00000000000..627a0f08a3a --- /dev/null +++ b/ql/instruments/crossccyswap.cpp @@ -0,0 +1,107 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + All rights reserved. + + 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 + +namespace QuantLib { + +CrossCcySwap::CrossCcySwap(const Leg& firstLeg, const Currency& firstLegCcy, const Leg& secondLeg, + const Currency& secondLegCcy) + : Swap(firstLeg, secondLeg), inCcyLegNPV_(2, 0.0), inCcyLegBPS_(2, 0.0), npvDateDiscounts_(2, 0.0) { + currencies_.resize(2); + currencies_[0] = firstLegCcy; + currencies_[1] = secondLegCcy; +} + +CrossCcySwap::CrossCcySwap(const std::vector& legs, const std::vector& payer, + const std::vector& currencies) + : Swap(legs, payer), currencies_(currencies), inCcyLegNPV_(legs.size(), 0.0), inCcyLegBPS_(legs.size(), 0.0), npvDateDiscounts_(legs.size(), 0.0) { + QL_REQUIRE(payer.size() == currencies_.size(), "Size mismatch " + "between payer (" + << payer.size() << ") and currencies (" << currencies_.size() + << ")"); +} + +CrossCcySwap::CrossCcySwap(Size legs) + : Swap(legs), currencies_(legs), inCcyLegNPV_(legs, 0.0), inCcyLegBPS_(legs, 0.0), npvDateDiscounts_(legs, 0.0) {} + +void CrossCcySwap::setupArguments(PricingEngine::arguments* args) const { + + Swap::setupArguments(args); + + CrossCcySwap::arguments* arguments = dynamic_cast(args); + QL_REQUIRE(arguments, "The arguments are not of type " + "cross currency swap"); + + arguments->currencies = currencies_; +} + +void CrossCcySwap::fetchResults(const PricingEngine::results* r) const { + + Swap::fetchResults(r); + + const CrossCcySwap::results* results = dynamic_cast(r); + QL_REQUIRE(results, "The results are not of type " + "cross currency swap"); + + if (!results->inCcyLegNPV.empty()) { + QL_REQUIRE(results->inCcyLegNPV.size() == inCcyLegNPV_.size(), + "Wrong number of in currency leg NPVs returned by engine"); + inCcyLegNPV_ = results->inCcyLegNPV; + } else { + std::fill(inCcyLegNPV_.begin(), inCcyLegNPV_.end(), Null()); + } + + if (!results->inCcyLegBPS.empty()) { + QL_REQUIRE(results->inCcyLegBPS.size() == inCcyLegBPS_.size(), + "Wrong number of in currency leg BPSs returned by engine"); + inCcyLegBPS_ = results->inCcyLegBPS; + } else { + std::fill(inCcyLegBPS_.begin(), inCcyLegBPS_.end(), Null()); + } + + if (!results->npvDateDiscounts.empty()) { + QL_REQUIRE(results->npvDateDiscounts.size() == npvDateDiscounts_.size(), + "Wrong number of npv date discounts returned by engine"); + npvDateDiscounts_ = results->npvDateDiscounts; + } else { + std::fill(npvDateDiscounts_.begin(), npvDateDiscounts_.end(), Null()); + } +} + +void CrossCcySwap::setupExpired() const { + Swap::setupExpired(); + std::fill(inCcyLegBPS_.begin(), inCcyLegBPS_.end(), 0.0); + std::fill(inCcyLegNPV_.begin(), inCcyLegNPV_.end(), 0.0); + std::fill(npvDateDiscounts_.begin(), npvDateDiscounts_.end(), 0.0); +} + +void CrossCcySwap::arguments::validate() const { + Swap::arguments::validate(); + QL_REQUIRE(legs.size() == currencies.size(), "Number of legs is not equal to number of currencies"); +} + +void CrossCcySwap::results::reset() { + Swap::results::reset(); + inCcyLegNPV.clear(); + inCcyLegBPS.clear(); + npvDateDiscounts.clear(); +} +} // namespace QuantLib diff --git a/ql/instruments/crossccyswap.hpp b/ql/instruments/crossccyswap.hpp new file mode 100644 index 00000000000..16cd04596ac --- /dev/null +++ b/ql/instruments/crossccyswap.hpp @@ -0,0 +1,147 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + All rights reserved. + + 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 crossccyswap.hpp + \brief Swap instrument with legs involving two currencies + + \ingroup instruments +*/ + +#ifndef quantlib_cross_ccy_swap_hpp +#define quantlib_cross_ccy_swap_hpp + +#include +#include + +namespace QuantLib { + +//! Cross currency swap +/*! The first leg holds the pay currency cashflows and second leg holds + the receive currency cashflows. + + \ingroup instruments +*/ +class CrossCcySwap : public QuantLib::Swap { +public: + class arguments; + class results; + class engine; + //! \name Constructors + //@{ + //! Constructs a cross-currency swap with two legs and their respective currencies + /*! + First leg is paid and the second is received. + + \param firstLeg The sequence of cash flows for the first leg of the swap. + \param firstLegCcy The currency in which the first leg's cash flows are denominated. + \param secondLeg The sequence of cash flows for the second leg of the swap. + \param secondLegCcy The currency in which the second leg's cash flows are denominated. + + \note The notional amounts, payment schedules, and other details of each leg must be + set up in the provided Leg objects before constructing the swap. + */ + CrossCcySwap(const Leg& firstLeg, const Currency& firstLegCcy, const Leg& secondLeg, const Currency& secondLegCcy); + //! Constructs a cross-currency swap with multiple legs and their respective currencies + /*! + Initializes a cross-currency swap with an arbitrary number of legs, each specified + by a sequence of cash flows (Leg) and associated with its own currency. The payer vector + determines the direction of each leg (payer or receiver). + + \param legs A vector of cash flow sequences, one for each leg of the swap. + \param payer A vector of booleans indicating the direction of each leg: + \c true for payer, \c false for receiver. + \param currencies A vector of currencies, one for each leg, specifying the currency + in which the corresponding leg's cash flows are denominated. + + \note The sizes of the \p legs, \p payer, and \p currencies vectors must all be equal. + \warning The notional amounts, payment schedules, and other details of each leg must be + set up in the provided Leg objects before constructing the swap. + */ + CrossCcySwap(const std::vector& legs, const std::vector& payer, const std::vector& currencies); + //@} + //! \name Instrument interface + //@{ + void setupArguments(PricingEngine::arguments* args) const override; + void fetchResults(const PricingEngine::results*) const override; + //@} + //! \name Additional interface + //@{ + const Currency& legCurrency(Size j) const { + QL_REQUIRE(j < legs_.size(), "leg# " << j << " doesn't exist!"); + return currencies_[j]; + } + Real inCcyLegBPS(Size j) const { + QL_REQUIRE(j < legs_.size(), "leg# " << j << " doesn't exist!"); + calculate(); + return inCcyLegBPS_[j]; + } + Real inCcyLegNPV(Size j) const { + QL_REQUIRE(j < legs_.size(), "leg #" << j << " doesn't exist!"); + calculate(); + return inCcyLegNPV_[j]; + } + DiscountFactor npvDateDiscounts(Size j) const { + QL_REQUIRE(j < legs_.size(), "leg #" << j << " doesn't exist!"); + calculate(); + return npvDateDiscounts_[j]; + } + //@} +protected: + //! \name Constructors + //@{ + /*! This constructor can be used by derived classes that will + build their legs themselves. + */ + CrossCcySwap(Size legs); + //@} + //! \name Instrument interface + //@{ + void setupExpired() const override; + //@} + + std::vector currencies_; + +private: + mutable std::vector inCcyLegNPV_; + mutable std::vector inCcyLegBPS_; + mutable std::vector npvDateDiscounts_; +}; + +//! \ingroup instruments +class CrossCcySwap::arguments : public Swap::arguments { +public: + std::vector currencies; + void validate() const override; +}; + +//! \ingroup instruments +class CrossCcySwap::results : public Swap::results { +public: + std::vector inCcyLegNPV; + std::vector inCcyLegBPS; + std::vector npvDateDiscounts; + void reset() override; +}; + +//! \ingroup instruments +class CrossCcySwap::engine : public GenericEngine {}; +} // namespace QuantLib + +#endif diff --git a/ql/pricingengines/swap/Makefile.am b/ql/pricingengines/swap/Makefile.am index 3d64c75c6e9..69b75947d11 100644 --- a/ql/pricingengines/swap/Makefile.am +++ b/ql/pricingengines/swap/Makefile.am @@ -4,12 +4,14 @@ AM_CPPFLAGS = -I${top_builddir} -I${top_srcdir} this_includedir=${includedir}/${subdir} this_include_HEADERS = \ all.hpp \ + crossccyswapengine.hpp \ cvaswapengine.hpp \ discountingswapengine.hpp \ discretizedswap.hpp \ treeswapengine.hpp cpp_files = \ + crossccyswapengine.cpp \ cvaswapengine.cpp \ discountingswapengine.cpp \ discretizedswap.cpp \ diff --git a/ql/pricingengines/swap/crossccyswapengine.cpp b/ql/pricingengines/swap/crossccyswapengine.cpp new file mode 100644 index 00000000000..85824299c6e --- /dev/null +++ b/ql/pricingengines/swap/crossccyswapengine.cpp @@ -0,0 +1,165 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + All rights reserved. + + 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 + +#include + +namespace QuantLib { + +CrossCcySwapEngine::CrossCcySwapEngine(const Currency& ccy1, const Handle& currency1Discountcurve, + const Currency& ccy2, const Handle& currency2Discountcurve, + const Handle& spotFX, ext::optional includeSettlementDateFlows, + const Date& settlementDate, const Date& npvDate, const Date& spotFXSettleDate) + : ccy1_(ccy1), currency1Discountcurve_(currency1Discountcurve), ccy2_(ccy2), + currency2Discountcurve_(currency2Discountcurve), spotFX_(spotFX), + includeSettlementDateFlows_(includeSettlementDateFlows), settlementDate_(settlementDate), npvDate_(npvDate), + spotFXSettleDate_(spotFXSettleDate) { + + registerWith(currency1Discountcurve_); + registerWith(currency2Discountcurve_); + registerWith(spotFX_); +} + +void CrossCcySwapEngine::calculate() const { + + QL_REQUIRE(!currency1Discountcurve_.empty() && !currency2Discountcurve_.empty(), + "Discounting term structure handle is empty."); + + QL_REQUIRE(!spotFX_.empty(), "FX spot quote handle is empty."); + + QL_REQUIRE(currency1Discountcurve_->referenceDate() == currency2Discountcurve_->referenceDate(), + "Term structures should have the same reference date."); + Date referenceDate = currency1Discountcurve_->referenceDate(); + Date settlementDate = settlementDate_; + if (settlementDate_ == Date()) { + settlementDate = referenceDate; + } else { + QL_REQUIRE(settlementDate >= referenceDate, "Settlement date (" << settlementDate + << ") cannot be before discount curve " + "reference date (" + << referenceDate << ")"); + } + + Size numLegs = arguments_.legs.size(); + // - Instrument::Results + if (npvDate_ == Date()) { + results_.valuationDate = referenceDate; + } else { + QL_REQUIRE(npvDate_ >= referenceDate, "NPV date (" << npvDate_ + << ") cannot be before " + "discount curve reference date (" + << referenceDate << ")"); + results_.valuationDate = npvDate_; + } + + Date spotFXSettleDate = spotFXSettleDate_; + if (spotFXSettleDate_ == Date()) { + spotFXSettleDate = referenceDate; + } else { + QL_REQUIRE(spotFXSettleDate >= referenceDate, "FX settlement date (" << spotFXSettleDate + << ") cannot be before discount curve " + "reference date (" + << referenceDate << ")"); + } + + results_.value = 0.0; + results_.errorEstimate = Null(); + // - Swap::Results + results_.legNPV.resize(numLegs); + results_.legBPS.resize(numLegs); + results_.startDiscounts.resize(numLegs); + results_.endDiscounts.resize(numLegs); + // - CrossCcySwap::Results + results_.inCcyLegNPV.resize(numLegs); + results_.inCcyLegBPS.resize(numLegs); + results_.npvDateDiscounts.resize(numLegs); + + bool includeReferenceDateFlows = + includeSettlementDateFlows_ ? *includeSettlementDateFlows_ : Settings::instance().includeReferenceDateEvents(); + + for (Size legNo = 0; legNo < numLegs; legNo++) { + try { + // Choose the correct discount curve for the leg. + Handle legDiscountCurve; + if (arguments_.currencies[legNo] == ccy1_) { + legDiscountCurve = currency1Discountcurve_; + } else { + QL_REQUIRE(arguments_.currencies[legNo] == ccy2_, "leg ccy (" << arguments_.currencies[legNo] + << ") must be ccy1 (" << ccy1_ + << ") or ccy2 (" << ccy2_ << ")"); + legDiscountCurve = currency2Discountcurve_; + } + results_.npvDateDiscounts[legNo] = legDiscountCurve->discount(results_.valuationDate); + + // Calculate the NPV and BPS of each leg in its currency. + std::tie(results_.inCcyLegNPV[legNo], results_.inCcyLegBPS[legNo]) = + CashFlows::npvbps(arguments_.legs[legNo], **legDiscountCurve, includeReferenceDateFlows, settlementDate, + results_.valuationDate); + results_.inCcyLegNPV[legNo] *= arguments_.payer[legNo]; + results_.inCcyLegBPS[legNo] *= arguments_.payer[legNo]; + + results_.legNPV[legNo] = results_.inCcyLegNPV[legNo]; + results_.legBPS[legNo] = results_.inCcyLegBPS[legNo]; + + // Convert to NPV currency if necessary. + if (arguments_.currencies[legNo] != ccy1_) { + // results_.legNPV[legNo] *= spotFX_->value(); + // results_.legBPS[legNo] *= spotFX_->value(); + Real spotFXRate = spotFX_->value(); + if (spotFXSettleDate != referenceDate) { + // Use the parity relation between discount factors and fx rates to compute spotFXRate + // Generic formula: fx(T1)/fx(T2) = FwdDF_Quote(T1->T2) / FwdDF_Base(T1->T2), + // where fx represents the currency ratio Base/Quote + Real ccy1DF = currency1Discountcurve_->discount(spotFXSettleDate); + Real ccy2DF = currency2Discountcurve_->discount(spotFXSettleDate); + QL_REQUIRE(ccy2DF != 0.0, "Discount Factor associated with currency " << ccy2_ + << " at maturity " << spotFXSettleDate << " cannot be zero"); + spotFXRate *= ccy1DF / ccy2DF; + } + results_.legNPV[legNo] *= spotFXRate; + results_.legBPS[legNo] *= spotFXRate; + } + + // Get start date and end date discount for the leg + Date startDate = CashFlows::startDate(arguments_.legs[legNo]); + if (startDate >= currency1Discountcurve_->referenceDate()) { + results_.startDiscounts[legNo] = legDiscountCurve->discount(startDate); + } else { + results_.startDiscounts[legNo] = Null(); + } + + Date maturityDate = CashFlows::maturityDate(arguments_.legs[legNo]); + if (maturityDate >= currency1Discountcurve_->referenceDate()) { + results_.endDiscounts[legNo] = legDiscountCurve->discount(maturityDate); + } else { + results_.endDiscounts[legNo] = Null(); + } + + } catch (std::exception& e) { + QL_FAIL(io::ordinal(legNo + 1) << " leg: " << e.what()); + } + + results_.value += results_.legNPV[legNo]; + } +} +} // namespace QuantLib diff --git a/ql/pricingengines/swap/crossccyswapengine.hpp b/ql/pricingengines/swap/crossccyswapengine.hpp new file mode 100644 index 00000000000..258312224ce --- /dev/null +++ b/ql/pricingengines/swap/crossccyswapengine.hpp @@ -0,0 +1,107 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + All rights reserved. + + 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 ql/pricingengines/swap/crossccyswapengine.hpp + \brief Cross currency swap engine + + \ingroup engines +*/ + +#ifndef quantlib_cross_ccy_swap_engine_hpp +#define quantlib_cross_ccy_swap_engine_hpp + +#include + +#include + +namespace QuantLib { + +//! Cross currency swap engine + +/*! This class implements an engine for pricing swaps comprising legs that + involve two currencies. The npv is expressed in ccy1. The given currencies + ccy1 and ccy2 are matched to the correct swap legs. The evaluation date is the + reference date of either discounting curve (which must be equal). + + \ingroup engines +*/ +class CrossCcySwapEngine : public CrossCcySwap::engine { +public: + //! \name Constructors + //@{ + /*! \param ccy1 + Currency 1 + \param currency1DiscountCurve + Discount curve for cash flows in currency 1 + \param ccy2 + Currency 2 + \param currency2DiscountCurve + Discount curve for cash flows in currency 2 + \param spotFX + The market spot rate quote, given as units of ccy1 + for one unit of ccy2. The spot rate must be given + w.r.t. a settlement equal to the npv date. + \param includeSettlementDateFlows, settlementDate + If includeSettlementDateFlows is true (false), cashflows + on the settlementDate are (not) included in the NPV. + If not given the settlement date is set to the + npv date. + \param npvDate + Discount to this date. If not given the npv date + is set to the evaluation date + \param spotFXSettleDate + FX conversion as of this date if specified explicitly + */ + CrossCcySwapEngine(const Currency& ccy1, const Handle& currency1DiscountCurve, + const Currency& ccy2, const Handle& currency2DiscountCurve, + const Handle& spotFX, ext::optional includeSettlementDateFlows = ext::nullopt, + const Date& settlementDate = Date(), const Date& npvDate = Date(), const Date& spotFXSettleDate = Date()); + //@} + + //! \name PricingEngine interface + //@{ + void calculate() const override; + //@} + + //! \name Inspectors + //@{ + const Handle& currency1DiscountCurve() const { return currency1Discountcurve_; } + const Handle& currency2DiscountCurve() const { return currency2Discountcurve_; } + + const Currency& currency1() const { return ccy1_; } + const Currency& currency2() const { return ccy2_; } + + const Handle& spotFX() const { return spotFX_; } + //@} + +private: + Currency ccy1_; + Handle currency1Discountcurve_; + Currency ccy2_; + Handle currency2Discountcurve_; + Handle spotFX_; + ext::optional includeSettlementDateFlows_; + Date settlementDate_; + Date npvDate_; + Date spotFXSettleDate_; +}; +} // namespace QuantLib + +#endif diff --git a/test-suite/CMakeLists.txt b/test-suite/CMakeLists.txt index 39ab5081e4d..372ab2d5422 100644 --- a/test-suite/CMakeLists.txt +++ b/test-suite/CMakeLists.txt @@ -39,6 +39,7 @@ set(QL_TEST_SOURCES covariance.cpp creditdefaultswap.cpp creditriskplus.cpp + crossccyswap.cpp crosscurrencyratehelpers.cpp currency.cpp curvestates.cpp diff --git a/test-suite/Makefile.am b/test-suite/Makefile.am index c881f4f3887..9c1643fde93 100644 --- a/test-suite/Makefile.am +++ b/test-suite/Makefile.am @@ -40,6 +40,7 @@ QL_TEST_SRCS = \ covariance.cpp \ creditdefaultswap.cpp \ creditriskplus.cpp \ + crossccyswap.cpp \ crosscurrencyratehelpers.cpp \ currency.cpp \ curvestates.cpp \ diff --git a/test-suite/crossccyswap.cpp b/test-suite/crossccyswap.cpp new file mode 100644 index 00000000000..28ced5a11f9 --- /dev/null +++ b/test-suite/crossccyswap.cpp @@ -0,0 +1,698 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + 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/ + + 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 "toplevelfixture.hpp" +#include "utilities.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace QuantLib; +using namespace boost::unit_test_framework; + +BOOST_FIXTURE_TEST_SUITE(QuantLibTests, TopLevelFixture) + +BOOST_AUTO_TEST_SUITE(CrossCcySwapTests) + +#define CHECK_XCCY_SWAP_RESULT(what, calculated, expected, tolerance) \ + if (std::fabs(calculated-expected) > tolerance) { \ + BOOST_ERROR("Failed to reproduce " what ":" \ + << "\n expected: " << std::setprecision(12) << expected \ + << "\n calculated: " << std::setprecision(12) << calculated \ + << "\n error: " << std::setprecision(12) << std::fabs(calculated-expected)); \ + } + +// Data for the tests + +Handle CHFDiscountCurve() { + std::vector dates(27); + std::vector dfs(27); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1.0; + dates[1] = Date(14, Sep, 2018); + dfs[1] = 0.99998; + dates[2] = Date(20, Sep, 2018); + dfs[2] = 0.99975; + dates[3] = Date(27, Sep, 2018); + dfs[3] = 0.99945; + dates[4] = Date(04, Oct, 2018); + dfs[4] = 0.99910; + dates[5] = Date(15, Oct, 2018); + dfs[5] = 0.99855; + dates[6] = Date(13, Nov, 2018); + dfs[6] = 0.99700; + dates[7] = Date(13, Dec, 2018); + dfs[7] = 0.99540; + dates[8] = Date(14, Jan, 2019); + dfs[8] = 0.99360; + dates[9] = Date(13, Feb, 2019); + dfs[9] = 0.99190; + dates[10] = Date(13, Mar, 2019); + dfs[10] = 0.99030; + dates[11] = Date(13, Jun, 2019); + dfs[11] = 0.98430; + dates[12] = Date(13, Sep, 2019); + dfs[12] = 0.97800; + dates[13] = Date(13, Mar, 2020); + dfs[13] = 0.96500; + dates[14] = Date(14, Sep, 2020); + dfs[14] = 0.95200; + dates[15] = Date(13, Sep, 2021); + dfs[15] = 0.92700; + dates[16] = Date(13, Sep, 2022); + dfs[16] = 0.90300; + dates[17] = Date(13, Sep, 2023); + dfs[17] = 0.88000; + dates[18] = Date(15, Sep, 2025); + dfs[18] = 0.83600; + dates[19] = Date(13, Sep, 2028); + dfs[19] = 0.77300; + dates[20] = Date(13, Sep, 2030); + dfs[20] = 0.73400; + dates[21] = Date(13, Sep, 2033); + dfs[21] = 0.67800; + dates[22] = Date(13, Sep, 2038); + dfs[22] = 0.59600; + dates[23] = Date(14, Sep, 2043); + dfs[23] = 0.52800; + dates[24] = Date(14, Sep, 2048); + dfs[24] = 0.46800; + dates[25] = Date(13, Sep, 2058); + dfs[25] = 0.36700; + dates[26] = Date(13, Sep, 2068); + dfs[26] = 0.29700; + + return Handle( + ext::make_shared(dates, dfs, dayCounter) + ); +} + +Handle USDDiscountCurve() { + + vector dates(27); + vector dfs(27); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(14, Sep, 2018); + dfs[1] = 0.99994666951096; + dates[2] = Date(20, Sep, 2018); + dfs[2] = 0.999627719221066; + dates[3] = Date(27, Sep, 2018); + dfs[3] = 0.999254084816959; + dates[4] = Date(04, Oct, 2018); + dfs[4] = 0.998837020905631; + dates[5] = Date(15, Oct, 2018); + dfs[5] = 0.998176132423265; + dates[6] = Date(13, Nov, 2018); + dfs[6] = 0.99644587210048; + dates[7] = Date(13, Dec, 2018); + dfs[7] = 0.994644668243218; + dates[8] = Date(14, Jan, 2019); + dfs[8] = 0.992596634984033; + dates[9] = Date(13, Feb, 2019); + dfs[9] = 0.990636503861861; + dates[10] = Date(13, Mar, 2019); + dfs[10] = 0.988809127958345; + dates[11] = Date(13, Jun, 2019); + dfs[11] = 0.982417991680868; + dates[12] = Date(13, Sep, 2019); + dfs[12] = 0.975723193871552; + dates[13] = Date(13, Mar, 2020); + dfs[13] = 0.96219213956104; + dates[14] = Date(14, Sep, 2020); + dfs[14] = 0.948588232418325; + dates[15] = Date(13, Sep, 2021); + dfs[15] = 0.92279636773464; + dates[16] = Date(13, Sep, 2022); + dfs[16] = 0.898345201557914; + dates[17] = Date(13, Sep, 2023); + dfs[17] = 0.874715322269088; + dates[18] = Date(15, Sep, 2025); + dfs[18] = 0.828658611114833; + dates[19] = Date(13, Sep, 2028); + dfs[19] = 0.763030152740947; + dates[20] = Date(13, Sep, 2030); + dfs[20] = 0.722238847877756; + dates[21] = Date(13, Sep, 2033); + dfs[21] = 0.664460629674362; + dates[22] = Date(13, Sep, 2038); + dfs[22] = 0.580288693473926; + dates[23] = Date(14, Sep, 2043); + dfs[23] = 0.510857007600479; + dates[24] = Date(14, Sep, 2048); + dfs[24] = 0.44941525649436; + dates[25] = Date(13, Sep, 2058); + dfs[25] = 0.352389176933952; + dates[26] = Date(13, Sep, 2068); + dfs[26] = 0.28183300653329; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle USDProjectionCurve() { + + vector dates(25); + vector dfs(25); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(13, Dec, 2018); + dfs[1] = 0.994134145990132; + dates[2] = Date(19, Dec, 2018); + dfs[2] = 0.993695776146116; + dates[3] = Date(20, Mar, 2019); + dfs[3] = 0.987047992958673; + dates[4] = Date(19, Jun, 2019); + dfs[4] = 0.980016364694049; + dates[5] = Date(18, Sep, 2019); + dfs[5] = 0.972708376777628; + dates[6] = Date(18, Dec, 2019); + dfs[6] = 0.965277162951128; + dates[7] = Date(18, Mar, 2020); + dfs[7] = 0.957799302363697; + dates[8] = Date(14, Sep, 2020); + dfs[8] = 0.943264331984248; + dates[9] = Date(13, Sep, 2021); + dfs[9] = 0.914816470778467; + dates[10] = Date(13, Sep, 2022); + dfs[10] = 0.88764714641623; + dates[11] = Date(13, Sep, 2023); + dfs[11] = 0.861475671008934; + dates[12] = Date(13, Sep, 2024); + dfs[12] = 0.835944798717806; + dates[13] = Date(15, Sep, 2025); + dfs[13] = 0.810833947617338; + dates[14] = Date(14, Sep, 2026); + dfs[14] = 0.78631849267276; + dates[15] = Date(13, Sep, 2027); + dfs[15] = 0.762267648509673; + dates[16] = Date(13, Sep, 2028); + dfs[16] = 0.738613627359076; + dates[17] = Date(13, Sep, 2029); + dfs[17] = 0.715502378943932; + dates[18] = Date(13, Sep, 2030); + dfs[18] = 0.693380472578176; + dates[19] = Date(13, Sep, 2033); + dfs[19] = 0.631097994110912; + dates[20] = Date(13, Sep, 2038); + dfs[20] = 0.540797634630251; + dates[21] = Date(14, Sep, 2043); + dfs[21] = 0.465599237331079; + dates[22] = Date(14, Sep, 2048); + dfs[22] = 0.402119473746341; + dates[23] = Date(13, Sep, 2058); + dfs[23] = 0.303129773289934; + dates[24] = Date(13, Sep, 2068); + dfs[24] = 0.23210070222569; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle GBPDiscountCurve() { + + vector dates(27); + vector dfs(27); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(14, Sep, 2018); + dfs[1] = 0.99994666951096; + dates[2] = Date(20, Sep, 2018); + dfs[2] = 0.999627719221066; + dates[3] = Date(27, Sep, 2018); + dfs[3] = 0.999254084816959; + dates[4] = Date(04, Oct, 2018); + dfs[4] = 0.998837020905631; + dates[5] = Date(15, Oct, 2018); + dfs[5] = 0.998176132423265; + dates[6] = Date(13, Nov, 2018); + dfs[6] = 0.99644587210048; + dates[7] = Date(13, Dec, 2018); + dfs[7] = 0.994644668243218; + dates[8] = Date(14, Jan, 2019); + dfs[8] = 0.992596634984033; + dates[9] = Date(13, Feb, 2019); + dfs[9] = 0.990636503861861; + dates[10] = Date(13, Mar, 2019); + dfs[10] = 0.988809127958345; + dates[11] = Date(13, Jun, 2019); + dfs[11] = 0.982417991680868; + dates[12] = Date(13, Sep, 2019); + dfs[12] = 0.975723193871552; + dates[13] = Date(13, Mar, 2020); + dfs[13] = 0.96219213956104; + dates[14] = Date(14, Sep, 2020); + dfs[14] = 0.948588232418325; + dates[15] = Date(13, Sep, 2021); + dfs[15] = 0.92279636773464; + dates[16] = Date(13, Sep, 2022); + dfs[16] = 0.898345201557914; + dates[17] = Date(13, Sep, 2023); + dfs[17] = 0.874715322269088; + dates[18] = Date(15, Sep, 2025); + dfs[18] = 0.828658611114833; + dates[19] = Date(13, Sep, 2028); + dfs[19] = 0.763030152740947; + dates[20] = Date(13, Sep, 2030); + dfs[20] = 0.722238847877756; + dates[21] = Date(13, Sep, 2033); + dfs[21] = 0.664460629674362; + dates[22] = Date(13, Sep, 2038); + dfs[22] = 0.580288693473926; + dates[23] = Date(14, Sep, 2043); + dfs[23] = 0.510857007600479; + dates[24] = Date(14, Sep, 2048); + dfs[24] = 0.44941525649436; + dates[25] = Date(13, Sep, 2058); + dfs[25] = 0.352389176933952; + dates[26] = Date(13, Sep, 2068); + dfs[26] = 0.28183300653329; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle GBPProjectionCurve() { + + vector dates(25); + vector dfs(25); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(13, Dec, 2018); + dfs[1] = 0.994134145990132; + dates[2] = Date(19, Dec, 2018); + dfs[2] = 0.993695776146116; + dates[3] = Date(20, Mar, 2019); + dfs[3] = 0.987047992958673; + dates[4] = Date(19, Jun, 2019); + dfs[4] = 0.980016364694049; + dates[5] = Date(18, Sep, 2019); + dfs[5] = 0.972708376777628; + dates[6] = Date(18, Dec, 2019); + dfs[6] = 0.965277162951128; + dates[7] = Date(18, Mar, 2020); + dfs[7] = 0.957799302363697; + dates[8] = Date(14, Sep, 2020); + dfs[8] = 0.943264331984248; + dates[9] = Date(13, Sep, 2021); + dfs[9] = 0.914816470778467; + dates[10] = Date(13, Sep, 2022); + dfs[10] = 0.88764714641623; + dates[11] = Date(13, Sep, 2023); + dfs[11] = 0.861475671008934; + dates[12] = Date(13, Sep, 2024); + dfs[12] = 0.835944798717806; + dates[13] = Date(15, Sep, 2025); + dfs[13] = 0.810833947617338; + dates[14] = Date(14, Sep, 2026); + dfs[14] = 0.78631849267276; + dates[15] = Date(13, Sep, 2027); + dfs[15] = 0.762267648509673; + dates[16] = Date(13, Sep, 2028); + dfs[16] = 0.738613627359076; + dates[17] = Date(13, Sep, 2029); + dfs[17] = 0.715502378943932; + dates[18] = Date(13, Sep, 2030); + dfs[18] = 0.693380472578176; + dates[19] = Date(13, Sep, 2033); + dfs[19] = 0.631097994110912; + dates[20] = Date(13, Sep, 2038); + dfs[20] = 0.540797634630251; + dates[21] = Date(14, Sep, 2043); + dfs[21] = 0.465599237331079; + dates[22] = Date(14, Sep, 2048); + dfs[22] = 0.402119473746341; + dates[23] = Date(13, Sep, 2058); + dfs[23] = 0.303129773289934; + dates[24] = Date(13, Sep, 2068); + dfs[24] = 0.23210070222569; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle TRYDiscountCurve() { + + vector dates(18); + vector dfs(18); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(15, Oct, 2018); + dfs[1] = 0.979316826759248; + dates[2] = Date(13, Nov, 2018); + dfs[2] = 0.959997676372812; + dates[3] = Date(13, Dec, 2018); + dfs[3] = 0.939987819768341; + dates[4] = Date(14, Jan, 2019); + dfs[4] = 0.917879348095857; + dates[5] = Date(13, Feb, 2019); + dfs[5] = 0.897309447005875; + dates[6] = Date(13, Mar, 2019); + dfs[6] = 0.878377243062539; + dates[7] = Date(13, Sep, 2019); + dfs[7] = 0.76374502801031; + dates[8] = Date(14, Sep, 2020); + dfs[8] = 0.595566112318217; + dates[9] = Date(13, Sep, 2021); + dfs[9] = 0.483132147134316; + dates[10] = Date(13, Sep, 2022); + dfs[10] = 0.402466076327945; + dates[11] = Date(13, Sep, 2023); + dfs[11] = 0.345531820837392; + dates[12] = Date(13, Sep, 2024); + dfs[12] = 0.298070398810781; + dates[13] = Date(13, Sep, 2025); + dfs[13] = 0.264039803303106; + dates[14] = Date(13, Sep, 2026); + dfs[14] = 0.237813130821584; + dates[15] = Date(13, Sep, 2027); + dfs[15] = 0.216456097559999; + dates[16] = Date(13, Sep, 2028); + dfs[16] = 0.200289181912326; + dates[17] = Date(13, Sep, 2033); + dfs[17] = 0.122659501286113; + + return Handle(QuantLib::ext::make_shared(dates, dfs, dayCounter)); +} + +// Helper functions + +ext::shared_ptr makeFixFixXCCYSwap(Real leg1Nominal, Rate spotFx) { + Calendar payCalendar = JointCalendar(UnitedStates(UnitedStates::Settlement), Switzerland()); + + Date referenceDate = payCalendar.adjust(Settings::instance().evaluationDate()); + Date start_date = payCalendar.advance(referenceDate, Period(2, Days)); + Date end_date = payCalendar.advance(referenceDate, Period(5, Years)); + DateGeneration::Rule rule = DateGeneration::Forward; + BusinessDayConvention convention = Following; + bool end_of_month = false; + DayCounter dc = Actual365Fixed(); + + Schedule schedule( + start_date, + end_date, + Period(3, Months), + payCalendar, + convention, + convention, + rule, + end_of_month + ); + + Rate usdRate = 0.0575; + Rate chfRate = 0.0201; + + // USD Leg + Leg usdLeg = FixedRateLeg(schedule) + .withNotionals(leg1Nominal) + .withCouponRates(usdRate, dc) + .withPaymentAdjustment(convention) + .withPaymentCalendar(payCalendar); + Date aDate = payCalendar.adjust(schedule.dates().front(), convention); + auto initialCapitalFlow = ext::make_shared(-leg1Nominal, aDate); + auto finalCapitalFlow = ext::make_shared(leg1Nominal, usdLeg.back()->date()); + usdLeg.insert(usdLeg.begin(), initialCapitalFlow); + usdLeg.push_back(finalCapitalFlow); + + // CHF Leg + Leg chfLeg = FixedRateLeg(schedule) + .withNotionals(leg1Nominal * spotFx) + .withCouponRates(chfRate, dc) + .withPaymentAdjustment(convention) + .withPaymentCalendar(payCalendar); + auto initialCHFCapitalFlow = ext::make_shared(-leg1Nominal * spotFx, aDate); + auto finalCHFCapitalFlow = ext::make_shared(leg1Nominal * spotFx, chfLeg.back()->date()); + chfLeg.insert(chfLeg.begin(), initialCHFCapitalFlow); + chfLeg.push_back(finalCHFCapitalFlow); + + // Create swap + return ext::shared_ptr (new CrossCcySwap( + usdLeg, USDCurrency(), chfLeg, CHFCurrency())); +} + +ext::shared_ptr makeFixFloatXCCYSwap(Real leg1Nominal, Rate spotFx) { + Calendar payCalendar = JointCalendar(UnitedStates(UnitedStates::Settlement), UnitedKingdom(), Turkey()); + + Date referenceDate = payCalendar.adjust(Settings::instance().evaluationDate()); + Date start_date = payCalendar.advance(referenceDate, Period(2, Days)); + Date end_date = payCalendar.advance(referenceDate, Period(5, Years)); + DateGeneration::Rule rule = DateGeneration::Backward; + BusinessDayConvention convention = ModifiedFollowing; + BusinessDayConvention payConvention = Following; + bool end_of_month = false; + DayCounter dc = Actual365Fixed(); + + Schedule floatSchedule( + start_date, + end_date, + Period(3, Months), + payCalendar, + convention, + convention, + rule, + end_of_month + ); + + Schedule fixSchedule( + start_date, + end_date, + Period(1, Years), + payCalendar, + convention, + convention, + rule, + end_of_month + ); + + // TRY Leg + Rate tryRate = 0.249; + Leg tryLeg = FixedRateLeg(fixSchedule) + .withNotionals(leg1Nominal * spotFx) + .withCouponRates(tryRate, dc) + .withPaymentAdjustment(payConvention) + .withPaymentCalendar(payCalendar); + Date aDate = payCalendar.adjust(fixSchedule.dates().front(), convention); + auto initialTRYCapitalFlow = ext::make_shared(-leg1Nominal * spotFx, aDate); + auto finalTRYCapitalFlow = ext::make_shared(leg1Nominal * spotFx, tryLeg.back()->date()); + tryLeg.insert(tryLeg.begin(), initialTRYCapitalFlow); + tryLeg.push_back(finalTRYCapitalFlow); + + // USD Leg + auto usdlibor3M = ext::make_shared(Period(3, Months), USDProjectionCurve()); + Leg usdLeg = IborLeg(floatSchedule, usdlibor3M) + .withNotionals(leg1Nominal) + .withPaymentAdjustment(payConvention) + .withPaymentCalendar(payCalendar); + aDate = payCalendar.adjust(floatSchedule.dates().front(), convention); + auto initialUSDNotionalExchange = ext::make_shared(-leg1Nominal, aDate); + usdLeg.insert(usdLeg.begin(), initialUSDNotionalExchange); + auto finalUSDNotionalExchange = ext::make_shared(leg1Nominal, usdLeg.back()->date()); + usdLeg.push_back(finalUSDNotionalExchange); + + // Create swap + return ext::shared_ptr(new CrossCcySwap( + tryLeg, TRYCurrency(), usdLeg, USDCurrency())); +} + +ext::shared_ptr makeFloatFloatXCCYSwap(Real leg1Nominal, Rate spotFx) { + Calendar payCalendar = JointCalendar(UnitedStates(UnitedStates::Settlement), UnitedKingdom()); + + Date referenceDate = payCalendar.adjust(Settings::instance().evaluationDate()); + Date start_date = payCalendar.advance(referenceDate, Period(2, Days)); + Date end_date = payCalendar.advance(referenceDate, Period(5, Years)); + DateGeneration::Rule rule = DateGeneration::Forward; + BusinessDayConvention convention = Following; + bool end_of_month = false; + + Schedule schedule( + start_date, + end_date, + Period(3, Months), + payCalendar, + convention, + convention, + rule, + end_of_month + ); + + // USD Leg + auto usdlibor3M = ext::make_shared(Period(3, Months), USDProjectionCurve()); + Leg usdLeg = IborLeg(schedule, usdlibor3M) + .withNotionals(leg1Nominal) + .withPaymentAdjustment(convention) + .withPaymentCalendar(payCalendar); + Date aDate = payCalendar.adjust(schedule.dates().front()); + auto initialUSDNotionalExchange = ext::make_shared(-leg1Nominal, aDate); + usdLeg.insert(usdLeg.begin(), initialUSDNotionalExchange); + auto finalUSDNotionalExchange = ext::make_shared(leg1Nominal, usdLeg.back()->date()); + usdLeg.push_back(finalUSDNotionalExchange); + + // GBP Leg + auto gbpLibor3M = ext::make_shared(Period(3, Months), GBPProjectionCurve()); + Leg gbpLeg = IborLeg(schedule, gbpLibor3M) + .withNotionals(leg1Nominal * spotFx) + .withPaymentAdjustment(convention) + .withPaymentCalendar(payCalendar); + auto initialGBPNotionalExchange = ext::make_shared(-leg1Nominal * spotFx, aDate); + gbpLeg.insert(gbpLeg.begin(), initialGBPNotionalExchange); + auto finalGBPNotionalExchange = ext::make_shared(leg1Nominal * spotFx, gbpLeg.back()->date()); + gbpLeg.push_back(finalGBPNotionalExchange); + + // Create swap + return ext::shared_ptr (new CrossCcySwap( + usdLeg, USDCurrency(), gbpLeg, GBPCurrency())); +} + + +BOOST_AUTO_TEST_CASE(testFixFixXCCYSwapPricing) { + BOOST_TEST_MESSAGE("Test Fix-Fix cross currency swap pricing against known results"); + + SavedSettings backup; + Settings::instance().evaluationDate() = Date(11, Sep, 2018); + + // Create swap + Real USDNominal = 125'000'000; + Real spotFx = 1.22; + auto xccySwap = makeFixFixXCCYSwap(USDNominal, spotFx); + + // Attach pricing engine + auto fxSpotQuote = makeQuoteHandle(1.0 / spotFx); + auto engine = ext::make_shared( + USDCurrency(), USDDiscountCurve(), CHFCurrency(), CHFDiscountCurve(), fxSpotQuote); + + xccySwap->setPricingEngine(engine); + + // Check values + Real tolerance = 0.01; + Real expNpv = -21108172.67; + + CHECK_XCCY_SWAP_RESULT("NPV", xccySwap->NPV(), expNpv, tolerance); + + Real expPayLegNpv = -17892458.36; + Real expPayLegBps = -58317.61; + CHECK_XCCY_SWAP_RESULT("Leg 0 NPV", xccySwap->legNPV(0), expPayLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 BPS", xccySwap->legBPS(0), expPayLegBps, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyNPV", xccySwap->inCcyLegNPV(0), expPayLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyBPS", xccySwap->inCcyLegBPS(0), expPayLegBps, tolerance); + + Real expRecLegNpv = -3215714.30; + Real expRecLegBps = 58542.62; + CHECK_XCCY_SWAP_RESULT("Leg 1 NPV", xccySwap->legNPV(1), expRecLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 BPS", xccySwap->legBPS(1), expRecLegBps, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyNPV", xccySwap->inCcyLegNPV(1), expRecLegNpv * spotFx, tolerance * spotFx); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyBPS", xccySwap->inCcyLegBPS(1), expRecLegBps * spotFx, tolerance * spotFx); +} + +BOOST_AUTO_TEST_CASE(testFloatFixXCCYSwapPricing) { + BOOST_TEST_MESSAGE("Test Float-Fix cross currency pricing against known results"); + + SavedSettings backup; + Settings::instance().evaluationDate() = Date(11, Sep, 2018); + + // Create swap + Real USDNominal = 10'000'000; + Rate spotFx = 6.4304; + auto xccySwap = makeFixFloatXCCYSwap(USDNominal, spotFx); + + // Attach pricing engine + auto fxSpotQuote = makeQuoteHandle(1.0 / spotFx); + auto engine = ext::make_shared( + USDCurrency(), USDDiscountCurve(), TRYCurrency(), TRYDiscountCurve(), fxSpotQuote); + xccySwap->setPricingEngine(engine); + + // Check values + Real tolerance = 0.01; + + Real expNpv = 218961.99; + Real npv = xccySwap->NPV(); + CHECK_XCCY_SWAP_RESULT("NPV", npv, expNpv, tolerance); + + Real expPayLegNpv = 77054.99; + Real expPayLegBps = -2591.34; + CHECK_XCCY_SWAP_RESULT("Leg 0 NPV", xccySwap->legNPV(0), expPayLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 BPS", xccySwap->legBPS(0), expPayLegBps, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyNPV", xccySwap->inCcyLegNPV(0), expPayLegNpv * spotFx, tolerance * spotFx); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyBPS", xccySwap->inCcyLegBPS(0), expPayLegBps * spotFx, tolerance * spotFx); + + Real expRecLegNpv = 141906.99; + Real expRecLegBps = 4730.19; + CHECK_XCCY_SWAP_RESULT("Leg 1 NPV", xccySwap->legNPV(1), expRecLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 BPS", xccySwap->legBPS(1), expRecLegBps, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyNPV", xccySwap->inCcyLegNPV(1), expRecLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyBPS", xccySwap->inCcyLegBPS(1), expRecLegBps, tolerance); +} + +BOOST_AUTO_TEST_CASE(testFloatFloatXCCYSwapPricing) { + BOOST_TEST_MESSAGE("Test Float-Float cross currency pricing against known results"); + + SavedSettings backup; + Settings::instance().evaluationDate() = Date(11, Sep, 2018); + + // Create swap + Real USDNominal = 125'000'000; + Rate spotFx = 1.35; + auto xccySwap = makeFloatFloatXCCYSwap(USDNominal, spotFx); + + // Attach pricing engine + auto fxSpotQuote = makeQuoteHandle(1.0 / spotFx); + auto engine = ext::make_shared( + USDCurrency(), USDDiscountCurve(), GBPCurrency(), GBPDiscountCurve(), fxSpotQuote); + + xccySwap->setPricingEngine(engine); + + // Check values + Real tolerance = 0.01; + + Real expNpv = 0.00; + CHECK_XCCY_SWAP_RESULT("NPV", xccySwap->NPV(), expNpv, tolerance); + + Real expPayLegNpv = -1773829.64; + Real expPayLegBps = -59127.58; + CHECK_XCCY_SWAP_RESULT("Leg 0 NPV", xccySwap->legNPV(0), expPayLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 BPS", xccySwap->legBPS(0), expPayLegBps, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyNPV", xccySwap->inCcyLegNPV(0), expPayLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyBPS", xccySwap->inCcyLegBPS(0), expPayLegBps, tolerance); + + Real expRecLegNpv = 1773829.64; + Real expRecLegBps = 58317.61; + CHECK_XCCY_SWAP_RESULT("Leg 1 NPV", xccySwap->legNPV(1), expRecLegNpv, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 BPS", xccySwap->legBPS(1), expRecLegBps, tolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyNPV", xccySwap->inCcyLegNPV(1), expRecLegNpv * spotFx, tolerance * spotFx); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyBPS", xccySwap->inCcyLegBPS(1), expRecLegBps * spotFx, tolerance * spotFx); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/test-suite/testsuite.vcxproj b/test-suite/testsuite.vcxproj index 01b89680660..f7358c16045 100644 --- a/test-suite/testsuite.vcxproj +++ b/test-suite/testsuite.vcxproj @@ -684,6 +684,7 @@ + diff --git a/test-suite/testsuite.vcxproj.filters b/test-suite/testsuite.vcxproj.filters index 0befa6b4dec..f7b4e153960 100644 --- a/test-suite/testsuite.vcxproj.filters +++ b/test-suite/testsuite.vcxproj.filters @@ -501,6 +501,9 @@ Source Files + + Source Files + Source Files From 48e48aacf212c780c9cca7d96924584af2a24599 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:17:31 +0000 Subject: [PATCH 02/18] Update generated headers --- ql/instruments/all.hpp | 1 + ql/pricingengines/swap/all.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/ql/instruments/all.hpp b/ql/instruments/all.hpp index 044fd4197d5..1f4205def77 100644 --- a/ql/instruments/all.hpp +++ b/ql/instruments/all.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include diff --git a/ql/pricingengines/swap/all.hpp b/ql/pricingengines/swap/all.hpp index 3596dffe1f8..da6d7d9c2fd 100644 --- a/ql/pricingengines/swap/all.hpp +++ b/ql/pricingengines/swap/all.hpp @@ -1,6 +1,7 @@ /* This file is automatically generated; do not edit. */ /* Add the files to be included into Makefile.am instead. */ +#include #include #include #include From 373255666bd4a156d67940a6432e80988fe38f7b Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Sat, 14 Jun 2025 12:17:42 +0200 Subject: [PATCH 03/18] Update CrossCcySwap tests to account for usingAtParCoupons setting --- test-suite/crossccyswap.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test-suite/crossccyswap.cpp b/test-suite/crossccyswap.cpp index 28ced5a11f9..9f586453ccc 100644 --- a/test-suite/crossccyswap.cpp +++ b/test-suite/crossccyswap.cpp @@ -620,6 +620,7 @@ BOOST_AUTO_TEST_CASE(testFloatFixXCCYSwapPricing) { SavedSettings backup; Settings::instance().evaluationDate() = Date(11, Sep, 2018); + bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); // Create swap Real USDNominal = 10'000'000; @@ -635,7 +636,7 @@ BOOST_AUTO_TEST_CASE(testFloatFixXCCYSwapPricing) { // Check values Real tolerance = 0.01; - Real expNpv = 218961.99; + Real expNpv = usingAtParCoupons ? 218961.99 : 218981.99; Real npv = xccySwap->NPV(); CHECK_XCCY_SWAP_RESULT("NPV", npv, expNpv, tolerance); @@ -646,7 +647,7 @@ BOOST_AUTO_TEST_CASE(testFloatFixXCCYSwapPricing) { CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyNPV", xccySwap->inCcyLegNPV(0), expPayLegNpv * spotFx, tolerance * spotFx); CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyBPS", xccySwap->inCcyLegBPS(0), expPayLegBps * spotFx, tolerance * spotFx); - Real expRecLegNpv = 141906.99; + Real expRecLegNpv = usingAtParCoupons ? 141906.99 : 141926.99; Real expRecLegBps = 4730.19; CHECK_XCCY_SWAP_RESULT("Leg 1 NPV", xccySwap->legNPV(1), expRecLegNpv, tolerance); CHECK_XCCY_SWAP_RESULT("Leg 1 BPS", xccySwap->legBPS(1), expRecLegBps, tolerance); @@ -659,6 +660,7 @@ BOOST_AUTO_TEST_CASE(testFloatFloatXCCYSwapPricing) { SavedSettings backup; Settings::instance().evaluationDate() = Date(11, Sep, 2018); + bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); // Create swap Real USDNominal = 125'000'000; @@ -678,14 +680,14 @@ BOOST_AUTO_TEST_CASE(testFloatFloatXCCYSwapPricing) { Real expNpv = 0.00; CHECK_XCCY_SWAP_RESULT("NPV", xccySwap->NPV(), expNpv, tolerance); - Real expPayLegNpv = -1773829.64; + Real expPayLegNpv = usingAtParCoupons ? -1773829.64 : -1773772.22; Real expPayLegBps = -59127.58; CHECK_XCCY_SWAP_RESULT("Leg 0 NPV", xccySwap->legNPV(0), expPayLegNpv, tolerance); CHECK_XCCY_SWAP_RESULT("Leg 0 BPS", xccySwap->legBPS(0), expPayLegBps, tolerance); CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyNPV", xccySwap->inCcyLegNPV(0), expPayLegNpv, tolerance); CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyBPS", xccySwap->inCcyLegBPS(0), expPayLegBps, tolerance); - Real expRecLegNpv = 1773829.64; + Real expRecLegNpv = usingAtParCoupons ? 1773829.64 : 1773772.22; Real expRecLegBps = 58317.61; CHECK_XCCY_SWAP_RESULT("Leg 1 NPV", xccySwap->legNPV(1), expRecLegNpv, tolerance); CHECK_XCCY_SWAP_RESULT("Leg 1 BPS", xccySwap->legBPS(1), expRecLegBps, tolerance); From a88020166ec75e5f65d514364cc9ed42059517ce Mon Sep 17 00:00:00 2001 From: Paolo D'Elia Date: Mon, 16 Jun 2025 14:00:06 +0200 Subject: [PATCH 04/18] Added missing imports in crossccyswapengine.hpp --- ql/pricingengines/swap/crossccyswapengine.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ql/pricingengines/swap/crossccyswapengine.hpp b/ql/pricingengines/swap/crossccyswapengine.hpp index 258312224ce..a5cf0faf34a 100644 --- a/ql/pricingengines/swap/crossccyswapengine.hpp +++ b/ql/pricingengines/swap/crossccyswapengine.hpp @@ -27,9 +27,10 @@ #ifndef quantlib_cross_ccy_swap_engine_hpp #define quantlib_cross_ccy_swap_engine_hpp -#include - +#include #include +#include +#include namespace QuantLib { From 5884906557a37af881a8a650c1b87a7e5772b4b1 Mon Sep 17 00:00:00 2001 From: Paolo D'Elia Date: Mon, 16 Jun 2025 14:37:24 +0200 Subject: [PATCH 05/18] Fix tag typo in QuantLib.vcxproj --- QuantLib.vcxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index 05daab44233..e2604363a8d 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -2638,7 +2638,7 @@ - + From 2d8185cad835f601847b960ce76f153ba90ee88f Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Mon, 16 Jun 2025 18:55:50 +0200 Subject: [PATCH 06/18] Refactor xccy tests file --- test-suite/crossccyswap.cpp | 122 +++++++++++++++++------------------- 1 file changed, 59 insertions(+), 63 deletions(-) diff --git a/test-suite/crossccyswap.cpp b/test-suite/crossccyswap.cpp index 9f586453ccc..9fd25dd41e5 100644 --- a/test-suite/crossccyswap.cpp +++ b/test-suite/crossccyswap.cpp @@ -47,6 +47,26 @@ BOOST_AUTO_TEST_SUITE(CrossCcySwapTests) } // Data for the tests +struct CommonVars { + Date today, startDate, endDate; + Calendar payCalendar; + DateGeneration::Rule rule; + BusinessDayConvention convention; + bool endOfMonth; + DayCounter dc; + + CommonVars(Calendar c, BusinessDayConvention conv, DateGeneration::Rule r) { + payCalendar = c; + today = Date(11, Sep, 2018); + Settings::instance().evaluationDate() = today; + startDate = payCalendar.advance(today, Period(2, Days)); + endDate = payCalendar.advance(today, Period(5, Years)); + rule = r; + convention = conv; + endOfMonth = false; + dc = Actual365Fixed(); + } +}; Handle CHFDiscountCurve() { std::vector dates(27); @@ -412,23 +432,17 @@ Handle TRYDiscountCurve() { ext::shared_ptr makeFixFixXCCYSwap(Real leg1Nominal, Rate spotFx) { Calendar payCalendar = JointCalendar(UnitedStates(UnitedStates::Settlement), Switzerland()); - Date referenceDate = payCalendar.adjust(Settings::instance().evaluationDate()); - Date start_date = payCalendar.advance(referenceDate, Period(2, Days)); - Date end_date = payCalendar.advance(referenceDate, Period(5, Years)); - DateGeneration::Rule rule = DateGeneration::Forward; - BusinessDayConvention convention = Following; - bool end_of_month = false; - DayCounter dc = Actual365Fixed(); + CommonVars vars(payCalendar, Following, DateGeneration::Forward); Schedule schedule( - start_date, - end_date, + vars.startDate, + vars.endDate, Period(3, Months), payCalendar, - convention, - convention, - rule, - end_of_month + vars.convention, + vars.convention, + vars.rule, + vars.endOfMonth ); Rate usdRate = 0.0575; @@ -437,10 +451,10 @@ ext::shared_ptr makeFixFixXCCYSwap(Real leg1Nominal, Rate spotFx) // USD Leg Leg usdLeg = FixedRateLeg(schedule) .withNotionals(leg1Nominal) - .withCouponRates(usdRate, dc) - .withPaymentAdjustment(convention) - .withPaymentCalendar(payCalendar); - Date aDate = payCalendar.adjust(schedule.dates().front(), convention); + .withCouponRates(usdRate, vars.dc) + .withPaymentAdjustment(vars.convention) + .withPaymentCalendar(vars.payCalendar); + Date aDate = payCalendar.adjust(schedule.dates().front(), vars.convention); auto initialCapitalFlow = ext::make_shared(-leg1Nominal, aDate); auto finalCapitalFlow = ext::make_shared(leg1Nominal, usdLeg.back()->date()); usdLeg.insert(usdLeg.begin(), initialCapitalFlow); @@ -449,9 +463,9 @@ ext::shared_ptr makeFixFixXCCYSwap(Real leg1Nominal, Rate spotFx) // CHF Leg Leg chfLeg = FixedRateLeg(schedule) .withNotionals(leg1Nominal * spotFx) - .withCouponRates(chfRate, dc) - .withPaymentAdjustment(convention) - .withPaymentCalendar(payCalendar); + .withCouponRates(chfRate, vars.dc) + .withPaymentAdjustment(vars.convention) + .withPaymentCalendar(vars.payCalendar); auto initialCHFCapitalFlow = ext::make_shared(-leg1Nominal * spotFx, aDate); auto finalCHFCapitalFlow = ext::make_shared(leg1Nominal * spotFx, chfLeg.back()->date()); chfLeg.insert(chfLeg.begin(), initialCHFCapitalFlow); @@ -465,45 +479,39 @@ ext::shared_ptr makeFixFixXCCYSwap(Real leg1Nominal, Rate spotFx) ext::shared_ptr makeFixFloatXCCYSwap(Real leg1Nominal, Rate spotFx) { Calendar payCalendar = JointCalendar(UnitedStates(UnitedStates::Settlement), UnitedKingdom(), Turkey()); - Date referenceDate = payCalendar.adjust(Settings::instance().evaluationDate()); - Date start_date = payCalendar.advance(referenceDate, Period(2, Days)); - Date end_date = payCalendar.advance(referenceDate, Period(5, Years)); - DateGeneration::Rule rule = DateGeneration::Backward; - BusinessDayConvention convention = ModifiedFollowing; + CommonVars vars(payCalendar, ModifiedFollowing, DateGeneration::Backward); BusinessDayConvention payConvention = Following; - bool end_of_month = false; - DayCounter dc = Actual365Fixed(); Schedule floatSchedule( - start_date, - end_date, + vars.startDate, + vars.endDate, Period(3, Months), payCalendar, - convention, - convention, - rule, - end_of_month + vars.convention, + vars.convention, + vars.rule, + vars.endOfMonth ); Schedule fixSchedule( - start_date, - end_date, + vars.startDate, + vars.endDate, Period(1, Years), payCalendar, - convention, - convention, - rule, - end_of_month + vars.convention, + vars.convention, + vars.rule, + vars.endOfMonth ); // TRY Leg Rate tryRate = 0.249; Leg tryLeg = FixedRateLeg(fixSchedule) .withNotionals(leg1Nominal * spotFx) - .withCouponRates(tryRate, dc) + .withCouponRates(tryRate, vars.dc) .withPaymentAdjustment(payConvention) .withPaymentCalendar(payCalendar); - Date aDate = payCalendar.adjust(fixSchedule.dates().front(), convention); + Date aDate = payCalendar.adjust(fixSchedule.dates().front(), vars.convention); auto initialTRYCapitalFlow = ext::make_shared(-leg1Nominal * spotFx, aDate); auto finalTRYCapitalFlow = ext::make_shared(leg1Nominal * spotFx, tryLeg.back()->date()); tryLeg.insert(tryLeg.begin(), initialTRYCapitalFlow); @@ -515,7 +523,7 @@ ext::shared_ptr makeFixFloatXCCYSwap(Real leg1Nominal, Rate spotFx .withNotionals(leg1Nominal) .withPaymentAdjustment(payConvention) .withPaymentCalendar(payCalendar); - aDate = payCalendar.adjust(floatSchedule.dates().front(), convention); + aDate = payCalendar.adjust(floatSchedule.dates().front(), vars.convention); auto initialUSDNotionalExchange = ext::make_shared(-leg1Nominal, aDate); usdLeg.insert(usdLeg.begin(), initialUSDNotionalExchange); auto finalUSDNotionalExchange = ext::make_shared(leg1Nominal, usdLeg.back()->date()); @@ -529,29 +537,24 @@ ext::shared_ptr makeFixFloatXCCYSwap(Real leg1Nominal, Rate spotFx ext::shared_ptr makeFloatFloatXCCYSwap(Real leg1Nominal, Rate spotFx) { Calendar payCalendar = JointCalendar(UnitedStates(UnitedStates::Settlement), UnitedKingdom()); - Date referenceDate = payCalendar.adjust(Settings::instance().evaluationDate()); - Date start_date = payCalendar.advance(referenceDate, Period(2, Days)); - Date end_date = payCalendar.advance(referenceDate, Period(5, Years)); - DateGeneration::Rule rule = DateGeneration::Forward; - BusinessDayConvention convention = Following; - bool end_of_month = false; + CommonVars vars(payCalendar, Following, DateGeneration::Forward); Schedule schedule( - start_date, - end_date, + vars.startDate, + vars.endDate, Period(3, Months), payCalendar, - convention, - convention, - rule, - end_of_month + vars.convention, + vars.convention, + vars.rule, + vars.endOfMonth ); // USD Leg auto usdlibor3M = ext::make_shared(Period(3, Months), USDProjectionCurve()); Leg usdLeg = IborLeg(schedule, usdlibor3M) .withNotionals(leg1Nominal) - .withPaymentAdjustment(convention) + .withPaymentAdjustment(vars.convention) .withPaymentCalendar(payCalendar); Date aDate = payCalendar.adjust(schedule.dates().front()); auto initialUSDNotionalExchange = ext::make_shared(-leg1Nominal, aDate); @@ -563,7 +566,7 @@ ext::shared_ptr makeFloatFloatXCCYSwap(Real leg1Nominal, Rate spot auto gbpLibor3M = ext::make_shared(Period(3, Months), GBPProjectionCurve()); Leg gbpLeg = IborLeg(schedule, gbpLibor3M) .withNotionals(leg1Nominal * spotFx) - .withPaymentAdjustment(convention) + .withPaymentAdjustment(vars.convention) .withPaymentCalendar(payCalendar); auto initialGBPNotionalExchange = ext::make_shared(-leg1Nominal * spotFx, aDate); gbpLeg.insert(gbpLeg.begin(), initialGBPNotionalExchange); @@ -579,9 +582,6 @@ ext::shared_ptr makeFloatFloatXCCYSwap(Real leg1Nominal, Rate spot BOOST_AUTO_TEST_CASE(testFixFixXCCYSwapPricing) { BOOST_TEST_MESSAGE("Test Fix-Fix cross currency swap pricing against known results"); - SavedSettings backup; - Settings::instance().evaluationDate() = Date(11, Sep, 2018); - // Create swap Real USDNominal = 125'000'000; Real spotFx = 1.22; @@ -618,8 +618,6 @@ BOOST_AUTO_TEST_CASE(testFixFixXCCYSwapPricing) { BOOST_AUTO_TEST_CASE(testFloatFixXCCYSwapPricing) { BOOST_TEST_MESSAGE("Test Float-Fix cross currency pricing against known results"); - SavedSettings backup; - Settings::instance().evaluationDate() = Date(11, Sep, 2018); bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); // Create swap @@ -658,8 +656,6 @@ BOOST_AUTO_TEST_CASE(testFloatFixXCCYSwapPricing) { BOOST_AUTO_TEST_CASE(testFloatFloatXCCYSwapPricing) { BOOST_TEST_MESSAGE("Test Float-Float cross currency pricing against known results"); - SavedSettings backup; - Settings::instance().evaluationDate() = Date(11, Sep, 2018); bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); // Create swap From 63f267fff09df93f4f737ccf393f92fcfb28fdaa Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Mon, 16 Jun 2025 19:07:18 +0200 Subject: [PATCH 07/18] Add crossccyfixfloatswap --- QuantLib.vcxproj | 2 + QuantLib.vcxproj.filters | 6 + ql/CMakeLists.txt | 2 + ql/instruments/Makefile.am | 2 + ql/instruments/crossccyfixfloatswap.cpp | 159 +++++++++++++ ql/instruments/crossccyfixfloatswap.hpp | 151 ++++++++++++ test-suite/CMakeLists.txt | 1 + test-suite/Makefile.am | 1 + test-suite/crossccyfixfloatswap.cpp | 300 ++++++++++++++++++++++++ test-suite/testsuite.vcxproj | 1 + test-suite/testsuite.vcxproj.filters | 3 + 11 files changed, 628 insertions(+) create mode 100644 ql/instruments/crossccyfixfloatswap.cpp create mode 100644 ql/instruments/crossccyfixfloatswap.hpp create mode 100644 test-suite/crossccyfixfloatswap.cpp diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index e2604363a8d..989f08e2322 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -912,6 +912,7 @@ + @@ -2189,6 +2190,7 @@ + diff --git a/QuantLib.vcxproj.filters b/QuantLib.vcxproj.filters index 86da7f1dcc5..f555f51f79a 100644 --- a/QuantLib.vcxproj.filters +++ b/QuantLib.vcxproj.filters @@ -822,6 +822,9 @@ instruments + + instruments + instruments @@ -4751,6 +4754,9 @@ instruments + + instruments + instruments diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 048a0c48d56..0fd56725288 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -271,6 +271,7 @@ set(QL_SOURCES instruments/cpicapfloor.cpp instruments/cpiswap.cpp instruments/creditdefaultswap.cpp + instruments/crossccyfixfloatswap.cpp instruments/crossccyswap.cpp instruments/doublebarrieroption.cpp instruments/doublebarriertype.cpp @@ -1338,6 +1339,7 @@ set(QL_HEADERS instruments/cpicapfloor.hpp instruments/cpiswap.hpp instruments/creditdefaultswap.hpp + instruments/crossccyfixfloatswap.hpp instruments/crossccyswap.hpp instruments/dividendbarrieroption.hpp instruments/dividendschedule.hpp diff --git a/ql/instruments/Makefile.am b/ql/instruments/Makefile.am index bc2d4062085..add73d8a074 100644 --- a/ql/instruments/Makefile.am +++ b/ql/instruments/Makefile.am @@ -25,6 +25,7 @@ this_include_HEADERS = \ cpicapfloor.hpp \ cpiswap.hpp \ creditdefaultswap.hpp \ + crossccyfixfloatswap.hpp \ crossccyswap.hpp \ dividendbarrieroption.hpp \ dividendschedule.hpp \ @@ -101,6 +102,7 @@ cpp_files = \ cpicapfloor.cpp \ cpiswap.cpp \ creditdefaultswap.cpp \ + crossccyfixfloatswap.cpp \ crossccyswap.cpp \ doublebarrieroption.cpp \ doublebarriertype.cpp \ diff --git a/ql/instruments/crossccyfixfloatswap.cpp b/ql/instruments/crossccyfixfloatswap.cpp new file mode 100644 index 00000000000..0dbc6a8c12d --- /dev/null +++ b/ql/instruments/crossccyfixfloatswap.cpp @@ -0,0 +1,159 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + All rights reserved. + + 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 +#include + +namespace QuantLib { + +CrossCcyFixFloatSwap::CrossCcyFixFloatSwap( + Type type, Real fixedNominal, const Currency& fixedCurrency, const Schedule& fixedSchedule, Rate fixedRate, + const DayCounter& fixedDayCount, BusinessDayConvention fixedPaymentBdc, Natural fixedPaymentLag, + const Calendar& fixedPaymentCalendar, Real floatNominal, const Currency& floatCurrency, + const Schedule& floatSchedule, const ext::shared_ptr& floatIndex, Spread floatSpread, + BusinessDayConvention floatPaymentBdc, Natural floatPaymentLag, const Calendar& floatPaymentCalendar) + : CrossCcySwap(2), type_(type), fixedNominal_(fixedNominal), fixedCurrency_(fixedCurrency), + fixedSchedule_(fixedSchedule), fixedRate_(fixedRate), fixedDayCount_(fixedDayCount), + fixedPaymentBdc_(fixedPaymentBdc), fixedPaymentLag_(fixedPaymentLag), fixedPaymentCalendar_(fixedPaymentCalendar), + floatNominal_(floatNominal), floatCurrency_(floatCurrency), floatSchedule_(floatSchedule), + floatIndex_(floatIndex), floatSpread_(floatSpread), floatPaymentBdc_(floatPaymentBdc), + floatPaymentLag_(floatPaymentLag), floatPaymentCalendar_(floatPaymentCalendar) { + + // Build the float leg + Leg floatLeg = IborLeg(floatSchedule_, floatIndex_) + .withNotionals(floatNominal_) + .withSpreads(floatSpread_) + .withPaymentAdjustment(floatPaymentBdc_) + .withPaymentLag(floatPaymentLag_) + .withPaymentCalendar(floatPaymentCalendar_); + + // Register with each floating rate coupon + for (Leg::const_iterator it = floatLeg.begin(); it < floatLeg.end(); ++it) + registerWith(*it); + + // Initial notional exchange + Date aDate = floatSchedule_.dates().front(); + aDate = floatPaymentCalendar_.adjust(aDate, floatPaymentBdc_); + ext::shared_ptr aCashflow = ext::make_shared(-floatNominal_, aDate); + floatLeg.insert(floatLeg.begin(), aCashflow); + + // Final notional exchange + aDate = floatLeg.back()->date(); + aCashflow = ext::make_shared(floatNominal_, aDate); + floatLeg.push_back(aCashflow); + + // Build the fixed rate leg + Leg fixedLeg = FixedRateLeg(fixedSchedule_) + .withNotionals(fixedNominal_) + .withCouponRates(fixedRate_, fixedDayCount_) + .withPaymentAdjustment(fixedPaymentBdc_) + .withPaymentLag(fixedPaymentLag) + .withPaymentCalendar(fixedPaymentCalendar); + + // Initial notional exchange + aDate = fixedSchedule_.dates().front(); + aDate = fixedPaymentCalendar_.adjust(aDate, fixedPaymentBdc_); + aCashflow = ext::make_shared(-fixedNominal_, aDate); + fixedLeg.insert(fixedLeg.begin(), aCashflow); + + // Final notional exchange + aDate = fixedLeg.back()->date(); + aCashflow = ext::make_shared(fixedNominal_, aDate); + fixedLeg.push_back(aCashflow); + + // Deriving from cross currency swap where: + // First leg should hold the pay flows + // Second leg should hold the receive flows + payer_[0] = -1.0; + payer_[1] = 1.0; + switch (type_) { + case Payer: + legs_[0] = fixedLeg; + currencies_[0] = fixedCurrency_; + legs_[1] = floatLeg; + currencies_[1] = floatCurrency_; + break; + case Receiver: + legs_[1] = fixedLeg; + currencies_[1] = fixedCurrency_; + legs_[0] = floatLeg; + currencies_[0] = floatCurrency_; + break; + default: + QL_FAIL("Unknown cross currency fix float swap type"); + } +} + +void CrossCcyFixFloatSwap::setupArguments(PricingEngine::arguments* a) const { + CrossCcySwap::setupArguments(a); + if (CrossCcyFixFloatSwap::arguments* args = dynamic_cast(a)) { + args->fixedRate = fixedRate_; + args->spread = floatSpread_; + } +} + +void CrossCcyFixFloatSwap::fetchResults(const PricingEngine::results* r) const { + + CrossCcySwap::fetchResults(r); + + // Depending on the pricing engine used, we may have CrossCcyFixFloatSwap::results + if (const CrossCcyFixFloatSwap::results* res = dynamic_cast(r)) { + // If we have CrossCcyFixFloatSwap::results from the pricing engine + fairFixedRate_ = res->fairFixedRate; + fairSpread_ = res->fairSpread; + } else { + // If not, set them to Null to indicate a calculation is needed below + fairFixedRate_ = Null(); + fairSpread_ = Null(); + } + + // Calculate fair rate and spread if they are still Null here + static Spread basisPoint = 1.0e-4; + + Size idxFixed = type_ == Payer ? 0 : 1; + if (fairFixedRate_ == Null() && legBPS_[idxFixed] != Null()) + fairFixedRate_ = fixedRate_ - NPV_ / (legBPS_[idxFixed] / basisPoint); + + Size idxFloat = type_ == Payer ? 1 : 0; + if (fairSpread_ == Null() && legBPS_[idxFloat] != Null()) + fairSpread_ = floatSpread_ - NPV_ / (legBPS_[idxFloat] / basisPoint); +} + +void CrossCcyFixFloatSwap::setupExpired() const { + CrossCcySwap::setupExpired(); + fairFixedRate_ = Null(); + fairSpread_ = Null(); +} + +void CrossCcyFixFloatSwap::arguments::validate() const { + CrossCcySwap::arguments::validate(); + QL_REQUIRE(fixedRate != Null(), "Fixed rate cannot be null"); + QL_REQUIRE(spread != Null(), "Spread cannot be null"); +} + +void CrossCcyFixFloatSwap::results::reset() { + CrossCcySwap::results::reset(); + fairFixedRate = Null(); + fairSpread = Null(); +} + +} // namespace QuantLib diff --git a/ql/instruments/crossccyfixfloatswap.hpp b/ql/instruments/crossccyfixfloatswap.hpp new file mode 100644 index 00000000000..e909c6a5c8c --- /dev/null +++ b/ql/instruments/crossccyfixfloatswap.hpp @@ -0,0 +1,151 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2018 Quaternion Risk Management Ltd + All rights reserved. + + 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 qle/instruments/crossccyfixfloatswap.hpp + \brief Cross currency fixed vs float swap instrument + \ingroup instruments +*/ + +#ifndef quantlib_cross_ccy_fix_float_swap_hpp +#define quantlib_cross_ccy_fix_float_swap_hpp + +#include +#include +#include + +namespace QuantLib { + +/*! Cross currency fixed vs float swap + \ingroup instruments +*/ +class CrossCcyFixFloatSwap : public CrossCcySwap { +public: + enum Type { Receiver = -1, Payer = 1 }; + class arguments; + class results; + + //! \name Constructors + //@{ + //! Detailed constructor + CrossCcyFixFloatSwap(Type type, Real fixedNominal, const Currency& fixedCurrency, + const Schedule& fixedSchedule, Rate fixedRate, + const DayCounter& fixedDayCount, BusinessDayConvention fixedPaymentBdc, + Natural fixedPaymentLag, const Calendar& fixedPaymentCalendar, + Real floatNominal, const Currency& floatCurrency, + const Schedule& floatSchedule, + const ext::shared_ptr& floatIndex, Spread floatSpread, + BusinessDayConvention floatPaymentBdc, Natural floatPaymentLag, + const Calendar& floatPaymentCalendar); + //@} + + //! \name Instrument interface + //@{ + void setupArguments(PricingEngine::arguments* a) const override; + void fetchResults(const PricingEngine::results* r) const override; + //@} + + //! \name Inspectors + //@{ + Type type() const { return type_; } + + Real fixedNominal() const { return fixedNominal_; } + const Currency& fixedCurrency() const { return fixedCurrency_; } + const Schedule& fixedSchedule() const { return fixedSchedule_; } + Rate fixedRate() const { return fixedRate_; } + const DayCounter& fixedDayCount() const { return fixedDayCount_; } + BusinessDayConvention fixedPaymentBdc() const { return fixedPaymentBdc_; } + Natural fixedPaymentLag() const { return fixedPaymentLag_; } + const Calendar& fixedPaymentCalendar() const { return fixedPaymentCalendar_; } + + Real floatNominal() const { return floatNominal_; } + const Currency& floatCurrency() const { return floatCurrency_; } + const Schedule& floatSchedule() const { return floatSchedule_; } + const ext::shared_ptr& floatIndex() const { return floatIndex_; } + Rate floatSpread() const { return floatSpread_; } + BusinessDayConvention floatPaymentBdc() const { return floatPaymentBdc_; } + Natural floatPaymentLag() const { return floatPaymentLag_; } + const Calendar& floatPaymentCalendar() const { return floatPaymentCalendar_; } + //@} + + //! \name Additional interface + //@{ + Rate fairFixedRate() const { + calculate(); + QL_REQUIRE(fairFixedRate_ != Null(), "Fair fixed rate is not available"); + return fairFixedRate_; + } + + Spread fairSpread() const { + calculate(); + QL_REQUIRE(fairSpread_ != Null(), "Fair spread is not available"); + return fairSpread_; + } + //@} + +protected: + //! \name Instrument interface + //@{ + void setupExpired() const override; + //@} + +private: + Type type_; + + Real fixedNominal_; + Currency fixedCurrency_; + Schedule fixedSchedule_; + Rate fixedRate_; + DayCounter fixedDayCount_; + BusinessDayConvention fixedPaymentBdc_; + Natural fixedPaymentLag_; + Calendar fixedPaymentCalendar_; + + Real floatNominal_; + Currency floatCurrency_; + Schedule floatSchedule_; + ext::shared_ptr floatIndex_; + Spread floatSpread_; + BusinessDayConvention floatPaymentBdc_; + Natural floatPaymentLag_; + Calendar floatPaymentCalendar_; + + mutable Rate fairFixedRate_; + mutable Spread fairSpread_; +}; + +//! \ingroup instruments +class CrossCcyFixFloatSwap::arguments : public CrossCcySwap::arguments { +public: + Rate fixedRate; + Spread spread; + void validate() const override; +}; + +//! \ingroup instruments +class CrossCcyFixFloatSwap::results : public CrossCcySwap::results { +public: + Rate fairFixedRate; + Spread fairSpread; + void reset() override; +}; + +} // namespace QuantLib + +#endif diff --git a/test-suite/CMakeLists.txt b/test-suite/CMakeLists.txt index 372ab2d5422..bf9ef1d4279 100644 --- a/test-suite/CMakeLists.txt +++ b/test-suite/CMakeLists.txt @@ -39,6 +39,7 @@ set(QL_TEST_SOURCES covariance.cpp creditdefaultswap.cpp creditriskplus.cpp + crossccyfixfloatswap.cpp crossccyswap.cpp crosscurrencyratehelpers.cpp currency.cpp diff --git a/test-suite/Makefile.am b/test-suite/Makefile.am index 9c1643fde93..f2852bad5ea 100644 --- a/test-suite/Makefile.am +++ b/test-suite/Makefile.am @@ -40,6 +40,7 @@ QL_TEST_SRCS = \ covariance.cpp \ creditdefaultswap.cpp \ creditriskplus.cpp \ + crossccyfixfloatswap.cpp \ crossccyswap.cpp \ crosscurrencyratehelpers.cpp \ currency.cpp \ diff --git a/test-suite/crossccyfixfloatswap.cpp b/test-suite/crossccyfixfloatswap.cpp new file mode 100644 index 00000000000..d3942dd8470 --- /dev/null +++ b/test-suite/crossccyfixfloatswap.cpp @@ -0,0 +1,300 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2018 Quaternion Risk Management Ltd + All rights reserved. + + 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 "toplevelfixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace boost::unit_test_framework; +using namespace QuantLib; + +BOOST_FIXTURE_TEST_SUITE(QuantLibTests, TopLevelFixture) + +BOOST_AUTO_TEST_SUITE(CrossCurrencyFixFloatSwapTest) + +#define CHECK_XCCY_SWAP_RESULT(what, calculated, expected, tolerance) \ + if (std::fabs(calculated-expected) > tolerance) { \ + BOOST_ERROR("Failed to reproduce " what ":" \ + << "\n expected: " << std::setprecision(12) << expected \ + << "\n calculated: " << std::setprecision(12) << calculated \ + << "\n error: " << std::setprecision(12) << std::fabs(calculated-expected)); \ + } + +Handle usdDiscountCurve() { + + vector dates(27); + vector dfs(27); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(14, Sep, 2018); + dfs[1] = 0.99994666951096; + dates[2] = Date(20, Sep, 2018); + dfs[2] = 0.999627719221066; + dates[3] = Date(27, Sep, 2018); + dfs[3] = 0.999254084816959; + dates[4] = Date(04, Oct, 2018); + dfs[4] = 0.998837020905631; + dates[5] = Date(15, Oct, 2018); + dfs[5] = 0.998176132423265; + dates[6] = Date(13, Nov, 2018); + dfs[6] = 0.99644587210048; + dates[7] = Date(13, Dec, 2018); + dfs[7] = 0.994644668243218; + dates[8] = Date(14, Jan, 2019); + dfs[8] = 0.992596634984033; + dates[9] = Date(13, Feb, 2019); + dfs[9] = 0.990636503861861; + dates[10] = Date(13, Mar, 2019); + dfs[10] = 0.988809127958345; + dates[11] = Date(13, Jun, 2019); + dfs[11] = 0.982417991680868; + dates[12] = Date(13, Sep, 2019); + dfs[12] = 0.975723193871552; + dates[13] = Date(13, Mar, 2020); + dfs[13] = 0.96219213956104; + dates[14] = Date(14, Sep, 2020); + dfs[14] = 0.948588232418325; + dates[15] = Date(13, Sep, 2021); + dfs[15] = 0.92279636773464; + dates[16] = Date(13, Sep, 2022); + dfs[16] = 0.898345201557914; + dates[17] = Date(13, Sep, 2023); + dfs[17] = 0.874715322269088; + dates[18] = Date(15, Sep, 2025); + dfs[18] = 0.828658611114833; + dates[19] = Date(13, Sep, 2028); + dfs[19] = 0.763030152740947; + dates[20] = Date(13, Sep, 2030); + dfs[20] = 0.722238847877756; + dates[21] = Date(13, Sep, 2033); + dfs[21] = 0.664460629674362; + dates[22] = Date(13, Sep, 2038); + dfs[22] = 0.580288693473926; + dates[23] = Date(14, Sep, 2043); + dfs[23] = 0.510857007600479; + dates[24] = Date(14, Sep, 2048); + dfs[24] = 0.44941525649436; + dates[25] = Date(13, Sep, 2058); + dfs[25] = 0.352389176933952; + dates[26] = Date(13, Sep, 2068); + dfs[26] = 0.28183300653329; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle usdProjectionCurve() { + + vector dates(25); + vector dfs(25); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(13, Dec, 2018); + dfs[1] = 0.994134145990132; + dates[2] = Date(19, Dec, 2018); + dfs[2] = 0.993695776146116; + dates[3] = Date(20, Mar, 2019); + dfs[3] = 0.987047992958673; + dates[4] = Date(19, Jun, 2019); + dfs[4] = 0.980016364694049; + dates[5] = Date(18, Sep, 2019); + dfs[5] = 0.972708376777628; + dates[6] = Date(18, Dec, 2019); + dfs[6] = 0.965277162951128; + dates[7] = Date(18, Mar, 2020); + dfs[7] = 0.957799302363697; + dates[8] = Date(14, Sep, 2020); + dfs[8] = 0.943264331984248; + dates[9] = Date(13, Sep, 2021); + dfs[9] = 0.914816470778467; + dates[10] = Date(13, Sep, 2022); + dfs[10] = 0.88764714641623; + dates[11] = Date(13, Sep, 2023); + dfs[11] = 0.861475671008934; + dates[12] = Date(13, Sep, 2024); + dfs[12] = 0.835944798717806; + dates[13] = Date(15, Sep, 2025); + dfs[13] = 0.810833947617338; + dates[14] = Date(14, Sep, 2026); + dfs[14] = 0.78631849267276; + dates[15] = Date(13, Sep, 2027); + dfs[15] = 0.762267648509673; + dates[16] = Date(13, Sep, 2028); + dfs[16] = 0.738613627359076; + dates[17] = Date(13, Sep, 2029); + dfs[17] = 0.715502378943932; + dates[18] = Date(13, Sep, 2030); + dfs[18] = 0.693380472578176; + dates[19] = Date(13, Sep, 2033); + dfs[19] = 0.631097994110912; + dates[20] = Date(13, Sep, 2038); + dfs[20] = 0.540797634630251; + dates[21] = Date(14, Sep, 2043); + dfs[21] = 0.465599237331079; + dates[22] = Date(14, Sep, 2048); + dfs[22] = 0.402119473746341; + dates[23] = Date(13, Sep, 2058); + dfs[23] = 0.303129773289934; + dates[24] = Date(13, Sep, 2068); + dfs[24] = 0.23210070222569; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle tryDiscountCurve() { + + vector dates(18); + vector dfs(18); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(15, Oct, 2018); + dfs[1] = 0.979316826759248; + dates[2] = Date(13, Nov, 2018); + dfs[2] = 0.959997676372812; + dates[3] = Date(13, Dec, 2018); + dfs[3] = 0.939987819768341; + dates[4] = Date(14, Jan, 2019); + dfs[4] = 0.917879348095857; + dates[5] = Date(13, Feb, 2019); + dfs[5] = 0.897309447005875; + dates[6] = Date(13, Mar, 2019); + dfs[6] = 0.878377243062539; + dates[7] = Date(13, Sep, 2019); + dfs[7] = 0.76374502801031; + dates[8] = Date(14, Sep, 2020); + dfs[8] = 0.595566112318217; + dates[9] = Date(13, Sep, 2021); + dfs[9] = 0.483132147134316; + dates[10] = Date(13, Sep, 2022); + dfs[10] = 0.402466076327945; + dates[11] = Date(13, Sep, 2023); + dfs[11] = 0.345531820837392; + dates[12] = Date(13, Sep, 2024); + dfs[12] = 0.298070398810781; + dates[13] = Date(13, Sep, 2025); + dfs[13] = 0.264039803303106; + dates[14] = Date(13, Sep, 2026); + dfs[14] = 0.237813130821584; + dates[15] = Date(13, Sep, 2027); + dfs[15] = 0.216456097559999; + dates[16] = Date(13, Sep, 2028); + dfs[16] = 0.200289181912326; + dates[17] = Date(13, Sep, 2033); + dfs[17] = 0.122659501286113; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +ext::shared_ptr makeFixFloatXCCYSwap(Rate spotFx, Rate rate, Spread spread) { + + // USD nominal + Real usdNominal = 10000000.0; + + // Shared settlement conventions + BusinessDayConvention payConvention = Following; + Natural payLag = 0; + JointCalendar payCalendar = JointCalendar(UnitedStates(UnitedStates::Settlement), UnitedKingdom(), Turkey()); + + // Swap start and end date + Date referenceDate = Settings::instance().evaluationDate(); + referenceDate = payCalendar.adjust(referenceDate); + Date start = payCalendar.advance(referenceDate, 2 * Days); + Date end = start + 5 * Years; + + // Fixed TRY schedule + Schedule fixedSchedule(start, end, 1 * Years, payCalendar, ModifiedFollowing, ModifiedFollowing, + DateGeneration::Backward, false); + + // Float USD schedule + Schedule floatSchedule(start, end, 3 * Months, payCalendar, ModifiedFollowing, ModifiedFollowing, + DateGeneration::Backward, false); + + auto index = ext::make_shared(3 * Months, usdProjectionCurve()); + + // Create swap + return ext::shared_ptr( + new CrossCcyFixFloatSwap(CrossCcyFixFloatSwap::Payer, usdNominal * spotFx, TRYCurrency(), fixedSchedule, rate, + Actual360(), payConvention, payLag, payCalendar, usdNominal, USDCurrency(), + floatSchedule, index, spread, payConvention, payLag, payCalendar)); +} + + +BOOST_AUTO_TEST_CASE(testFixFloatXCCYSwapPricing) { + + BOOST_TEST_MESSAGE("Test cross currency fix float swap pricing against known results"); + + SavedSettings backup; + + Settings::instance().evaluationDate() = Date(11, Sep, 2018); + + // Create swap, USD 3M Libor vs TRY annual fixed + Rate spotFx = 6.4304; + Rate rate = 0.249; + Spread spread = 0.0; + auto swap = makeFixFloatXCCYSwap(spotFx, rate, spread); + + // Attach pricing engine + auto fxSpotQuote = makeQuoteHandle(1.0 / spotFx); + auto engine = ext::make_shared( + USDCurrency(), usdDiscountCurve(), TRYCurrency(), tryDiscountCurve(), fxSpotQuote); + swap->setPricingEngine(engine); + + // Check values + Real usdTolerance = 0.01; + + Real expNpv = 129777.91; + CHECK_XCCY_SWAP_RESULT("NPV", swap->NPV(), expNpv, usdTolerance); + + Real expPayLegNpv = -12286.45; + Real expPayLegBps = -2628.39; + CHECK_XCCY_SWAP_RESULT("Leg 0 NPV", swap->legNPV(0), expPayLegNpv, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 BPS", swap->legBPS(0), expPayLegBps, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyNPV", swap->inCcyLegNPV(0), expPayLegNpv * spotFx, usdTolerance * spotFx); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyBPS", swap->inCcyLegBPS(0), expPayLegBps * spotFx, usdTolerance * spotFx); + + Real expRecLegNpv = 142064.36; + Real expRecLegBps = 4735.03; + CHECK_XCCY_SWAP_RESULT("Leg 1 NPV", swap->legNPV(1), expRecLegNpv, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 BPS", swap->legBPS(1), expRecLegBps, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyNPV", swap->inCcyLegNPV(1), expRecLegNpv, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyBPS", swap->inCcyLegBPS(1), expRecLegBps, usdTolerance); + + CHECK_XCCY_SWAP_RESULT("Fair Fixed Rate", swap->fairFixedRate(), 0.253937551076, 1e-10); + CHECK_XCCY_SWAP_RESULT("Fair Spread", swap->fairSpread(), -0.002740802104, 1e-10); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/testsuite.vcxproj b/test-suite/testsuite.vcxproj index f7358c16045..24255aa8316 100644 --- a/test-suite/testsuite.vcxproj +++ b/test-suite/testsuite.vcxproj @@ -684,6 +684,7 @@ + diff --git a/test-suite/testsuite.vcxproj.filters b/test-suite/testsuite.vcxproj.filters index f7b4e153960..78bd90e0a33 100644 --- a/test-suite/testsuite.vcxproj.filters +++ b/test-suite/testsuite.vcxproj.filters @@ -501,6 +501,9 @@ Source Files + + Source Files + Source Files From 960ab1552e011809f197bd3b822be06346cd29b2 Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Mon, 16 Jun 2025 19:13:08 +0200 Subject: [PATCH 08/18] Fixed typo in comments --- ql/instruments/crossccyfixfloatswap.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ql/instruments/crossccyfixfloatswap.hpp b/ql/instruments/crossccyfixfloatswap.hpp index e909c6a5c8c..9048fc2bfda 100644 --- a/ql/instruments/crossccyfixfloatswap.hpp +++ b/ql/instruments/crossccyfixfloatswap.hpp @@ -18,7 +18,7 @@ FOR A PARTICULAR PURPOSE. See the license for more details. */ -/*! \file qle/instruments/crossccyfixfloatswap.hpp +/*! \file ql/instruments/crossccyfixfloatswap.hpp \brief Cross currency fixed vs float swap instrument \ingroup instruments */ From 59df14c7748282d7021ae226e129ad725128807b Mon Sep 17 00:00:00 2001 From: Paolo D'Elia Date: Tue, 17 Jun 2025 10:11:44 +0200 Subject: [PATCH 09/18] add usingAtParCoupons, and fix code format in crossccyfixfloatswap.cpp --- test-suite/crossccyfixfloatswap.cpp | 54 ++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/test-suite/crossccyfixfloatswap.cpp b/test-suite/crossccyfixfloatswap.cpp index d3942dd8470..a5f1da03a6e 100644 --- a/test-suite/crossccyfixfloatswap.cpp +++ b/test-suite/crossccyfixfloatswap.cpp @@ -19,8 +19,6 @@ */ #include "toplevelfixture.hpp" -#include -#include #include #include #include @@ -252,47 +250,49 @@ ext::shared_ptr makeFixFloatXCCYSwap(Rate spotFx, Rate rat BOOST_AUTO_TEST_CASE(testFixFloatXCCYSwapPricing) { - BOOST_TEST_MESSAGE("Test cross currency fix float swap pricing against known results"); SavedSettings backup; - Settings::instance().evaluationDate() = Date(11, Sep, 2018); + bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); // Create swap, USD 3M Libor vs TRY annual fixed Rate spotFx = 6.4304; Rate rate = 0.249; Spread spread = 0.0; - auto swap = makeFixFloatXCCYSwap(spotFx, rate, spread); + auto xccy = makeFixFloatXCCYSwap(spotFx, rate, spread); // Attach pricing engine auto fxSpotQuote = makeQuoteHandle(1.0 / spotFx); auto engine = ext::make_shared( USDCurrency(), usdDiscountCurve(), TRYCurrency(), tryDiscountCurve(), fxSpotQuote); - swap->setPricingEngine(engine); + xccy->setPricingEngine(engine); // Check values - Real usdTolerance = 0.01; - - Real expNpv = 129777.91; - CHECK_XCCY_SWAP_RESULT("NPV", swap->NPV(), expNpv, usdTolerance); - - Real expPayLegNpv = -12286.45; - Real expPayLegBps = -2628.39; - CHECK_XCCY_SWAP_RESULT("Leg 0 NPV", swap->legNPV(0), expPayLegNpv, usdTolerance); - CHECK_XCCY_SWAP_RESULT("Leg 0 BPS", swap->legBPS(0), expPayLegBps, usdTolerance); - CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyNPV", swap->inCcyLegNPV(0), expPayLegNpv * spotFx, usdTolerance * spotFx); - CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyBPS", swap->inCcyLegBPS(0), expPayLegBps * spotFx, usdTolerance * spotFx); - - Real expRecLegNpv = 142064.36; - Real expRecLegBps = 4735.03; - CHECK_XCCY_SWAP_RESULT("Leg 1 NPV", swap->legNPV(1), expRecLegNpv, usdTolerance); - CHECK_XCCY_SWAP_RESULT("Leg 1 BPS", swap->legBPS(1), expRecLegBps, usdTolerance); - CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyNPV", swap->inCcyLegNPV(1), expRecLegNpv, usdTolerance); - CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyBPS", swap->inCcyLegBPS(1), expRecLegBps, usdTolerance); - - CHECK_XCCY_SWAP_RESULT("Fair Fixed Rate", swap->fairFixedRate(), 0.253937551076, 1e-10); - CHECK_XCCY_SWAP_RESULT("Fair Spread", swap->fairSpread(), -0.002740802104, 1e-10); + Real usdTolerance = 0.01; + + Real expNpv = usingAtParCoupons ? 129777.91 : 129767.99; + CHECK_XCCY_SWAP_RESULT("NPV", xccy->NPV(), expNpv, usdTolerance); + + Real expPayLegNpv = -12286.45; + Real expPayLegBps = -2628.39; + CHECK_XCCY_SWAP_RESULT("Leg 0 NPV", xccy->legNPV(0), expPayLegNpv, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 BPS", xccy->legBPS(0), expPayLegBps, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyNPV", xccy->inCcyLegNPV(0), expPayLegNpv * spotFx, usdTolerance * spotFx); + CHECK_XCCY_SWAP_RESULT("Leg 0 inCcyBPS", xccy->inCcyLegBPS(0), expPayLegBps * spotFx, usdTolerance * spotFx); + + Real expRecLegNpv = usingAtParCoupons ? 142064.36 : 142054.44; + Real expRecLegBps = 4735.03; + CHECK_XCCY_SWAP_RESULT("Leg 1 NPV", xccy->legNPV(1), expRecLegNpv, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 BPS", xccy->legBPS(1), expRecLegBps, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyNPV", xccy->inCcyLegNPV(1), expRecLegNpv, usdTolerance); + CHECK_XCCY_SWAP_RESULT("Leg 1 inCcyBPS", xccy->inCcyLegBPS(1), expRecLegBps, usdTolerance); + + Real expectedFairRate = usingAtParCoupons ? 0.253937551076 : 0.253937173908; + Real expectedFairSpread = usingAtParCoupons ? -0.002740802104 : -0.002740592739; + + CHECK_XCCY_SWAP_RESULT("Fair Fixed Rate", xccy->fairFixedRate(), expectedFairRate, 1e-10); + CHECK_XCCY_SWAP_RESULT("Fair Spread", xccy->fairSpread(), expectedFairSpread, 1e-10); } BOOST_AUTO_TEST_SUITE_END() From 9b62fab040c1521577a8bad55ab438c7107259bc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:15:30 +0000 Subject: [PATCH 10/18] Update generated headers --- ql/instruments/all.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ql/instruments/all.hpp b/ql/instruments/all.hpp index 1f4205def77..a6326bcbbf7 100644 --- a/ql/instruments/all.hpp +++ b/ql/instruments/all.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include From a65859e7d9a00f8ed9fef0309ce2d21891314368 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:11:58 +0000 Subject: [PATCH 11/18] Update copyright list in license --- LICENSE.TXT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.TXT b/LICENSE.TXT index 028efcbf89d..564a8bd1b12 100644 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -126,8 +126,8 @@ Copyright (C) 2015, 2016 Andres Hernandez Copyright (C) 2016 Nicholas Bertocchi Copyright (C) 2016 Stefano Fondi Copyright (C) 2016, 2017 Fabrice Lecuyer +Copyright (C) 2016, 2018, 2022 Quaternion Risk Management Ltd Copyright (C) 2016, 2019, 2020 Eisuke Tani -Copyright (C) 2016, 2022 Quaternion Risk Management Ltd Copyright (C) 2017 BN Algorithms Ltd Copyright (C) 2017 Joseph Jeisman From ecdda37e9edd6116d3268749c7690bda2a58cf60 Mon Sep 17 00:00:00 2001 From: Paolo D'Elia Date: Tue, 17 Jun 2025 10:30:02 +0200 Subject: [PATCH 12/18] Add missing import in crossccyfixfloatswap tests --- test-suite/crossccyfixfloatswap.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test-suite/crossccyfixfloatswap.cpp b/test-suite/crossccyfixfloatswap.cpp index a5f1da03a6e..bebb5e51bbc 100644 --- a/test-suite/crossccyfixfloatswap.cpp +++ b/test-suite/crossccyfixfloatswap.cpp @@ -21,6 +21,7 @@ #include "toplevelfixture.hpp" #include #include +#include #include #include #include From cecf12414b77f03f5d7f92355825ceb2ecbc7165 Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Tue, 17 Jun 2025 19:17:14 +0200 Subject: [PATCH 13/18] Added crossccybasisswap and related tests --- QuantLib.vcxproj | 2 + QuantLib.vcxproj.filters | 6 + ql/CMakeLists.txt | 2 + ql/instruments/Makefile.am | 2 + ql/instruments/crossccybasisswap.cpp | 176 +++++++++++++ ql/instruments/crossccybasisswap.hpp | 149 +++++++++++ test-suite/CMakeLists.txt | 1 + test-suite/Makefile.am | 1 + test-suite/crossccybasisswap.cpp | 356 +++++++++++++++++++++++++++ test-suite/testsuite.vcxproj | 1 + test-suite/testsuite.vcxproj.filters | 3 + 11 files changed, 699 insertions(+) create mode 100644 ql/instruments/crossccybasisswap.cpp create mode 100644 ql/instruments/crossccybasisswap.hpp create mode 100644 test-suite/crossccybasisswap.cpp diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index 989f08e2322..20d5cae79cb 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -912,6 +912,7 @@ + @@ -2190,6 +2191,7 @@ + diff --git a/QuantLib.vcxproj.filters b/QuantLib.vcxproj.filters index f555f51f79a..558092aa40d 100644 --- a/QuantLib.vcxproj.filters +++ b/QuantLib.vcxproj.filters @@ -822,6 +822,9 @@ instruments + + instruments + instruments @@ -4754,6 +4757,9 @@ instruments + + instruments + instruments diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 0fd56725288..d2a43a89362 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -271,6 +271,7 @@ set(QL_SOURCES instruments/cpicapfloor.cpp instruments/cpiswap.cpp instruments/creditdefaultswap.cpp + instruments/crossccybasisswap.cpp instruments/crossccyfixfloatswap.cpp instruments/crossccyswap.cpp instruments/doublebarrieroption.cpp @@ -1339,6 +1340,7 @@ set(QL_HEADERS instruments/cpicapfloor.hpp instruments/cpiswap.hpp instruments/creditdefaultswap.hpp + instruments/crossccybasisswap.hpp instruments/crossccyfixfloatswap.hpp instruments/crossccyswap.hpp instruments/dividendbarrieroption.hpp diff --git a/ql/instruments/Makefile.am b/ql/instruments/Makefile.am index add73d8a074..223aeec8115 100644 --- a/ql/instruments/Makefile.am +++ b/ql/instruments/Makefile.am @@ -25,6 +25,7 @@ this_include_HEADERS = \ cpicapfloor.hpp \ cpiswap.hpp \ creditdefaultswap.hpp \ + crossccybasisswap.hpp \ crossccyfixfloatswap.hpp \ crossccyswap.hpp \ dividendbarrieroption.hpp \ @@ -102,6 +103,7 @@ cpp_files = \ cpicapfloor.cpp \ cpiswap.cpp \ creditdefaultswap.cpp \ + crossccybasisswap.cpp \ crossccyfixfloatswap.cpp \ crossccyswap.cpp \ doublebarrieroption.cpp \ diff --git a/ql/instruments/crossccybasisswap.cpp b/ql/instruments/crossccybasisswap.cpp new file mode 100644 index 00000000000..c07ed86bcf9 --- /dev/null +++ b/ql/instruments/crossccybasisswap.cpp @@ -0,0 +1,176 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + All rights reserved. + + 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 +#include + +namespace QuantLib { + +CrossCcyBasisSwap::CrossCcyBasisSwap(Real payNominal, const Currency& payCurrency, const Schedule& paySchedule, + const ext::shared_ptr& payIndex, Spread paySpread, Real payGearing, + Real recNominal, const Currency& recCurrency, const Schedule& recSchedule, + const ext::shared_ptr& recIndex, Spread recSpread, Real recGearing, + Size payPaymentLag, Size recPaymentLag, ext::optional payIncludeSpread, + ext::optional payLookbackDays, ext::optional recIncludeSpread, + ext::optional recLookbackDays, const bool telescopicValueDates) + : CrossCcySwap(2), payNominal_(payNominal), payCurrency_(payCurrency), paySchedule_(paySchedule), + payIndex_(payIndex), paySpread_(paySpread), payGearing_(payGearing), recNominal_(recNominal), + recCurrency_(recCurrency), recSchedule_(recSchedule), recIndex_(recIndex), recSpread_(recSpread), + recGearing_(recGearing), payPaymentLag_(payPaymentLag), recPaymentLag_(recPaymentLag), + payIncludeSpread_(payIncludeSpread), payLookbackDays_(payLookbackDays), recIncludeSpread_(recIncludeSpread), + recLookbackDays_(recLookbackDays), telescopicValueDates_(telescopicValueDates) { + registerWith(payIndex_); + registerWith(recIndex_); + initialize(); +} + +void CrossCcyBasisSwap::initialize() { + // Pay leg + if (auto on = ext::dynamic_pointer_cast(payIndex_)) { + // ON leg + legs_[0] = OvernightLeg(paySchedule_, on) + .withNotionals(payNominal_) + .withSpreads(paySpread_) + .withGearings(payGearing_) + .withPaymentLag(payPaymentLag_) + .withSpreads(payIncludeSpread_ ? *payIncludeSpread_ : false) + .withLookbackDays(payLookbackDays_ ? *payLookbackDays_ : 0) + .withTelescopicValueDates(telescopicValueDates_); + } else { + // Ibor leg + legs_[0] = IborLeg(paySchedule_, payIndex_) + .withNotionals(payNominal_) + .withSpreads(paySpread_) + .withGearings(payGearing_) + .withPaymentLag(payPaymentLag_); + } + payer_[0] = -1.0; + currencies_[0] = payCurrency_; + // Pay leg notional exchange at start. + Date initialPayDate = paySchedule_.dates().front(); + ext::shared_ptr initialPayCF(new SimpleCashFlow(-payNominal_, initialPayDate)); + legs_[0].insert(legs_[0].begin(), initialPayCF); + // Pay leg notional exchange at end. + Date finalPayDate = paySchedule_.dates().back(); + ext::shared_ptr finalPayCF(new SimpleCashFlow(payNominal_, finalPayDate)); + legs_[0].push_back(finalPayCF); + + // Receive leg + if (auto on = ext::dynamic_pointer_cast(recIndex_)) { + // ON leg + legs_[1] = OvernightLeg(recSchedule_, on) + .withNotionals(recNominal_) + .withSpreads(recSpread_) + .withGearings(recGearing_) + .withPaymentLag(recPaymentLag_) + .withSpreads(recIncludeSpread_ ? *recIncludeSpread_ : false) + .withLookbackDays(recLookbackDays_ ? *recLookbackDays_ : 0) + .withTelescopicValueDates(telescopicValueDates_); + } else { + // Ibor leg + legs_[1] = IborLeg(recSchedule_, recIndex_) + .withNotionals(recNominal_) + .withSpreads(recSpread_) + .withGearings(recGearing_) + .withPaymentLag(recPaymentLag_); + } + payer_[1] = +1.0; + currencies_[1] = recCurrency_; + // Receive leg notional exchange at start. + Date initialRecDate = recSchedule_.dates().front(); + ext::shared_ptr initialRecCF(new SimpleCashFlow(-recNominal_, initialRecDate)); + legs_[1].insert(legs_[1].begin(), initialRecCF); + // Receive leg notional exchange at end. + Date finalRecDate = recSchedule_.dates().back(); + ext::shared_ptr finalRecCF(new SimpleCashFlow(recNominal_, finalRecDate)); + legs_[1].push_back(finalRecCF); + + // Register the instrument with all cashflows on each leg. + for (Size legNo = 0; legNo < 2; legNo++) { + Leg::iterator it; + for (it = legs_[legNo].begin(); it != legs_[legNo].end(); ++it) { + registerWith(*it); + } + } +} + +void CrossCcyBasisSwap::setupArguments(PricingEngine::arguments* args) const { + + CrossCcySwap::setupArguments(args); + + CrossCcyBasisSwap::arguments* arguments = dynamic_cast(args); + + /* Returns here if e.g. args is CrossCcySwap::arguments which + is the case if PricingEngine is a CrossCcySwap::engine. */ + if (!arguments) + return; + + arguments->paySpread = paySpread_; + arguments->recSpread = recSpread_; +} + +void CrossCcyBasisSwap::fetchResults(const PricingEngine::results* r) const { + + CrossCcySwap::fetchResults(r); + + const CrossCcyBasisSwap::results* results = dynamic_cast(r); + if (results) { + /* If PricingEngine::results are of type + CrossCcyBasisSwap::results */ + fairPaySpread_ = results->fairPaySpread; + fairRecSpread_ = results->fairRecSpread; + } else { + /* If not, e.g. if the engine is a CrossCcySwap::engine */ + fairPaySpread_ = Null(); + fairRecSpread_ = Null(); + } + + /* Calculate the fair pay and receive spreads if they are null */ + static Spread basisPoint = 1.0e-4; + if (fairPaySpread_ == Null()) { + if (legBPS_[0] != Null()) + fairPaySpread_ = paySpread_ - NPV_ / (legBPS_[0] / basisPoint); + } + if (fairRecSpread_ == Null()) { + if (legBPS_[1] != Null()) + fairRecSpread_ = recSpread_ - NPV_ / (legBPS_[1] / basisPoint); + } +} + +void CrossCcyBasisSwap::setupExpired() const { + CrossCcySwap::setupExpired(); + fairPaySpread_ = Null(); + fairRecSpread_ = Null(); +} + +void CrossCcyBasisSwap::arguments::validate() const { + CrossCcySwap::arguments::validate(); + QL_REQUIRE(paySpread != Null(), "Pay spread cannot be null"); + QL_REQUIRE(recSpread != Null(), "Rec spread cannot be null"); +} + +void CrossCcyBasisSwap::results::reset() { + CrossCcySwap::results::reset(); + fairPaySpread = Null(); + fairRecSpread = Null(); +} +} // namespace QuantLib diff --git a/ql/instruments/crossccybasisswap.hpp b/ql/instruments/crossccybasisswap.hpp new file mode 100644 index 00000000000..29b75120b77 --- /dev/null +++ b/ql/instruments/crossccybasisswap.hpp @@ -0,0 +1,149 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + All rights reserved. + + 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 crossccybasisswap.hpp + \brief Cross currency basis swap instrument + + \ingroup instruments +*/ + +#ifndef quantlib_cross_ccy_basis_swap_hpp +#define quantlib_cross_ccy_basis_swap_hpp + +#include +#include +#include + +namespace QuantLib { + +//! Cross currency basis swap +/*! The first leg holds the pay currency cashflows and second leg holds + the receive currency cashflows. + + \ingroup instruments +*/ +class CrossCcyBasisSwap : public CrossCcySwap { +public: + class arguments; + class results; + //! \name Constructors + //@{ + /*! First leg holds the pay currency cashflows and the second leg + holds the receive currency cashflows. + */ + CrossCcyBasisSwap( + Real payNominal, const Currency& payCurrency, const Schedule& paySchedule, + const ext::shared_ptr& payIndex, Spread paySpread, Real payGearing, Real recNominal, + const Currency& recCurrency, const Schedule& recSchedule, const ext::shared_ptr& recIndex, + Spread recSpread, Real recGearing, Size payPaymentLag = 0, Size recPaymentLag = 0, + ext::optional payIncludeSpread = ext::nullopt, ext::optional payLookbackDays = ext::nullopt, + ext::optional recIncludeSpread = ext::nullopt, ext::optional recLookbackDays = ext::nullopt, + const bool telescopicValueDates = false); + //@} + //! \name Instrument interface + //@{ + void setupArguments(PricingEngine::arguments* args) const override; + void fetchResults(const PricingEngine::results*) const override; + //@} + //! \name Inspectors + //@{ + Real payNominal() const { return payNominal_; } + const Currency& payCurrency() const { return payCurrency_; } + const Schedule& paySchedule() const { return paySchedule_; } + const ext::shared_ptr& payIndex() const { return payIndex_; } + Spread paySpread() const { return paySpread_; } + Real payGearing() const { return payGearing_; } + + Real recNominal() const { return recNominal_; } + const Currency& recCurrency() const { return recCurrency_; } + const Schedule& recSchedule() const { return recSchedule_; } + const ext::shared_ptr& recIndex() const { return recIndex_; } + Spread recSpread() const { return recSpread_; } + Real recGearing() const { return recGearing_; } + //@} + + //! \name Additional interface + //@{ + Spread fairPaySpread() const { + calculate(); + QL_REQUIRE(fairPaySpread_ != Null(), "Fair pay spread is not available"); + return fairPaySpread_; + } + Spread fairRecSpread() const { + calculate(); + QL_REQUIRE(fairRecSpread_ != Null(), "Fair pay spread is not available"); + return fairRecSpread_; + } + //@} + +protected: + //! \name Instrument interface + //@{ + void setupExpired() const override; + //@} + +private: + void initialize(); + + Real payNominal_; + Currency payCurrency_; + Schedule paySchedule_; + ext::shared_ptr payIndex_; + Spread paySpread_; + Real payGearing_; + + Real recNominal_; + Currency recCurrency_; + Schedule recSchedule_; + ext::shared_ptr recIndex_; + Spread recSpread_; + Real recGearing_; + + Size payPaymentLag_; + Size recPaymentLag_; + // OIS only + ext::optional payIncludeSpread_; + ext::optional payLookbackDays_; + ext::optional recIncludeSpread_; + ext::optional recLookbackDays_; + bool telescopicValueDates_; + + mutable Spread fairPaySpread_; + mutable Spread fairRecSpread_; +}; + +//! \ingroup instruments +class CrossCcyBasisSwap::arguments : public CrossCcySwap::arguments { +public: + Spread paySpread; + Spread recSpread; + void validate() const override; +}; + +//! \ingroup instruments +class CrossCcyBasisSwap::results : public CrossCcySwap::results { +public: + Spread fairPaySpread; + Spread fairRecSpread; + void reset() override; +}; +} // namespace QuantLib + +#endif diff --git a/test-suite/CMakeLists.txt b/test-suite/CMakeLists.txt index bf9ef1d4279..3c5b35e7042 100644 --- a/test-suite/CMakeLists.txt +++ b/test-suite/CMakeLists.txt @@ -39,6 +39,7 @@ set(QL_TEST_SOURCES covariance.cpp creditdefaultswap.cpp creditriskplus.cpp + crossccybasisswap.cpp crossccyfixfloatswap.cpp crossccyswap.cpp crosscurrencyratehelpers.cpp diff --git a/test-suite/Makefile.am b/test-suite/Makefile.am index f2852bad5ea..c2cc0714ff8 100644 --- a/test-suite/Makefile.am +++ b/test-suite/Makefile.am @@ -40,6 +40,7 @@ QL_TEST_SRCS = \ covariance.cpp \ creditdefaultswap.cpp \ creditriskplus.cpp \ + crossccybasissswap.cpp \ crossccyfixfloatswap.cpp \ crossccyswap.cpp \ crosscurrencyratehelpers.cpp \ diff --git a/test-suite/crossccybasisswap.cpp b/test-suite/crossccybasisswap.cpp new file mode 100644 index 00000000000..4807cc0bc1f --- /dev/null +++ b/test-suite/crossccybasisswap.cpp @@ -0,0 +1,356 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2025 Paolo D'Elia + All rights reserved. + + 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 "toplevelfixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace boost::unit_test_framework; +using namespace QuantLib; + +BOOST_FIXTURE_TEST_SUITE(QuantLibTests, TopLevelFixture) + +BOOST_AUTO_TEST_SUITE(CrossCcyBasisSwapTest) + +#define CHECK_XCCY_SWAP_RESULT(what, calculated, expected, tolerance) \ + if (std::fabs(calculated-expected) > tolerance) { \ + BOOST_ERROR("Failed to reproduce " what ":" \ + << "\n expected: " << std::setprecision(12) << expected \ + << "\n calculated: " << std::setprecision(12) << calculated \ + << "\n error: " << std::setprecision(12) << std::fabs(calculated-expected)); \ + } + +Handle USDDiscountCurve() { + + vector dates(27); + vector dfs(27); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(14, Sep, 2018); + dfs[1] = 0.99994666951096; + dates[2] = Date(20, Sep, 2018); + dfs[2] = 0.999627719221066; + dates[3] = Date(27, Sep, 2018); + dfs[3] = 0.999254084816959; + dates[4] = Date(04, Oct, 2018); + dfs[4] = 0.998837020905631; + dates[5] = Date(15, Oct, 2018); + dfs[5] = 0.998176132423265; + dates[6] = Date(13, Nov, 2018); + dfs[6] = 0.99644587210048; + dates[7] = Date(13, Dec, 2018); + dfs[7] = 0.994644668243218; + dates[8] = Date(14, Jan, 2019); + dfs[8] = 0.992596634984033; + dates[9] = Date(13, Feb, 2019); + dfs[9] = 0.990636503861861; + dates[10] = Date(13, Mar, 2019); + dfs[10] = 0.988809127958345; + dates[11] = Date(13, Jun, 2019); + dfs[11] = 0.982417991680868; + dates[12] = Date(13, Sep, 2019); + dfs[12] = 0.975723193871552; + dates[13] = Date(13, Mar, 2020); + dfs[13] = 0.96219213956104; + dates[14] = Date(14, Sep, 2020); + dfs[14] = 0.948588232418325; + dates[15] = Date(13, Sep, 2021); + dfs[15] = 0.92279636773464; + dates[16] = Date(13, Sep, 2022); + dfs[16] = 0.898345201557914; + dates[17] = Date(13, Sep, 2023); + dfs[17] = 0.874715322269088; + dates[18] = Date(15, Sep, 2025); + dfs[18] = 0.828658611114833; + dates[19] = Date(13, Sep, 2028); + dfs[19] = 0.763030152740947; + dates[20] = Date(13, Sep, 2030); + dfs[20] = 0.722238847877756; + dates[21] = Date(13, Sep, 2033); + dfs[21] = 0.664460629674362; + dates[22] = Date(13, Sep, 2038); + dfs[22] = 0.580288693473926; + dates[23] = Date(14, Sep, 2043); + dfs[23] = 0.510857007600479; + dates[24] = Date(14, Sep, 2048); + dfs[24] = 0.44941525649436; + dates[25] = Date(13, Sep, 2058); + dfs[25] = 0.352389176933952; + dates[26] = Date(13, Sep, 2068); + dfs[26] = 0.28183300653329; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle USDProjectionCurve() { + + vector dates(25); + vector dfs(25); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(13, Dec, 2018); + dfs[1] = 0.994134145990132; + dates[2] = Date(19, Dec, 2018); + dfs[2] = 0.993695776146116; + dates[3] = Date(20, Mar, 2019); + dfs[3] = 0.987047992958673; + dates[4] = Date(19, Jun, 2019); + dfs[4] = 0.980016364694049; + dates[5] = Date(18, Sep, 2019); + dfs[5] = 0.972708376777628; + dates[6] = Date(18, Dec, 2019); + dfs[6] = 0.965277162951128; + dates[7] = Date(18, Mar, 2020); + dfs[7] = 0.957799302363697; + dates[8] = Date(14, Sep, 2020); + dfs[8] = 0.943264331984248; + dates[9] = Date(13, Sep, 2021); + dfs[9] = 0.914816470778467; + dates[10] = Date(13, Sep, 2022); + dfs[10] = 0.88764714641623; + dates[11] = Date(13, Sep, 2023); + dfs[11] = 0.861475671008934; + dates[12] = Date(13, Sep, 2024); + dfs[12] = 0.835944798717806; + dates[13] = Date(15, Sep, 2025); + dfs[13] = 0.810833947617338; + dates[14] = Date(14, Sep, 2026); + dfs[14] = 0.78631849267276; + dates[15] = Date(13, Sep, 2027); + dfs[15] = 0.762267648509673; + dates[16] = Date(13, Sep, 2028); + dfs[16] = 0.738613627359076; + dates[17] = Date(13, Sep, 2029); + dfs[17] = 0.715502378943932; + dates[18] = Date(13, Sep, 2030); + dfs[18] = 0.693380472578176; + dates[19] = Date(13, Sep, 2033); + dfs[19] = 0.631097994110912; + dates[20] = Date(13, Sep, 2038); + dfs[20] = 0.540797634630251; + dates[21] = Date(14, Sep, 2043); + dfs[21] = 0.465599237331079; + dates[22] = Date(14, Sep, 2048); + dfs[22] = 0.402119473746341; + dates[23] = Date(13, Sep, 2058); + dfs[23] = 0.303129773289934; + dates[24] = Date(13, Sep, 2068); + dfs[24] = 0.23210070222569; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle GBPDiscountCurve() { + + vector dates(27); + vector dfs(27); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(14, Sep, 2018); + dfs[1] = 0.99994666951096; + dates[2] = Date(20, Sep, 2018); + dfs[2] = 0.999627719221066; + dates[3] = Date(27, Sep, 2018); + dfs[3] = 0.999254084816959; + dates[4] = Date(04, Oct, 2018); + dfs[4] = 0.998837020905631; + dates[5] = Date(15, Oct, 2018); + dfs[5] = 0.998176132423265; + dates[6] = Date(13, Nov, 2018); + dfs[6] = 0.99644587210048; + dates[7] = Date(13, Dec, 2018); + dfs[7] = 0.994644668243218; + dates[8] = Date(14, Jan, 2019); + dfs[8] = 0.992596634984033; + dates[9] = Date(13, Feb, 2019); + dfs[9] = 0.990636503861861; + dates[10] = Date(13, Mar, 2019); + dfs[10] = 0.988809127958345; + dates[11] = Date(13, Jun, 2019); + dfs[11] = 0.982417991680868; + dates[12] = Date(13, Sep, 2019); + dfs[12] = 0.975723193871552; + dates[13] = Date(13, Mar, 2020); + dfs[13] = 0.96219213956104; + dates[14] = Date(14, Sep, 2020); + dfs[14] = 0.948588232418325; + dates[15] = Date(13, Sep, 2021); + dfs[15] = 0.92279636773464; + dates[16] = Date(13, Sep, 2022); + dfs[16] = 0.898345201557914; + dates[17] = Date(13, Sep, 2023); + dfs[17] = 0.874715322269088; + dates[18] = Date(15, Sep, 2025); + dfs[18] = 0.828658611114833; + dates[19] = Date(13, Sep, 2028); + dfs[19] = 0.763030152740947; + dates[20] = Date(13, Sep, 2030); + dfs[20] = 0.722238847877756; + dates[21] = Date(13, Sep, 2033); + dfs[21] = 0.664460629674362; + dates[22] = Date(13, Sep, 2038); + dfs[22] = 0.580288693473926; + dates[23] = Date(14, Sep, 2043); + dfs[23] = 0.510857007600479; + dates[24] = Date(14, Sep, 2048); + dfs[24] = 0.44941525649436; + dates[25] = Date(13, Sep, 2058); + dfs[25] = 0.352389176933952; + dates[26] = Date(13, Sep, 2068); + dfs[26] = 0.28183300653329; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +Handle GBPProjectionCurve() { + + vector dates(25); + vector dfs(25); + Actual365Fixed dayCounter; + + dates[0] = Date(11, Sep, 2018); + dfs[0] = 1; + dates[1] = Date(13, Dec, 2018); + dfs[1] = 0.994134145990132; + dates[2] = Date(19, Dec, 2018); + dfs[2] = 0.993695776146116; + dates[3] = Date(20, Mar, 2019); + dfs[3] = 0.987047992958673; + dates[4] = Date(19, Jun, 2019); + dfs[4] = 0.980016364694049; + dates[5] = Date(18, Sep, 2019); + dfs[5] = 0.972708376777628; + dates[6] = Date(18, Dec, 2019); + dfs[6] = 0.965277162951128; + dates[7] = Date(18, Mar, 2020); + dfs[7] = 0.957799302363697; + dates[8] = Date(14, Sep, 2020); + dfs[8] = 0.943264331984248; + dates[9] = Date(13, Sep, 2021); + dfs[9] = 0.914816470778467; + dates[10] = Date(13, Sep, 2022); + dfs[10] = 0.88764714641623; + dates[11] = Date(13, Sep, 2023); + dfs[11] = 0.861475671008934; + dates[12] = Date(13, Sep, 2024); + dfs[12] = 0.835944798717806; + dates[13] = Date(15, Sep, 2025); + dfs[13] = 0.810833947617338; + dates[14] = Date(14, Sep, 2026); + dfs[14] = 0.78631849267276; + dates[15] = Date(13, Sep, 2027); + dfs[15] = 0.762267648509673; + dates[16] = Date(13, Sep, 2028); + dfs[16] = 0.738613627359076; + dates[17] = Date(13, Sep, 2029); + dfs[17] = 0.715502378943932; + dates[18] = Date(13, Sep, 2030); + dfs[18] = 0.693380472578176; + dates[19] = Date(13, Sep, 2033); + dfs[19] = 0.631097994110912; + dates[20] = Date(13, Sep, 2038); + dfs[20] = 0.540797634630251; + dates[21] = Date(14, Sep, 2043); + dfs[21] = 0.465599237331079; + dates[22] = Date(14, Sep, 2048); + dfs[22] = 0.402119473746341; + dates[23] = Date(13, Sep, 2058); + dfs[23] = 0.303129773289934; + dates[24] = Date(13, Sep, 2068); + dfs[24] = 0.23210070222569; + + return Handle(ext::make_shared(dates, dfs, dayCounter)); +} + +ext::shared_ptr makeBasisXCCY(Rate spotFx, Spread GBPSpread) { + + // USD nominal + Real GBPNominal = 10000000.0; + + // Dates and calendars + JointCalendar payCalendar = JointCalendar(UnitedStates(UnitedStates::Settlement), UnitedKingdom()); + Date referenceDate = Settings::instance().evaluationDate(); + referenceDate = payCalendar.adjust(referenceDate); + Date start = payCalendar.advance(referenceDate, 2 * Days); + Date end = start + 5 * Years; + Schedule schedule(start, end, 3 * Months, payCalendar, ModifiedFollowing, ModifiedFollowing, + DateGeneration::Backward, false); + + // Indices + auto USDindex = ext::make_shared(3 * Months, USDProjectionCurve()); + auto GBPindex = ext::make_shared(3 * Months, GBPProjectionCurve()); + + // Create swap + return ext::shared_ptr(new CrossCcyBasisSwap( + GBPNominal, GBPCurrency(), schedule, GBPindex, GBPSpread, 1.0, + GBPNominal * spotFx, USDCurrency(), schedule, USDindex, 0.0, 1.0)); +} + + +BOOST_AUTO_TEST_CASE(testBasisXCCYSwapPricing) { + BOOST_TEST_MESSAGE("Test cross currency basis swap pricing against known results"); + + SavedSettings backup; + Settings::instance().evaluationDate() = Date(11, Sep, 2018); + bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); + + // Create swap + Rate spotFx = 1; + Spread spread = 0; + auto xccy = makeBasisXCCY(spotFx, spread); + + // Attach pricing engine + auto fxSpotQuote = makeQuoteHandle(spotFx); + auto engine = ext::make_shared( + USDCurrency(), USDDiscountCurve(), GBPCurrency(), GBPDiscountCurve(), fxSpotQuote); + + xccy->setPricingEngine(engine); + + // Check values + Real tol = 0.01; + Real expectedNPV = usingAtParCoupons ? 0.0 : 0.0; + + CHECK_XCCY_SWAP_RESULT("NPV", xccy->NPV(), expectedNPV, tol); + + Real expBps = -4670.170509677384; // cached value + CHECK_XCCY_SWAP_RESULT("Leg 0 BPS", xccy->legBPS(0), expBps, tol); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/testsuite.vcxproj b/test-suite/testsuite.vcxproj index 24255aa8316..809045e08e0 100644 --- a/test-suite/testsuite.vcxproj +++ b/test-suite/testsuite.vcxproj @@ -684,6 +684,7 @@ + diff --git a/test-suite/testsuite.vcxproj.filters b/test-suite/testsuite.vcxproj.filters index 78bd90e0a33..67cfe7e0115 100644 --- a/test-suite/testsuite.vcxproj.filters +++ b/test-suite/testsuite.vcxproj.filters @@ -501,6 +501,9 @@ Source Files + + Source Files + Source Files From 77e673f17c998282af66294a7498846a663c1901 Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Tue, 17 Jun 2025 19:23:11 +0200 Subject: [PATCH 14/18] Fix typo in crossccybasisswap filename in Makefile.am --- test-suite/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite/Makefile.am b/test-suite/Makefile.am index c2cc0714ff8..b1da5d76d4f 100644 --- a/test-suite/Makefile.am +++ b/test-suite/Makefile.am @@ -40,7 +40,7 @@ QL_TEST_SRCS = \ covariance.cpp \ creditdefaultswap.cpp \ creditriskplus.cpp \ - crossccybasissswap.cpp \ + crossccybasisswap.cpp \ crossccyfixfloatswap.cpp \ crossccyswap.cpp \ crosscurrencyratehelpers.cpp \ From e35c609ae3e94c4a7313dde75ad7ccaae53f6810 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:25:20 +0000 Subject: [PATCH 15/18] Update generated headers --- ql/instruments/all.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ql/instruments/all.hpp b/ql/instruments/all.hpp index a6326bcbbf7..d2264fe3b28 100644 --- a/ql/instruments/all.hpp +++ b/ql/instruments/all.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include From 496d95d6226bb2dcf699dca689799a55fe4f9f55 Mon Sep 17 00:00:00 2001 From: Paolo D'Elia Date: Thu, 19 Jun 2025 15:13:14 +0200 Subject: [PATCH 16/18] Add constructor docstring in crossccybasisswap.hpp --- ql/instruments/crossccybasisswap.hpp | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/ql/instruments/crossccybasisswap.hpp b/ql/instruments/crossccybasisswap.hpp index 29b75120b77..f23f5b8c562 100644 --- a/ql/instruments/crossccybasisswap.hpp +++ b/ql/instruments/crossccybasisswap.hpp @@ -45,8 +45,30 @@ class CrossCcyBasisSwap : public CrossCcySwap { class results; //! \name Constructors //@{ - /*! First leg holds the pay currency cashflows and the second leg - holds the receive currency cashflows. + /*! + \brief Constructs a cross-currency basis swap. + + First leg holds the pay currency cashflows and the second leg holds the receive currency cashflows. + + \param payNominal Notional amount for the pay leg. + \param payCurrency Currency of the pay leg. + \param paySchedule Payment schedule for the pay leg. + \param payIndex Floating rate index for the pay leg. + \param paySpread Spread over the floating rate for the pay leg. + \param payGearing Gearing factor for the pay leg. + \param recNominal Notional amount for the receive leg. + \param recCurrency Currency of the receive leg. + \param recSchedule Payment schedule for the receive leg. + \param recIndex Floating rate index for the receive leg. + \param recSpread Spread over the floating rate for the receive leg. + \param recGearing Gearing factor for the receive leg. + \param payPaymentLag Payment lag for the pay leg (default: 0). + \param recPaymentLag Payment lag for the receive leg (default: 0). + \param payIncludeSpread Optional flag to include the spread in the pay leg calculation (default: null). + \param payLookbackDays Optional lookback days for the pay leg (default: null). + \param recIncludeSpread Optional flag to include the spread in the receive leg calculation (default: null). + \param recLookbackDays Optional lookback days for the receive leg (default: null). + \param telescopicValueDates Flag indicating whether telescopic value dates are used (default: false). */ CrossCcyBasisSwap( Real payNominal, const Currency& payCurrency, const Schedule& paySchedule, From 9c82a36e02280f4a95cc99ba20e23350ebbdad33 Mon Sep 17 00:00:00 2001 From: Paolo D'Elia Date: Thu, 19 Jun 2025 15:15:13 +0200 Subject: [PATCH 17/18] Add constructor docstring in crossccyfixfloatswap.hpp --- ql/instruments/crossccyfixfloatswap.hpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/ql/instruments/crossccyfixfloatswap.hpp b/ql/instruments/crossccyfixfloatswap.hpp index 9048fc2bfda..78c7f67f39e 100644 --- a/ql/instruments/crossccyfixfloatswap.hpp +++ b/ql/instruments/crossccyfixfloatswap.hpp @@ -43,7 +43,30 @@ class CrossCcyFixFloatSwap : public CrossCcySwap { //! \name Constructors //@{ - //! Detailed constructor + /*! + \brief Constructs a cross-currency fixed vs floating rate swap. + + This instrument represents a cross-currency swap where one leg pays fixed-rate cashflows in one currency, + and the other leg pays floating-rate cashflows in another currency. + + \param type The type of the swap (Receiver or Payer). + \param fixedNominal Notional amount for the fixed leg. + \param fixedCurrency Currency of the fixed leg. + \param fixedSchedule Payment schedule for the fixed leg. + \param fixedRate Fixed interest rate for the fixed leg. + \param fixedDayCount Day count convention for the fixed leg. + \param fixedPaymentBdc Business day convention for fixed leg payments. + \param fixedPaymentLag Payment lag for the fixed leg (default: 0). + \param fixedPaymentCalendar Calendar for fixed leg payments. + \param floatNominal Notional amount for the floating leg. + \param floatCurrency Currency of the floating leg. + \param floatSchedule Payment schedule for the floating leg. + \param floatIndex Floating rate index for the floating leg. + \param floatSpread Spread over the floating rate for the floating leg. + \param floatPaymentBdc Business day convention for floating leg payments. + \param floatPaymentLag Payment lag for the floating leg (default: 0). + \param floatPaymentCalendar Calendar for floating leg payments. + */ CrossCcyFixFloatSwap(Type type, Real fixedNominal, const Currency& fixedCurrency, const Schedule& fixedSchedule, Rate fixedRate, const DayCounter& fixedDayCount, BusinessDayConvention fixedPaymentBdc, From 68c83530bbcf3414f7bae0cf937f97cd80ff2f2d Mon Sep 17 00:00:00 2001 From: paolodelia99 Date: Fri, 1 Aug 2025 15:18:54 +0200 Subject: [PATCH 18/18] Rename xccyswapengine constructor params --- ql/pricingengines/swap/crossccyswapengine.cpp | 44 +++++++++---------- ql/pricingengines/swap/crossccyswapengine.hpp | 38 ++++++++-------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/ql/pricingengines/swap/crossccyswapengine.cpp b/ql/pricingengines/swap/crossccyswapengine.cpp index 85824299c6e..be9d07ef2ab 100644 --- a/ql/pricingengines/swap/crossccyswapengine.cpp +++ b/ql/pricingengines/swap/crossccyswapengine.cpp @@ -26,30 +26,30 @@ namespace QuantLib { -CrossCcySwapEngine::CrossCcySwapEngine(const Currency& ccy1, const Handle& currency1Discountcurve, - const Currency& ccy2, const Handle& currency2Discountcurve, +CrossCcySwapEngine::CrossCcySwapEngine(const Currency& domesticCcy, const Handle& domesticCcyDiscountcurve, + const Currency& foreignCcy, const Handle& foreignCcyDiscountcurve, const Handle& spotFX, ext::optional includeSettlementDateFlows, const Date& settlementDate, const Date& npvDate, const Date& spotFXSettleDate) - : ccy1_(ccy1), currency1Discountcurve_(currency1Discountcurve), ccy2_(ccy2), - currency2Discountcurve_(currency2Discountcurve), spotFX_(spotFX), + : domesticCcy_(domesticCcy), domesticCcyDiscountcurve_(domesticCcyDiscountcurve), foreignCcy_(foreignCcy), + foreignCcyDiscountcurve_(foreignCcyDiscountcurve), spotFX_(spotFX), includeSettlementDateFlows_(includeSettlementDateFlows), settlementDate_(settlementDate), npvDate_(npvDate), spotFXSettleDate_(spotFXSettleDate) { - registerWith(currency1Discountcurve_); - registerWith(currency2Discountcurve_); + registerWith(domesticCcyDiscountcurve_); + registerWith(foreignCcyDiscountcurve_); registerWith(spotFX_); } void CrossCcySwapEngine::calculate() const { - QL_REQUIRE(!currency1Discountcurve_.empty() && !currency2Discountcurve_.empty(), + QL_REQUIRE(!domesticCcyDiscountcurve_.empty() && !foreignCcyDiscountcurve_.empty(), "Discounting term structure handle is empty."); QL_REQUIRE(!spotFX_.empty(), "FX spot quote handle is empty."); - QL_REQUIRE(currency1Discountcurve_->referenceDate() == currency2Discountcurve_->referenceDate(), + QL_REQUIRE(domesticCcyDiscountcurve_->referenceDate() == foreignCcyDiscountcurve_->referenceDate(), "Term structures should have the same reference date."); - Date referenceDate = currency1Discountcurve_->referenceDate(); + Date referenceDate = domesticCcyDiscountcurve_->referenceDate(); Date settlementDate = settlementDate_; if (settlementDate_ == Date()) { settlementDate = referenceDate; @@ -101,13 +101,13 @@ void CrossCcySwapEngine::calculate() const { try { // Choose the correct discount curve for the leg. Handle legDiscountCurve; - if (arguments_.currencies[legNo] == ccy1_) { - legDiscountCurve = currency1Discountcurve_; + if (arguments_.currencies[legNo] == domesticCcy_) { + legDiscountCurve = domesticCcyDiscountcurve_; } else { - QL_REQUIRE(arguments_.currencies[legNo] == ccy2_, "leg ccy (" << arguments_.currencies[legNo] - << ") must be ccy1 (" << ccy1_ - << ") or ccy2 (" << ccy2_ << ")"); - legDiscountCurve = currency2Discountcurve_; + QL_REQUIRE(arguments_.currencies[legNo] == foreignCcy_, "leg ccy (" << arguments_.currencies[legNo] + << ") must be domesticCcy (" << domesticCcy_ + << ") or foreignCcy (" << foreignCcy_ << ")"); + legDiscountCurve = foreignCcyDiscountcurve_; } results_.npvDateDiscounts[legNo] = legDiscountCurve->discount(results_.valuationDate); @@ -122,7 +122,7 @@ void CrossCcySwapEngine::calculate() const { results_.legBPS[legNo] = results_.inCcyLegBPS[legNo]; // Convert to NPV currency if necessary. - if (arguments_.currencies[legNo] != ccy1_) { + if (arguments_.currencies[legNo] != domesticCcy_) { // results_.legNPV[legNo] *= spotFX_->value(); // results_.legBPS[legNo] *= spotFX_->value(); Real spotFXRate = spotFX_->value(); @@ -130,11 +130,11 @@ void CrossCcySwapEngine::calculate() const { // Use the parity relation between discount factors and fx rates to compute spotFXRate // Generic formula: fx(T1)/fx(T2) = FwdDF_Quote(T1->T2) / FwdDF_Base(T1->T2), // where fx represents the currency ratio Base/Quote - Real ccy1DF = currency1Discountcurve_->discount(spotFXSettleDate); - Real ccy2DF = currency2Discountcurve_->discount(spotFXSettleDate); - QL_REQUIRE(ccy2DF != 0.0, "Discount Factor associated with currency " << ccy2_ + Real domesticCcyDF = domesticCcyDiscountcurve_->discount(spotFXSettleDate); + Real foreignCcyDF = foreignCcyDiscountcurve_->discount(spotFXSettleDate); + QL_REQUIRE(foreignCcyDF != 0.0, "Discount Factor associated with currency " << foreignCcy_ << " at maturity " << spotFXSettleDate << " cannot be zero"); - spotFXRate *= ccy1DF / ccy2DF; + spotFXRate *= domesticCcyDF / foreignCcyDF; } results_.legNPV[legNo] *= spotFXRate; results_.legBPS[legNo] *= spotFXRate; @@ -142,14 +142,14 @@ void CrossCcySwapEngine::calculate() const { // Get start date and end date discount for the leg Date startDate = CashFlows::startDate(arguments_.legs[legNo]); - if (startDate >= currency1Discountcurve_->referenceDate()) { + if (startDate >= domesticCcyDiscountcurve_->referenceDate()) { results_.startDiscounts[legNo] = legDiscountCurve->discount(startDate); } else { results_.startDiscounts[legNo] = Null(); } Date maturityDate = CashFlows::maturityDate(arguments_.legs[legNo]); - if (maturityDate >= currency1Discountcurve_->referenceDate()) { + if (maturityDate >= domesticCcyDiscountcurve_->referenceDate()) { results_.endDiscounts[legNo] = legDiscountCurve->discount(maturityDate); } else { results_.endDiscounts[legNo] = Null(); diff --git a/ql/pricingengines/swap/crossccyswapengine.hpp b/ql/pricingengines/swap/crossccyswapengine.hpp index a5cf0faf34a..07e777fcef3 100644 --- a/ql/pricingengines/swap/crossccyswapengine.hpp +++ b/ql/pricingengines/swap/crossccyswapengine.hpp @@ -37,27 +37,27 @@ namespace QuantLib { //! Cross currency swap engine /*! This class implements an engine for pricing swaps comprising legs that - involve two currencies. The npv is expressed in ccy1. The given currencies - ccy1 and ccy2 are matched to the correct swap legs. The evaluation date is the + involve two currencies. The npv is expressed in domesticCcy. The given currencies + domesticCcy and foreignCcy are matched to the correct swap legs. The evaluation date is the reference date of either discounting curve (which must be equal). - \ingroup engines + \ingroup engines */ class CrossCcySwapEngine : public CrossCcySwap::engine { public: //! \name Constructors //@{ - /*! \param ccy1 + /*! \param domesticCcy Currency 1 - \param currency1DiscountCurve + \param domesitcCcyDiscountCurve Discount curve for cash flows in currency 1 - \param ccy2 + \param foreignCcy Currency 2 - \param currency2DiscountCurve + \param foreignCcyDiscountCurve Discount curve for cash flows in currency 2 \param spotFX - The market spot rate quote, given as units of ccy1 - for one unit of ccy2. The spot rate must be given + The market spot rate quote, given as units of domesticCcy + for one unit of foreignCcy. The spot rate must be given w.r.t. a settlement equal to the npv date. \param includeSettlementDateFlows, settlementDate If includeSettlementDateFlows is true (false), cashflows @@ -70,8 +70,8 @@ class CrossCcySwapEngine : public CrossCcySwap::engine { \param spotFXSettleDate FX conversion as of this date if specified explicitly */ - CrossCcySwapEngine(const Currency& ccy1, const Handle& currency1DiscountCurve, - const Currency& ccy2, const Handle& currency2DiscountCurve, + CrossCcySwapEngine(const Currency& domesticCcy, const Handle& domesitcCcyDiscountCurve, + const Currency& foreignCcy, const Handle& foreignCcyDiscountCurve, const Handle& spotFX, ext::optional includeSettlementDateFlows = ext::nullopt, const Date& settlementDate = Date(), const Date& npvDate = Date(), const Date& spotFXSettleDate = Date()); //@} @@ -83,20 +83,20 @@ class CrossCcySwapEngine : public CrossCcySwap::engine { //! \name Inspectors //@{ - const Handle& currency1DiscountCurve() const { return currency1Discountcurve_; } - const Handle& currency2DiscountCurve() const { return currency2Discountcurve_; } + const Handle& domesitcCcyDiscountCurve() const { return domesticCcyDiscountcurve_; } + const Handle& foreignCcyDiscountCurve() const { return foreignCcyDiscountcurve_; } - const Currency& currency1() const { return ccy1_; } - const Currency& currency2() const { return ccy2_; } + const Currency& currency1() const { return domesticCcy_; } + const Currency& currency2() const { return foreignCcy_; } const Handle& spotFX() const { return spotFX_; } //@} private: - Currency ccy1_; - Handle currency1Discountcurve_; - Currency ccy2_; - Handle currency2Discountcurve_; + Currency domesticCcy_; + Handle domesticCcyDiscountcurve_; + Currency foreignCcy_; + Handle foreignCcyDiscountcurve_; Handle spotFX_; ext::optional includeSettlementDateFlows_; Date settlementDate_;