diff --git a/LICENSE.TXT b/LICENSE.TXT index 620bc216b9a..df32dea5db4 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, 2020, 2022 Quaternion Risk Management Ltd Copyright (C) 2016, 2019, 2020 Eisuke Tani -Copyright (C) 2016, 2020, 2022 Quaternion Risk Management Ltd Copyright (C) 2017 BN Algorithms Ltd Copyright (C) 2017 Joseph Jeisman diff --git a/QuantLib.vcxproj b/QuantLib.vcxproj index fefce491f95..08905f9bda9 100644 --- a/QuantLib.vcxproj +++ b/QuantLib.vcxproj @@ -914,6 +914,9 @@ + + + @@ -1587,6 +1590,7 @@ + @@ -2195,6 +2199,9 @@ + + + @@ -2649,6 +2656,7 @@ + diff --git a/QuantLib.vcxproj.filters b/QuantLib.vcxproj.filters index cf31803b959..8e57a48689a 100644 --- a/QuantLib.vcxproj.filters +++ b/QuantLib.vcxproj.filters @@ -834,6 +834,15 @@ instruments + + instruments + + + instruments + + + instruments + instruments @@ -2730,6 +2739,9 @@ pricingengines\swap + + pricingengines\swap + pricingengines\swap @@ -4793,6 +4805,15 @@ instruments + + instruments + + + instruments + + + instruments + instruments @@ -6038,6 +6059,9 @@ pricingengines\bond + + pricingengines\swap + pricingengines\swap diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 48633262538..809443d2dc7 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -266,6 +266,9 @@ set(QL_SOURCES instruments/cpicapfloor.cpp instruments/cpiswap.cpp instruments/creditdefaultswap.cpp + instruments/crossccybasisswap.cpp + instruments/crossccyfixfloatswap.cpp + instruments/crossccyswap.cpp instruments/doublebarrieroption.cpp instruments/doublebarriertype.cpp instruments/equitytotalreturnswap.cpp @@ -724,6 +727,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 @@ -1343,6 +1347,9 @@ set(QL_HEADERS instruments/cpicapfloor.hpp instruments/cpiswap.hpp instruments/creditdefaultswap.hpp + instruments/crossccybasisswap.hpp + instruments/crossccyfixfloatswap.hpp + instruments/crossccyswap.hpp instruments/dividendschedule.hpp instruments/doublebarrieroption.hpp instruments/doublebarriertype.hpp @@ -1966,6 +1973,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/cashflows/overnightindexedcoupon.hpp b/ql/cashflows/overnightindexedcoupon.hpp index 31ffb2f30b3..e6bbcf9b59d 100644 --- a/ql/cashflows/overnightindexedcoupon.hpp +++ b/ql/cashflows/overnightindexedcoupon.hpp @@ -227,7 +227,6 @@ namespace QuantLib { OvernightLeg& withLockoutDays(Natural lockoutDays); OvernightLeg& withObservationShift(bool applyObservationShift = true); OvernightLeg& compoundingSpreadDaily(bool compoundSpreadDaily = true); - OvernightLeg& withLookback(const Period& lookback); OvernightLeg& withCaps(Rate cap); OvernightLeg& withCaps(const std::vector& caps); OvernightLeg& withFloors(Rate floor); diff --git a/ql/instruments/Makefile.am b/ql/instruments/Makefile.am index 0bde212b88e..edfc04f3e19 100644 --- a/ql/instruments/Makefile.am +++ b/ql/instruments/Makefile.am @@ -25,6 +25,9 @@ this_include_HEADERS = \ cpicapfloor.hpp \ cpiswap.hpp \ creditdefaultswap.hpp \ + crossccybasisswap.hpp \ + crossccyfixfloatswap.hpp \ + crossccyswap.hpp \ dividendschedule.hpp \ doublebarrieroption.hpp \ doublebarriertype.hpp \ @@ -100,6 +103,9 @@ cpp_files = \ cpicapfloor.cpp \ cpiswap.cpp \ creditdefaultswap.cpp \ + crossccybasisswap.cpp \ + crossccyfixfloatswap.cpp \ + crossccyswap.cpp \ doublebarrieroption.cpp \ doublebarriertype.cpp \ equitytotalreturnswap.cpp \ diff --git a/ql/instruments/all.hpp b/ql/instruments/all.hpp index 61a7954efcd..908c1854737 100644 --- a/ql/instruments/all.hpp +++ b/ql/instruments/all.hpp @@ -20,6 +20,9 @@ #include #include #include +#include +#include +#include #include #include #include diff --git a/ql/instruments/crossccybasisswap.cpp b/ql/instruments/crossccybasisswap.cpp new file mode 100644 index 00000000000..0d9a462363a --- /dev/null +++ b/ql/instruments/crossccybasisswap.cpp @@ -0,0 +1,186 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + 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 +#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 payLookback, ext::optional payLockoutDays, + ext::optional payIsAveraged, ext::optional recIncludeSpread, + ext::optional recLookback, ext::optional recLockoutDays, + ext::optional recIsAveraged, 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), payLookback_(payLookback), payLockoutDays_(payLockoutDays), + payIsAveraged_(payIsAveraged), recIncludeSpread_(recIncludeSpread), recLookback_(recLookback), + recLockoutDays_(recLockoutDays), recIsAveraged_(recIsAveraged), 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(payLookback_ ? *payLookback_ : 0) + .withLockoutDays(payLockoutDays_ ? *payLockoutDays_ : 0) + .withAveragingMethod(payIsAveraged_ ? + (*payIsAveraged_ ? RateAveraging::Simple : RateAveraging::Compound) : RateAveraging::Compound) + .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(recLookback_ ? *recLookback_ : 0) + .withLockoutDays(recLockoutDays_ ? *recLockoutDays_ : 0) + .withAveragingMethod(recIsAveraged_ ? + (*recIsAveraged_ ? RateAveraging::Simple : RateAveraging::Compound) : RateAveraging::Compound) + .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..76e8730d41a --- /dev/null +++ b/ql/instruments/crossccybasisswap.hpp @@ -0,0 +1,182 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + 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. +*/ + +/*! \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 + //@{ + /*! + \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 payLookback Optional lookback days for the pay leg (default: null). + \param payLockoutDays Optional lockout period (in business days) before payment during which rate observations are frozen for the pay leg (defaul: 0). + \param payIsAveraged If true, use arithmetic averaging of overnight rates instead of compounding when building the pay leg (defaul: 0). + \param recIncludeSpread Optional flag to include the spread in the receive leg calculation (default: null). + \param recLookback Optional lookback days for the receive leg (default: null). + \param recLockoutDays Optional lockout period (in business days) before payment during which rate observations are frozen for the rec leg (defaul: 0). + \param recIsAveraged If true, use arithmetic averaging of overnight rates instead of compounding when building the rec leg (defaul: 0). + \param telescopicValueDates Flag indicating whether telescopic value dates are used (default: false). + */ + 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 payLookback = ext::nullopt, + ext::optional payLockoutDays = ext::nullopt, ext::optional payIsAveraged = ext::nullopt, + ext::optional recIncludeSpread = ext::nullopt, ext::optional recLookback = ext::nullopt, + ext::optional recLockoutDays = ext::nullopt, ext::optional recIsAveraged = 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 payLookback_; + ext::optional payLockoutDays_; + ext::optional payIsAveraged_; + ext::optional recIncludeSpread_; + ext::optional recLookback_; + ext::optional recLockoutDays_; + ext::optional recIsAveraged_; + 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/ql/instruments/crossccyfixfloatswap.cpp b/ql/instruments/crossccyfixfloatswap.cpp new file mode 100644 index 00000000000..f239fd11387 --- /dev/null +++ b/ql/instruments/crossccyfixfloatswap.cpp @@ -0,0 +1,181 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + 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 +#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, + const bool telescopicValueDates, ext::optional floatIncludeSpread, ext::optional floatLookbackDays, + ext::optional floatLockoutDays, ext::optional floatIsAveraged) + : 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), + telescopicValueDates_(telescopicValueDates), + floatIncludeSpread_(floatIncludeSpread), floatLookbackDays_(floatLookbackDays), + floatLockoutDays_(floatLockoutDays), floatIsAveraged_(floatIsAveraged) { + + // Build the float leg + Leg floatLeg; + if (auto on = ext::dynamic_pointer_cast(floatIndex_)) { + floatLeg = OvernightLeg(floatSchedule_, on) + .withNotionals(floatNominal_) + .withSpreads(floatSpread_) + .withPaymentAdjustment(floatPaymentBdc_) + .withPaymentLag(floatPaymentLag_) + .withLookbackDays(floatLookbackDays ? *floatLookbackDays : 0) + .withPaymentCalendar(floatPaymentCalendar_) + .withLockoutDays(floatLockoutDays_ ? *floatLockoutDays_ : 0) + .withAveragingMethod(floatIsAveraged_ ? + (*floatIsAveraged_ ? RateAveraging::Simple : RateAveraging::Compound) : RateAveraging::Compound) + .withTelescopicValueDates(telescopicValueDates_); + } else { + 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..65a52c46f58 --- /dev/null +++ b/ql/instruments/crossccyfixfloatswap.hpp @@ -0,0 +1,194 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2018 Quaternion Risk Management Ltd + 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. +*/ + +/*! \file ql/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 + //@{ + /*! + \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. + \param telescopicValueDates Set to true if the floatIndex is an OvernightIndex and you want to use telescopic values when calculating the rate. + \param floatIncludeSpread If true, include the `floatSpread` when computing compounded or averaged overnight rates; if not provided, the index default behaviour is used. + \param floatLookbackDays Optional lookback period (in calendar days) applied to the floating-rate observation window for the floating leg. + \param floatLockoutDays Optional lockout period (in business days) before payment during which rate observations are frozen. + \param floatIsAveraged If true, use arithmetic averaging of overnight rates instead of compounding when building the floating leg. + */ + 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, + const bool telescopicValueDates = false, + ext::optional floatIncludeSpread = ext::nullopt, + ext::optional floatLookbackDays = ext::nullopt, + ext::optional floatLockoutDays = ext::nullopt, + ext::optional floatIsAveraged = ext::nullopt); + //@} + + //! \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_; } + ext::optional floatIncludeSpread() const { return floatIncludeSpread_; } + ext::optional floatLookbackDays() const { return floatLookbackDays_; } + ext::optional floatLockoutDays() const { return floatLockoutDays_; } + ext::optional floatIsAveraged() const { return floatIsAveraged_; } + //@} + + //! \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_; + bool telescopicValueDates_; + ext::optional floatIncludeSpread_; + ext::optional floatLookbackDays_; + ext::optional floatLockoutDays_; + ext::optional floatIsAveraged_; + + 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/ql/instruments/crossccyswap.cpp b/ql/instruments/crossccyswap.cpp new file mode 100644 index 00000000000..bcfca5b4ee7 --- /dev/null +++ b/ql/instruments/crossccyswap.cpp @@ -0,0 +1,108 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + 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 + +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..9cd547710b8 --- /dev/null +++ b/ql/instruments/crossccyswap.hpp @@ -0,0 +1,148 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2016 Quaternion Risk Management Ltd + 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. +*/ + +/*! \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. + */ + explicit 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/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 diff --git a/ql/pricingengines/swap/crossccyswapengine.cpp b/ql/pricingengines/swap/crossccyswapengine.cpp new file mode 100644 index 00000000000..cf6315e4fea --- /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& 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) + : domesticCcy_(domesticCcy), domesticCcyDiscountcurve_(domesticCcyDiscountcurve), foreignCcy_(foreignCcy), + foreignCcyDiscountcurve_(foreignCcyDiscountcurve), spotFX_(spotFX), + includeSettlementDateFlows_(includeSettlementDateFlows), settlementDate_(settlementDate), npvDate_(npvDate), + spotFXSettleDate_(spotFXSettleDate) { + + registerWith(domesticCcyDiscountcurve_); + registerWith(foreignCcyDiscountcurve_); + registerWith(spotFX_); +} + +void CrossCcySwapEngine::calculate() const { + + QL_REQUIRE(!domesticCcyDiscountcurve_.empty() && !foreignCcyDiscountcurve_.empty(), + "Discounting term structure handle is empty."); + + QL_REQUIRE(!spotFX_.empty(), "FX spot quote handle is empty."); + + QL_REQUIRE(domesticCcyDiscountcurve_->referenceDate() == foreignCcyDiscountcurve_->referenceDate(), + "Term structures should have the same reference date."); + Date referenceDate = domesticCcyDiscountcurve_->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] == domesticCcy_) { + legDiscountCurve = domesticCcyDiscountcurve_; + } else { + 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); + + // 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] != domesticCcy_) { + // 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 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 *= domesticCcyDF / foreignCcyDF; + } + 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 >= domesticCcyDiscountcurve_->referenceDate()) { + results_.startDiscounts[legNo] = legDiscountCurve->discount(startDate); + } else { + results_.startDiscounts[legNo] = Null(); + } + + Date maturityDate = CashFlows::maturityDate(arguments_.legs[legNo]); + if (maturityDate >= domesticCcyDiscountcurve_->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..9e7087c10c6 --- /dev/null +++ b/ql/pricingengines/swap/crossccyswapengine.hpp @@ -0,0 +1,108 @@ +/* -*- 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 +#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 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 +*/ +class CrossCcySwapEngine : public CrossCcySwap::engine { +public: + //! \name Constructors + //@{ + /*! \param domesticCcy + Currency 1 + \param domesitcCcyDiscountCurve + Discount curve for cash flows in currency 1 + \param foreignCcy + Currency 2 + \param foreignCcyDiscountCurve + Discount curve for cash flows in currency 2 + \param spotFX + 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 + 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& 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()); + //@} + + //! \name PricingEngine interface + //@{ + void calculate() const override; + //@} + + //! \name Inspectors + //@{ + const Handle& domesitcCcyDiscountCurve() const { return domesticCcyDiscountcurve_; } + const Handle& foreignCcyDiscountCurve() const { return foreignCcyDiscountcurve_; } + + const Currency& currency1() const { return domesticCcy_; } + const Currency& currency2() const { return foreignCcy_; } + + const Handle& spotFX() const { return spotFX_; } + //@} + +private: + Currency domesticCcy_; + Handle domesticCcyDiscountcurve_; + Currency foreignCcy_; + Handle foreignCcyDiscountcurve_; + 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 15bfd8e9cea..27691d667ad 100644 --- a/test-suite/CMakeLists.txt +++ b/test-suite/CMakeLists.txt @@ -40,6 +40,9 @@ set(QL_TEST_SOURCES convertiblebonds.cpp covariance.cpp creditdefaultswap.cpp + crossccybasisswap.cpp + crossccyfixfloatswap.cpp + crossccyswap.cpp crosscurrencyratehelpers.cpp currency.cpp curvestates.cpp diff --git a/test-suite/Makefile.am b/test-suite/Makefile.am index df4651f5cd9..f43c53b9151 100644 --- a/test-suite/Makefile.am +++ b/test-suite/Makefile.am @@ -41,6 +41,9 @@ QL_TEST_SRCS = \ convertiblebonds.cpp \ covariance.cpp \ creditdefaultswap.cpp \ + crossccybasisswap.cpp \ + crossccyfixfloatswap.cpp \ + crossccyswap.cpp \ crosscurrencyratehelpers.cpp \ currency.cpp \ curvestates.cpp \ diff --git a/test-suite/crossccybasisswap.cpp b/test-suite/crossccybasisswap.cpp new file mode 100644 index 00000000000..03f47684454 --- /dev/null +++ b/test-suite/crossccybasisswap.cpp @@ -0,0 +1,422 @@ +/* -*- 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 +#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)); +} + + +ext::shared_ptr makeONBasisXCCY(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 sofrIndex = ext::make_shared(USDProjectionCurve()); + auto soniaIndex = ext::make_shared(GBPProjectionCurve()); + + // Create swap + return ext::shared_ptr(new CrossCcyBasisSwap( + GBPNominal, GBPCurrency(), schedule, soniaIndex, GBPSpread, 1.0, + GBPNominal * spotFx, USDCurrency(), schedule, sofrIndex, 0.0, 1.0, + 0, // payPaymentLag + 0, // recPaymentLaf + false, // payIncludeSpread + 0, // payLookback + 3, // payLockoutDays + false, // payIsAveraged + false, // recIncludeSpread + 0, // recLookback + 2, // recLockoutDays + false, // recIsAveraged + false // telescopicValueDates + )); +} + + +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); + + // 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 = 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_CASE(testBasisONXCCYSwapPricing) { + BOOST_TEST_MESSAGE("Test cross currency Overnight legs basis swap pricing against known results"); + + SavedSettings backup; + Settings::instance().evaluationDate() = Date(11, Sep, 2018); + + // Create swap + Rate spotFx = 1; + Spread spread = 0; + auto xccy = makeONBasisXCCY(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 = 0.26367734931409; + + 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/crossccyfixfloatswap.cpp b/test-suite/crossccyfixfloatswap.cpp new file mode 100644 index 00000000000..003a4fd06eb --- /dev/null +++ b/test-suite/crossccyfixfloatswap.cpp @@ -0,0 +1,301 @@ +/* -*- 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 + +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); + 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 xccy = makeFixFloatXCCYSwap(spotFx, rate, spread); + + // Attach pricing engine + auto fxSpotQuote = makeQuoteHandle(1.0 / spotFx); + auto engine = ext::make_shared( + USDCurrency(), usdDiscountCurve(), TRYCurrency(), tryDiscountCurve(), fxSpotQuote); + xccy->setPricingEngine(engine); + + // Check values + 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() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/crossccyswap.cpp b/test-suite/crossccyswap.cpp new file mode 100644 index 00000000000..8eb81e223ef --- /dev/null +++ b/test-suite/crossccyswap.cpp @@ -0,0 +1,696 @@ +/* -*- 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 +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); + 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()); + + CommonVars vars(payCalendar, Following, DateGeneration::Forward); + + Schedule schedule( + vars.startDate, + vars.endDate, + Period(3, Months), + payCalendar, + vars.convention, + vars.convention, + vars.rule, + vars.endOfMonth + ); + + Rate usdRate = 0.0575; + Rate chfRate = 0.0201; + + // USD Leg + Leg usdLeg = FixedRateLeg(schedule) + .withNotionals(leg1Nominal) + .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); + usdLeg.push_back(finalCapitalFlow); + + // CHF Leg + Leg chfLeg = FixedRateLeg(schedule) + .withNotionals(leg1Nominal * spotFx) + .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); + 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()); + + CommonVars vars(payCalendar, ModifiedFollowing, DateGeneration::Backward); + BusinessDayConvention payConvention = Following; + + Schedule floatSchedule( + vars.startDate, + vars.endDate, + Period(3, Months), + payCalendar, + vars.convention, + vars.convention, + vars.rule, + vars.endOfMonth + ); + + Schedule fixSchedule( + vars.startDate, + vars.endDate, + Period(1, Years), + payCalendar, + vars.convention, + vars.convention, + vars.rule, + vars.endOfMonth + ); + + // TRY Leg + Rate tryRate = 0.249; + Leg tryLeg = FixedRateLeg(fixSchedule) + .withNotionals(leg1Nominal * spotFx) + .withCouponRates(tryRate, vars.dc) + .withPaymentAdjustment(payConvention) + .withPaymentCalendar(payCalendar); + 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); + 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(), vars.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()); + + CommonVars vars(payCalendar, Following, DateGeneration::Forward); + + Schedule schedule( + vars.startDate, + vars.endDate, + Period(3, Months), + payCalendar, + 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(vars.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(vars.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"); + + // 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"); + + bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); + + // 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 = usingAtParCoupons ? 218961.99 : 218981.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 = 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); + 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"); + + bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons(); + + // 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 = 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 = 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); + 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 dfc5d41be5f..11e5282696e 100644 --- a/test-suite/testsuite.vcxproj +++ b/test-suite/testsuite.vcxproj @@ -685,6 +685,9 @@ + + + diff --git a/test-suite/testsuite.vcxproj.filters b/test-suite/testsuite.vcxproj.filters index 8574e483369..9840906e199 100644 --- a/test-suite/testsuite.vcxproj.filters +++ b/test-suite/testsuite.vcxproj.filters @@ -501,6 +501,15 @@ Source Files + + Source Files + + + Source Files + + + Source Files + Source Files