diff --git a/OREData/ored/configuration/yieldcurveconfig.cpp b/OREData/ored/configuration/yieldcurveconfig.cpp index 6c82fc11da..4a7fd3bfe5 100644 --- a/OREData/ored/configuration/yieldcurveconfig.cpp +++ b/OREData/ored/configuration/yieldcurveconfig.cpp @@ -1,6 +1,6 @@ /* Copyright (C) 2016 Quaternion Risk Management Ltd - Copyright (C) 2023 Oleg Kulkov + Copyright (C) 2023,2024 Oleg Kulkov All rights reserved. This file is part of ORE, a free-software/open-source library @@ -72,6 +72,8 @@ YieldCurveSegment::Type parseYieldCurveSegment(const string& s) { return YieldCurveSegment::Type::DiscountRatio; else if (iequals(s, "FittedBond")) return YieldCurveSegment::Type::FittedBond; + else if (iequals(s, "Cheapest To Deliver")) + return YieldCurveSegment::Type::CheapestToDeliver; else if (iequals(s, "Yield Plus Default")) return YieldCurveSegment::Type::YieldPlusDefault; else if (iequals(s, "Weighted Average")) @@ -93,6 +95,7 @@ class SegmentIDGetter : public AcyclicVisitor, public Visitor, public Visitor, public Visitor, + public Visitor, public Visitor, public Visitor, public Visitor { @@ -112,6 +115,7 @@ class SegmentIDGetter : public AcyclicVisitor, void visit(WeightedAverageYieldCurveSegment& s) override; void visit(YieldPlusDefaultYieldCurveSegment& s) override; void visit(BondYieldShiftedYieldCurveSegment& s) override; + void visit(CheapestToDeliverCurveSegment& s) override; void visit(IborFallbackCurveSegment& s) override; private: @@ -193,6 +197,15 @@ void SegmentIDGetter::visit(BondYieldShiftedYieldCurveSegment& s) { requiredCurveIds_[CurveSpec::CurveType::Yield].insert(s.referenceCurveID()); } +void SegmentIDGetter::visit(CheapestToDeliverCurveSegment& s) { + for (auto const& c : s.ctdCurves()) { + string cname = c.second; + if (curveID_ != cname && !cname.empty()) { + requiredCurveIds_[CurveSpec::CurveType::Yield].insert(cname); + } + } +} + void SegmentIDGetter::visit(WeightedAverageYieldCurveSegment& s) { string aCurveID1 = s.referenceCurveID1(); string aCurveID2 = s.referenceCurveID2(); @@ -281,6 +294,8 @@ void YieldCurveConfig::fromXML(XMLNode* node) { segment.reset(new BondYieldShiftedYieldCurveSegment()); } else if (childName == "WeightedAverage") { segment.reset(new WeightedAverageYieldCurveSegment()); + } else if (childName == "CheapestToDeliver") { + segment.reset(new CheapestToDeliverCurveSegment()); } else if (childName == "YieldPlusDefault") { segment.reset(new YieldPlusDefaultYieldCurveSegment()); } else if(childName == "IborFallback"){ @@ -399,7 +414,8 @@ void YieldCurveSegment::fromXML(XMLNode* node) { {"WeightedAverage", {"Weighted Average"}}, {"DiscountRatio", {"Discount Ratio"}}, {"IborFallback", {"Ibor Fallback"}}, - {"BondYieldShifted", {"Bond Yield Shifted"}} + {"BondYieldShifted", {"Bond Yield Shifted"}}, + {"CheapestToDeliver", {"Cheapest To Deliver"}} }; std::list validTypes = validSegmentTypes.at(name); @@ -820,6 +836,76 @@ void YieldPlusDefaultYieldCurveSegment::accept(AcyclicVisitor& v) { YieldCurveSegment::accept(v); } +void CheapestToDeliverCurveSegment::fromXML(XMLNode* node) { + XMLUtils::checkNode(node, "CheapestToDeliver"); + YieldCurveSegment::fromXML(node); + vector ctdCurveCcys; + vector ctdCurves = XMLUtils::getChildrenValuesWithAttributes( + node, "CollateralCurves", "CollateralCurve", "currency", ctdCurveCcys, true); + for (Size i = 0; i < ctdCurveCcys.size(); ++i) { + ctdCurves_[ctdCurveCcys[i]] = ctdCurves[i]; + } + + vector ctSpreadsCcy; + vector ctdSpreads = XMLUtils::getChildrenValuesWithAttributes( + node, "CollateralSpreads", "CollateralSpread", "currency", ctSpreadsCcy, true); + + //collect information on the schedule for the final curve grid + XMLNode* pillarGenNode = XMLUtils::getChildNode(node, "PillarsGeneration"); + rule_ = parseBool(XMLUtils::getChildValue(pillarGenNode, "Rule", false, "false")); + + if (rule_) { + + vector grid; + vector tenors = XMLUtils::getChildrenValuesWithAttributes( + pillarGenNode, "Periods", "Period", "maxTenor", grid, true); + for (Size i = 0; i < grid.size(); ++i) { + Period tmpMaxPeriod = parsePeriod(grid[i]); + Period tmpTenor = parsePeriod(tenors[i]); + if (tmpMaxPeriod > tmpTenor) { + periods_.push_back(std::make_pair(tmpMaxPeriod,tmpTenor)); + } else { + ALOG("Period " << tmpMaxPeriod << " must not be longer than maximum tenor " << tmpTenor); + } + } + } else { + vector pillars = XMLUtils::getChildrenValuesAsStrings(pillarGenNode, "Pillars", true); + pillars_ = parseVectorOfValues(pillars, &parsePeriod); + + } + + for (Size i = 0; i < ctdSpreads.size(); ++i) { + ctdSpreads_[ctSpreadsCcy[i]] = parseReal(ctdSpreads[i]); + auto it = ctdCurves_.find(ctSpreadsCcy[i]); + if (it == ctdCurves_.end()) { + QL_FAIL("CTD spread for currency " << ctdCurveCcys[i] << " is not defined in Spreads."); + } + } + + QL_REQUIRE(ctdCurves_.size() == ctdSpreads_.size(), "size of ctd spreads " << ctdSpreads_.size() << " does not correspond to size of ctd curves " << ctdCurves_.size()); + +} + +XMLNode* CheapestToDeliverCurveSegment::toXML(XMLDocument& doc) { + XMLNode* node = YieldCurveSegment::toXML(doc); + XMLUtils::setNodeName(doc, node, "CheapestToDeliver"); + //XMLUtils::addChild(doc, node, "CCBasisIncluded", ccBasisIncluded_); + XMLUtils::addChild(doc, node, "CollateralCurves"); + XMLUtils::addChild(doc, node, "CollateralSpreads"); + XMLUtils::addGenericChildAsList(doc, node, "Pillars", pillars_); + XMLUtils::addChild(doc, node, "Conventions"); + XMLUtils::addChild(doc, node, "PillarsGeneration"); + return node; +} + +void CheapestToDeliverCurveSegment::accept(AcyclicVisitor& v) { + Visitor* v1 = dynamic_cast*>(&v); + if (v1 != 0) + v1->visit(*this); + else + YieldCurveSegment::accept(v); +} + IborFallbackCurveSegment::IborFallbackCurveSegment(const string& typeID, const string& iborIndex, const string& rfrCurve, const boost::optional& rfrIndex, const boost::optional& spread) diff --git a/OREData/ored/configuration/yieldcurveconfig.hpp b/OREData/ored/configuration/yieldcurveconfig.hpp index 6ae3f211b2..f9b7dbe49c 100644 --- a/OREData/ored/configuration/yieldcurveconfig.hpp +++ b/OREData/ored/configuration/yieldcurveconfig.hpp @@ -1,6 +1,6 @@ /* Copyright (C) 2016 Quaternion Risk Management Ltd - Copyright (C) 2023 Oleg Kulkov + Copyright (C) 2023,2024 Oleg Kulkov All rights reserved. This file is part of ORE, a free-software/open-source library @@ -79,7 +79,8 @@ class YieldCurveSegment : public XMLSerializable { WeightedAverage, YieldPlusDefault, IborFallback, - BondYieldShifted + BondYieldShifted, + CheapestToDeliver }; //! Default destructor virtual ~YieldCurveSegment() {} @@ -517,6 +518,56 @@ class DiscountRatioYieldCurveSegment : public YieldCurveSegment { std::string denominatorCurveCurrency_; }; +//! Cheapest To Deliver curve segment +/*! Used to configure a QuantExt::Type::CheapestToDeliver. +\ingroup configuration + */ + class CheapestToDeliverCurveSegment : public YieldCurveSegment { +public: + //! \name Constructors/Destructors + //@{ + //! Default constructor + CheapestToDeliverCurveSegment() {} + //! Detailed constructor + CheapestToDeliverCurveSegment(const std::string& typeId, + const bool ccBasisIncluded, + const map& ctdCurvesId, + const std::vector pillars, + const vector& ctdSpreads); + //@} + + //! \name Serialisation + //@{ + virtual void fromXML(XMLNode* node) override; + virtual XMLNode* toXML(XMLDocument& doc) override; + //@} + + //! \name Inspectors + //@{ + //const bool CCBasisIncluded() const { return ccBasisIncluded_; } + const map& ctdCurves() const { return ctdCurves_; } + const std::vector pillars() { return pillars_; } + const map& ctdSpreads() const { return ctdSpreads_; } + const bool getRule() { return rule_; } + const std::vector> getPeriods() { return periods_; } + + //@} + + //! \name Visitability + //@{ + void accept(QuantLib::AcyclicVisitor& v) override; + //@} + +private: + //bool ccBasisIncluded_; + map ctdCurves_; + std::vector> periods_; + std::vector pillars_; + map ctdSpreads_; + bool rule_; + +}; + //! FittedBond yield curve segment /*! A bond segment is used to build a yield curve from liquid bond quotes. diff --git a/OREData/ored/marketdata/yieldcurve.cpp b/OREData/ored/marketdata/yieldcurve.cpp index 354d9beb00..d6c5f00b3d 100644 --- a/OREData/ored/marketdata/yieldcurve.cpp +++ b/OREData/ored/marketdata/yieldcurve.cpp @@ -1,6 +1,7 @@ /* Copyright (C) 2016 Quaternion Risk Management Ltd Copyright (C) 2021 Skandinaviska Enskilda Banken AB (publ) + Copyright (C) 2024 Oleg Kulkov All rights reserved. This file is part of ORE, a free-software/open-source library @@ -31,10 +32,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -61,6 +64,7 @@ #include #include #include +#include #include #include @@ -341,6 +345,9 @@ YieldCurve::YieldCurve(Date asof, YieldCurveSpec curveSpec, const CurveConfigura } else if (curveSegments_[0]->type() == YieldCurveSegment::Type::BondYieldShifted) { DLOG("Building BondYieldShiftedCurve " << curveSpec_); buildBondYieldShiftedCurve(); + } else if (curveSegments_[0]->type() == YieldCurveSegment::Type::CheapestToDeliver) { + DLOG("Building CheapestToDeliverCurve " << curveSpec_); + buildCheapestToDeliverCurve(); } else { DLOG("Bootstrapping YieldCurve " << curveSpec_); buildBootstrappedCurve(); @@ -1342,6 +1349,162 @@ boost::shared_ptr YieldCurve::getYieldCurve(const string& ccy, const } } +void YieldCurve::buildCheapestToDeliverCurve() { + + QL_REQUIRE(curveSegments_.size() == 1, "Only one ctd curve segment is supported yet."); + QL_REQUIRE(curveSegments_[0]->type() == YieldCurveSegment::Type::CheapestToDeliver, "The curve segment is not of type Cheapest To Deliver."); + + boost::shared_ptr segment = + boost::dynamic_pointer_cast(curveSegments_[0]); + QL_REQUIRE(segment != nullptr, "expected CheapestToDeliverCurveSegment, this is unexpected"); + + const boost::shared_ptr& conventions = InstrumentConventions::instance().conventions(); + + boost::shared_ptr convention = conventions->get(segment->conventionsID()); + QL_REQUIRE(convention, "No conventions found with ID: " << segment->conventionsID()); + QL_REQUIRE(convention->type() == Convention::Type::Zero, "Conventions ID does not give zero rate conventions."); + boost::shared_ptr zeroConvention = boost::dynamic_pointer_cast(convention); + zeroDayCounter_ = zeroConvention->dayCounter(); + Compounding comp = zeroConvention->compounding(); + Frequency freq = zeroConvention->compoundingFrequency(); + + std::vector> ctdCurves; + std::vector> origAltCurves; + std::vector ctdCurvePillars; + std::vector ctdDates; + std::vector> ctdDfs; + //std::vector ctdZrs; + + map ctdCurveNames = segment->ctdCurves(); + map ctdCurveSpreads = segment->ctdSpreads(); + + string ccyCTDs(""); + // Find the underlying curves in the reference curves and make spreaded from these + // using corresponding spreads + for (auto const& crv : ctdCurveNames) { + ccyCTDs.append(" " + crv.first); + auto key = yieldCurveKey(currency_, crv.second, asofDate_); + auto tmpYldCrv = requiredYieldCurves_.find(key); + QL_REQUIRE(tmpYldCrv != requiredYieldCurves_.end(), "could not find the ctd curve '" << crv.second << "'"); + auto sprCTD = ctdCurveSpreads.find(crv.first); + if (sprCTD != ctdCurveSpreads.end()) { + DLOG("constructing ctd curve from " << crv.second << " and spread of " << std::to_string(sprCTD->second) << "."); + //ctdCals.push_back(tmpYldCrv->second->handle()->calendar()); + origAltCurves.push_back(tmpYldCrv->second->handle()); + //TODO: use correct conventions + ctdCurves.push_back(Handle + (boost::make_shared(tmpYldCrv->second->handle(), + Handle(boost::make_shared(sprCTD->second)), + comp, + freq, + tmpYldCrv->second->handle()->dayCounter()))); + } else { + DLOG("spread in " << crv.first << " has not been found. Use zero spread instead."); + ctdCurves.push_back(tmpYldCrv->second->handle()); + } + + } + + + ctdDates.push_back(asofDate_); + // pull dates defined in default calibration and in the schedule if any + if (segment->getRule()) { + + std::vector> periods = segment->getPeriods(); + sort(periods.begin(),periods.end(),[](pair &left, pair &right) { return left.second < right.second; }); + std::vector tmpDates; + QuantLib::Date tmpDate; + for (auto const& p : periods) { + do { + if (tmpDate != Null()) { + ctdDates.push_back(NullCalendar().adjust(tmpDate)); + } + tmpDate = ctdDates.back() + p.second; + } while (tmpDate < asofDate_ + p.first); + ctdDates.push_back(NullCalendar().adjust(asofDate_ + p.first)); + } + } else { + //adding the pillars + for (Period const& p : segment->pillars()) { + //ctdCurvePillars.push_back(p); + ctdDates.push_back(NullCalendar().adjust(asofDate_ + p)); + } + } + + if (ctdDates.size() < 1) { + DLOG("no dates/pillars for the curve have been added from the curve configuration. Use default grid instead."); + } + + // add default calibration pillars + for (Period const& p : YieldCurveCalibrationInfo::defaultPeriods) { + ctdDates.push_back(NullCalendar().adjust(asofDate_ + p)); + } + + //remove duplicates and sort + sort(ctdDates.begin(), ctdDates.end()); + ctdDates.erase(unique(ctdDates.begin(), + ctdDates.end()), + ctdDates.end()); + + //set calibration dates in the calibration info + if (calibrationInfo_ == nullptr) + calibrationInfo_ = boost::make_shared(); + for (auto const& p : ctdDates) + calibrationInfo_->pillarDates.push_back(p); + + //initialize as of discount factor equal to 1.0 + auto q = boost::make_shared(1.0); + ctdDfs.push_back(Handle(q)); + + // calculate discount factors via daily forward rate comparison + for (Size l = 1; l < ctdDates.size(); ++l) { + Date pillarStartDate(ctdDates[l - 1]); + Date pillarEndDate(ctdDates[l]); + QL_REQUIRE(pillarStartDate != pillarEndDate,"equal dates in the tenor sequence. This is unexpected."); + QuantLib::Date runningDate(pillarStartDate); + QuantLib::DiscountFactor minDcf = ctdDfs[l - 1]->value(); + while (runningDate < pillarEndDate) { + //TODO: use correct conventions + QuantLib::InterestRate maxFwd(QL_MIN_REAL, zeroDayCounter_, comp, freq); + for (auto const& c : ctdCurves) { + QuantLib::InterestRate altFwd = + c->forwardRate(runningDate, NullCalendar().advance(runningDate, 1 * Days, Following, false), zeroDayCounter_, comp, freq, extrapolation_); + if (maxFwd < altFwd) { + maxFwd = altFwd; + } + } + //calculate new ctd discount factor using previous discount factor anf current forward rate + minDcf = minDcf * maxFwd.discountFactor(runningDate,NullCalendar().advance(runningDate, 1 * Days, Following, false)); + runningDate = NullCalendar().advance(runningDate, 1 * Days, Following, false); + } + + ctdDfs.push_back(Handle(boost::make_shared(minDcf))); + + } + + DLOG("building " << currency_.code() << " cash flow ctd curve based on currencies: " << ccyCTDs); + + switch (interpolationMethod_) { + + case InterpolationMethod::LogLinear: { + + p_ = boost::make_shared(ctdCurves, ctdDates, ctdDfs, QuantExt::CheapestToDeliverTermStructure::Interpolation::logLinear); + } + break; + + case InterpolationMethod::Linear: { + + p_ = boost::make_shared(ctdCurves, ctdDates, ctdDfs, QuantExt::CheapestToDeliverTermStructure::Interpolation::linearZero); + } + break; + + default: + DLOG("Interpolation method not supported. Using LogLinear instead."); + p_ = boost::make_shared(ctdCurves, ctdDates, ctdDfs, QuantExt::CheapestToDeliverTermStructure::Interpolation::logLinear); + } + +} + void YieldCurve::buildFittedBondCurve() { QL_REQUIRE(curveSegments_.size() == 1, "FittedBond curve must contain exactly one segment."); QL_REQUIRE(curveSegments_[0]->type() == YieldCurveSegment::Type::FittedBond, diff --git a/OREData/ored/marketdata/yieldcurve.hpp b/OREData/ored/marketdata/yieldcurve.hpp index 26501f1627..9b86041fef 100644 --- a/OREData/ored/marketdata/yieldcurve.hpp +++ b/OREData/ored/marketdata/yieldcurve.hpp @@ -1,7 +1,7 @@ /* Copyright (C) 2016 Quaternion Risk Management Ltd Copyright (C) 2021 Skandinaviska Enskilda Banken AB (publ) - Copyright (C) 2023 Oleg Kulkov + Copyright (C) 2023,2024 Oleg Kulkov All rights reserved. This file is part of ORE, a free-software/open-source library @@ -152,6 +152,8 @@ class YieldCurve { void buildIborFallbackCurve(); //! Build a yield curve that uses QuantExt::bondYieldShiftedCurve void buildBondYieldShiftedCurve(); + //! Build a yield curve that uses QuantExt::cheapestToDeliverCurve + void buildCheapestToDeliverCurve(); //! Return the yield curve with the given \p id from the requiredYieldCurves_ map boost::shared_ptr getYieldCurve(const std::string& ccy, const std::string& id) const; diff --git a/QuantExt/qle/CMakeLists.txt b/QuantExt/qle/CMakeLists.txt index 7c46230204..38960336bb 100644 --- a/QuantExt/qle/CMakeLists.txt +++ b/QuantExt/qle/CMakeLists.txt @@ -843,6 +843,7 @@ termstructures/brlcdiratehelper.hpp termstructures/capfloorhelper.hpp termstructures/capfloortermvolcurve.hpp termstructures/capfloortermvolsurface.hpp +termstructures/cheapesttodelivercurve.hpp termstructures/capfloortermvolsurfacesparse.hpp termstructures/commodityaveragebasispricecurve.hpp termstructures/commoditybasispricecurve.hpp diff --git a/QuantExt/qle/termstructures/cheapesttodelivercurve.hpp b/QuantExt/qle/termstructures/cheapesttodelivercurve.hpp new file mode 100644 index 0000000000..f39887daf2 --- /dev/null +++ b/QuantExt/qle/termstructures/cheapesttodelivercurve.hpp @@ -0,0 +1,132 @@ +/* +Copyright (C) 2024 Oleg Kulkov +All rights reserved. +This file is part of ORE, a free-software/open-source library +for transparent pricing and risk analysis - http://opensourcerisk.org +ORE is free software: you can redistribute it and/or modify it +under the terms of the Modified BSD License. You should have received a +copy of the license along with this program. +The license is also available online at +This program is distributed on the basis that it will form a useful +contribution to risk analytics and model standardisation, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +/*! \file qle/termstructures/cheapesttodelivercurve.hpp + \brief cheapest to deliver curve helper + \ingroup termstructures +*/ + +#ifndef AGILIB_CHEAPESTTODELIVERCURVE_HPP +#define AGILIB_CHEAPESTTODELIVERCURVE_HPP + +#include +#include +#include +#include + +#include + +#include + + +namespace QuantExt { +using namespace QuantLib; + +//! cheapest to deliver term structure +/*! this yield term structure is defined by discount factors given by a maximum forward daily rates of n yield term structures + and a spread over the resulting curve + */ +class CheapestToDeliverTermStructure : public YieldTermStructure { +public: + enum class Interpolation { logLinear, linearZero }; + enum class Extrapolation { flatZero, flatFwd }; + + CheapestToDeliverTermStructure(const std::vector>& yts, + const std::vector& dts, + const std::vector>& dfs, + Interpolation interpol = Interpolation::logLinear, + Extrapolation extrapol = Extrapolation::flatZero) + : YieldTermStructure(yts[0]->dayCounter()), altYts_(yts), dts_(dts), + interpolation_(interpol), extrapolation_(extrapol){ + //register alternative collateral curves + for (const Handle& yts_i : altYts_) { + if (!yts_i.empty()) { + registerWith(yts_i); + } + } + //initialize log quotes for interpolation + for (auto const & df: dfs) { + dfs_.push_back(boost::make_shared(df)); + } + //initialize time grid for interpolation + boost::optional lastTime = boost::none; + for (auto const & date: dts_) { + QuantLib::Time time = timeFromReference(date); + times_.push_back(time); + if (lastTime) + timeDiffs_.push_back(time - *lastTime); + lastTime = time; + } + } + + Date maxDate() const override { return dts_.back(); } + const Date& referenceDate() const override; + + //! \name Visitability + //@{ + void accept(AcyclicVisitor&) ; + //@} + +protected: + Real discountImpl(Time t) const override; + const std::vector> altYts_; + +private: + std::vector dts_; + std::vector> dfs_; + std::vector