From 06b53f375c284446a1139f90ac24672247005ed9 Mon Sep 17 00:00:00 2001 From: Jaskeerat Singh Saluja Date: Sat, 22 Aug 2026 21:58:02 +0530 Subject: [PATCH] resolved #356 --- OREData/ored/marketdata/yieldcurve.cpp | 25 ++++++--- OREData/test/todaysmarket.cpp | 74 +++++++++++++++++++++----- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/OREData/ored/marketdata/yieldcurve.cpp b/OREData/ored/marketdata/yieldcurve.cpp index 8302b92994..4bb9ee7884 100644 --- a/OREData/ored/marketdata/yieldcurve.cpp +++ b/OREData/ored/marketdata/yieldcurve.cpp @@ -1306,15 +1306,11 @@ void YieldCurve::buildZeroSpreadedCurve(const std::size_t index) { QuantLib::ext::shared_ptr conventions = InstrumentConventions::instance().conventions(); - // Fill a vector of zero spread quotes. - vector> quotes; QuantLib::ext::shared_ptr segment = QuantLib::ext::dynamic_pointer_cast(curveSegments_[index][0]); auto quoteIDs = segment->quotes(); Date today = Settings::instance().evaluationDate(); - vector dates; - vector> quoteHandles; vector quotesVector; quotesVector.reserve(quoteIDs.size()); @@ -1331,6 +1327,9 @@ void YieldCurve::buildZeroSpreadedCurve(const std::size_t index) { } } + vector>> nodes; + nodes.reserve(marketData.size()); + // process market data for (const auto& marketQuote : marketData) { QL_REQUIRE(marketQuote->instrumentType() == MarketDatum::InstrumentType::ZERO, @@ -1345,14 +1344,24 @@ void YieldCurve::buildZeroSpreadedCurve(const std::size_t index) { if (it == quoteIDs.end()) continue; } - quotes.push_back(zeroQuote); - dates.push_back(zeroQuote->tenorBased() ? today + zeroQuote->tenor() : zeroQuote->date()); - quoteHandles.push_back(zeroQuote->quote()); + nodes.emplace_back(zeroQuote->tenorBased() ? today + zeroQuote->tenor() : zeroQuote->date(), + zeroQuote->quote()); } - QL_REQUIRE(!quotes.empty(), + QL_REQUIRE(!nodes.empty(), "Cannot build curve with spec " << curveSpec_[index]->name() << " because there are no spread quotes"); + std::sort(nodes.begin(), nodes.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + vector dates; + vector> quoteHandles; + dates.reserve(nodes.size()); + quoteHandles.reserve(nodes.size()); + for (auto& [date, quote] : nodes) { + dates.emplace_back(date); + quoteHandles.emplace_back(std::move(quote)); + } + string referenceCurveID = segment->referenceCurveID(); QuantLib::Handle referenceCurve; if (referenceCurveID != curveConfig_[index]->curveID() && !referenceCurveID.empty()) { diff --git a/OREData/test/todaysmarket.cpp b/OREData/test/todaysmarket.cpp index e21723305e..0c6f967d91 100644 --- a/OREData/test/todaysmarket.cpp +++ b/OREData/test/todaysmarket.cpp @@ -50,8 +50,9 @@ namespace { class MarketDataLoader : public Loader { public: - MarketDataLoader(); + explicit MarketDataLoader(bool cloneZeroSpreadQuotes = false); std::vector> loadQuotes(const QuantLib::Date&) const override; + QuantLib::ext::shared_ptr get(const std::string& name, const QuantLib::Date& d) const override; std::set asofDates() const override { return {}; } std::set loadFixings() const override { return fixings_; } std::set loadDividends() const override { return dividends_; } @@ -63,6 +64,7 @@ class MarketDataLoader : public Loader { std::map>> data_; std::set fixings_; std::set dividends_; + bool cloneZeroSpreadQuotes_; }; vector> MarketDataLoader::loadQuotes(const Date& d) const { @@ -71,7 +73,15 @@ vector> MarketDataLoader::loadQuotes(cons return it->second; } -MarketDataLoader::MarketDataLoader() { +QuantLib::ext::shared_ptr MarketDataLoader::get(const std::string& name, const Date& d) const { + auto marketDatum = Loader::get(name, d); + // Clone Zero quotes so allocation follows the deliberately non-chronological lookup order. + if (cloneZeroSpreadQuotes_ && marketDatum->instrumentType() == MarketDatum::InstrumentType::ZERO) + return marketDatum->clone(); + return marketDatum; +} + +MarketDataLoader::MarketDataLoader(bool cloneZeroSpreadQuotes) : cloneZeroSpreadQuotes_(cloneZeroSpreadQuotes) { // clang-format off vector data = boost::assign::list_of // borrow spread curve @@ -80,10 +90,10 @@ MarketDataLoader::MarketDataLoader() { ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_BORROW/A365/10Y -0.0010") ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_BORROW/A365/20Y -0.0010") // lending spread curve - ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/2Y 0.0050") + ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/2Y 0.0020") ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/5Y 0.0050") - ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/10Y 0.0050") - ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/20Y 0.0050") + ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/10Y 0.0100") + ("20160226 ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/20Y 0.0200") // Eonia curve ("20160226 MM/RATE/EUR/0D/1D -0.0025") ("20160226 IR_SWAP/RATE/EUR/0D/1D/1D -0.0025") @@ -567,10 +577,10 @@ QuantLib::ext::shared_ptr curveConfigurations() { // Lending curve segments.clear(); // clang-format off - quotes = {"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/2Y", - "ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/5Y", - "ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/10Y", - "ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/20Y"}; + quotes = {"ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/5Y", + "ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/20Y", + "ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/2Y", + "ZERO/YIELD_SPREAD/EUR/BANK_EUR_LEND/A365/10Y"}; // clang-format on segments.push_back(QuantLib::ext::make_shared( "Zero Spread", "EUR-ZERO-CONVENTIONS-TENOR-BASED", quotes, "EUR1D")); @@ -777,11 +787,11 @@ class F : public TopLevelFixture { public: QuantLib::ext::shared_ptr market; - F() { + explicit F(bool cloneZeroSpreadQuotes = false) { Date asof(26, February, 2016); Settings::instance().evaluationDate() = asof; - auto loader = QuantLib::ext::make_shared(); + auto loader = QuantLib::ext::make_shared(cloneZeroSpreadQuotes); auto params = marketParameters(); auto configs = curveConfigurations(); auto convs = conventions(); @@ -798,6 +808,29 @@ class F : public TopLevelFixture { } }; +class FWithClonedZeroSpreadQuotes : public F { +public: + FWithClonedZeroSpreadQuotes() : F(true) {} +}; + +const vector> lendingCurveSpreads = { + {2 * Years, 0.002}, {5 * Years, 0.005}, {10 * Years, 0.010}, {20 * Years, 0.020}}; + +void checkLendingCurveSpreads(const Handle& referenceCurve, + const Handle& spreadedCurve) { + const Date today = Settings::instance().evaluationDate(); + const DayCounter dc = Actual365Fixed(); + constexpr Real tolerance = 1.0e-5; + + for (const auto& [tenor, expectedSpread] : lendingCurveSpreads) { + const Date d = today + tenor; + const Real referenceZero = referenceCurve->zeroRate(d, dc, Continuous); + const Real spreadedZero = spreadedCurve->zeroRate(d, dc, Continuous); + BOOST_CHECK_MESSAGE(fabs(spreadedZero - referenceZero - expectedSpread) < tolerance, + "zero spread at " << io::iso_date(d) << " is not paired with its resolved maturity"); + } +} + } // namespace BOOST_FIXTURE_TEST_SUITE(OREDataTestSuite, TopLevelFixture) @@ -819,16 +852,15 @@ BOOST_AUTO_TEST_CASE(testZeroSpreadedYieldCurve) { Date today = Settings::instance().evaluationDate(); DayCounter dc = Actual365Fixed(); Real tolerance = 1.0e-5; // 0.1 bp - Real expected1 = 0.005; Real expected2 = -0.001; for (Size i = 1; i <= 120; i++) { Date d = today + i * Months; Real z0 = dts->zeroRate(d, dc, Continuous); - Real z1 = dtsLend->zeroRate(d, dc, Continuous); Real z2 = dtsBorrow->zeroRate(d, dc, Continuous); - BOOST_CHECK_MESSAGE(fabs(z1 - z0 - expected1) < tolerance, "error in lending spread curve setup"); BOOST_CHECK_MESSAGE(fabs(z2 - z0 - expected2) < tolerance, "error in borrowing spread curve setup"); } + + checkLendingCurveSpreads(dts, dtsLend); } BOOST_AUTO_TEST_CASE(testNormalOptionletVolatility) { @@ -1041,4 +1073,18 @@ BOOST_AUTO_TEST_CASE(testCorrelationCurve) { BOOST_AUTO_TEST_SUITE_END() +BOOST_FIXTURE_TEST_SUITE(ZeroSpreadRegressionTests, FWithClonedZeroSpreadQuotes) + +BOOST_AUTO_TEST_CASE(testZeroSpreadedYieldCurveOrdersNodesByResolvedMaturity) { + Handle referenceCurve = market->discountCurve("EUR"); + Handle spreadedCurve = market->yieldCurve("EUR_LEND"); + + QL_REQUIRE(!referenceCurve.empty(), "EUR discount curve not found"); + QL_REQUIRE(!spreadedCurve.empty(), "EUR lending curve not found"); + + checkLendingCurveSpreads(referenceCurve, spreadedCurve); +} + +BOOST_AUTO_TEST_SUITE_END() + BOOST_AUTO_TEST_SUITE_END()