From 7000a3e21192c6d31b1c6902778024cf7d016e8e Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:56:16 +0200 Subject: [PATCH 01/15] The boolean argument does not have an effect --- OREData/ored/marketdata/inflationcurve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OREData/ored/marketdata/inflationcurve.cpp b/OREData/ored/marketdata/inflationcurve.cpp index 38b734a246..f906c0c70d 100644 --- a/OREData/ored/marketdata/inflationcurve.cpp +++ b/OREData/ored/marketdata/inflationcurve.cpp @@ -213,7 +213,7 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& - auto baseFixing = index->fixing(baseDate, true); + auto baseFixing = index->fixing(baseDate); if (config->interpolationMethod().empty() || config->interpolationMethod() == "Linear"){ curve_ = QuantLib::ext::make_shared>( From 1ff6b4085e3b62f97934f5c38adfaa33beeef4cc Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:56:21 +0200 Subject: [PATCH 02/15] Update QuantLib --- QuantLib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuantLib b/QuantLib index cd55083cce..55967e649a 160000 --- a/QuantLib +++ b/QuantLib @@ -1 +1 @@ -Subproject commit cd55083ccee60c721f570cd4e2b00df0ce657d10 +Subproject commit 55967e649a54a2c6ce6ba19963bb82bd409a0191 From a3dcd21efb7fb0793e425b9b8b943d735fe3e2e7 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:57:37 +0200 Subject: [PATCH 03/15] Add unseasonalized CPI values to cal info --- OREAnalytics/orea/app/marketcalibrationreport.cpp | 1 + OREData/ored/marketdata/inflationcurve.cpp | 3 +++ OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp | 1 + 3 files changed, 5 insertions(+) diff --git a/OREAnalytics/orea/app/marketcalibrationreport.cpp b/OREAnalytics/orea/app/marketcalibrationreport.cpp index 7014477b5a..54f3494168 100644 --- a/OREAnalytics/orea/app/marketcalibrationreport.cpp +++ b/OREAnalytics/orea/app/marketcalibrationreport.cpp @@ -261,6 +261,7 @@ void MarketCalibrationReport::addInflationCurveImpl( addRowReport(type, id, "time", key1, "", "", z->times.at(i)); addRowReport(type, id, "zeroRate", key1, "", "", z->zeroRates.at(i)); addRowReport(type, id, "cpi", key1, "", "", z->forwardCpis.at(i)); + addRowReport(type, id, "unSeasonalizedCpi", key1, "", "", z->unSeasonalizedForwardCpis.at(i)); } } diff --git a/OREData/ored/marketdata/inflationcurve.cpp b/OREData/ored/marketdata/inflationcurve.cpp index f906c0c70d..a72ed44259 100644 --- a/OREData/ored/marketdata/inflationcurve.cpp +++ b/OREData/ored/marketdata/inflationcurve.cpp @@ -362,11 +362,14 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& calInfo->zeroRates.push_back(zcCurve->zeroRate(pillarDates[i], 0 * Days)); calInfo->times.push_back(zcCurve->timeFromReference(pillarDates[i])); Real cpi = 0.0; + Real unSeasonalizedCpi = 0.0; try { cpi = zcIndex->fixing(pillarDates[i]); + unSeasonalizedCpi = zcIndex->fixing(pillarDates[i], false, false); } catch (...) { } calInfo->forwardCpis.push_back(cpi); + calInfo->unSeasonalizedForwardCpis.push_back(unSeasonalizedCpi); } calibrationInfo_ = calInfo; } diff --git a/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp b/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp index dfb2d5a883..7933832a59 100644 --- a/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp +++ b/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp @@ -85,6 +85,7 @@ struct ZeroInflationCurveCalibrationInfo : public InflationCurveCalibrationInfo double baseCpi = 0.0; std::vector zeroRates; std::vector forwardCpis; + std::vector unSeasonalizedForwardCpis; }; struct YoYInflationCurveCalibrationInfo : public InflationCurveCalibrationInfo { From 160fe018642ecdd303ec44c65453917b2683dbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Gerdin=20B=C3=B6rjesson?= Date: Tue, 5 Aug 2025 14:12:04 +0200 Subject: [PATCH 04/15] Workaround broken fixing loading --- OREAnalytics/orea/app/marketdataloader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OREAnalytics/orea/app/marketdataloader.cpp b/OREAnalytics/orea/app/marketdataloader.cpp index aca1caf393..bef8814730 100644 --- a/OREAnalytics/orea/app/marketdataloader.cpp +++ b/OREAnalytics/orea/app/marketdataloader.cpp @@ -188,7 +188,7 @@ void MarketDataLoader::populateFixings( amendInflationFixingDates(fixings_); } - if (fixings_.size() > 0 && impl_) + if (impl_) impl()->retrieveFixings(loader_, fixings_, lastAvailableFixingLookupMap); applyFixings(loader_->loadFixings()); From db427c338fa12ddf3f64392fae660e65fd079c0a Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:32:04 +0200 Subject: [PATCH 05/15] Update QuantLib --- QuantLib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuantLib b/QuantLib index 55967e649a..2f8a8be62c 160000 --- a/QuantLib +++ b/QuantLib @@ -1 +1 @@ -Subproject commit 55967e649a54a2c6ce6ba19963bb82bd409a0191 +Subproject commit 2f8a8be62ce96a4ff16cef1ed8711174c3a0e216 From 61ab91f4225c9b7005b3e904d015591086bbd0f3 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:53:28 +0200 Subject: [PATCH 06/15] Add unSeasonalizedZeroRate to CalInfo --- OREAnalytics/orea/app/marketcalibrationreport.cpp | 1 + OREData/ored/marketdata/inflationcurve.cpp | 1 + OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp | 1 + 3 files changed, 3 insertions(+) diff --git a/OREAnalytics/orea/app/marketcalibrationreport.cpp b/OREAnalytics/orea/app/marketcalibrationreport.cpp index 54f3494168..9a55432b16 100644 --- a/OREAnalytics/orea/app/marketcalibrationreport.cpp +++ b/OREAnalytics/orea/app/marketcalibrationreport.cpp @@ -260,6 +260,7 @@ void MarketCalibrationReport::addInflationCurveImpl( std::string key1 = ore::data::to_string(z->pillarDates[i]); addRowReport(type, id, "time", key1, "", "", z->times.at(i)); addRowReport(type, id, "zeroRate", key1, "", "", z->zeroRates.at(i)); + addRowReport(type, id, "unSeasonalizedZeroRate", key1, "", "", z->unSeasonalizedZeroRates.at(i)); addRowReport(type, id, "cpi", key1, "", "", z->forwardCpis.at(i)); addRowReport(type, id, "unSeasonalizedCpi", key1, "", "", z->unSeasonalizedForwardCpis.at(i)); } diff --git a/OREData/ored/marketdata/inflationcurve.cpp b/OREData/ored/marketdata/inflationcurve.cpp index a72ed44259..e04fdff5bf 100644 --- a/OREData/ored/marketdata/inflationcurve.cpp +++ b/OREData/ored/marketdata/inflationcurve.cpp @@ -361,6 +361,7 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& calInfo->pillarDates.push_back(pillarDates[i]); calInfo->zeroRates.push_back(zcCurve->zeroRate(pillarDates[i], 0 * Days)); calInfo->times.push_back(zcCurve->timeFromReference(pillarDates[i])); + calInfo->unSeasonalizedZeroRates.push_back(zcCurve->zeroRate(pillarDates[i], 0 * Days, false, false, false)); Real cpi = 0.0; Real unSeasonalizedCpi = 0.0; try { diff --git a/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp b/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp index 7933832a59..9b724183e2 100644 --- a/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp +++ b/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp @@ -84,6 +84,7 @@ struct InflationCurveCalibrationInfo { struct ZeroInflationCurveCalibrationInfo : public InflationCurveCalibrationInfo { double baseCpi = 0.0; std::vector zeroRates; + std::vector unSeasonalizedZeroRates; std::vector forwardCpis; std::vector unSeasonalizedForwardCpis; }; From 23f4f6dd124a2e0b22c5b6c1c4c970051b9b68dd Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:53:59 +0200 Subject: [PATCH 07/15] Calculate time from curve basedate --- OREData/ored/marketdata/inflationcurve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OREData/ored/marketdata/inflationcurve.cpp b/OREData/ored/marketdata/inflationcurve.cpp index e04fdff5bf..71e5acb71a 100644 --- a/OREData/ored/marketdata/inflationcurve.cpp +++ b/OREData/ored/marketdata/inflationcurve.cpp @@ -360,8 +360,8 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& for (Size i = 0; i < pillarDates.size(); ++i) { calInfo->pillarDates.push_back(pillarDates[i]); calInfo->zeroRates.push_back(zcCurve->zeroRate(pillarDates[i], 0 * Days)); - calInfo->times.push_back(zcCurve->timeFromReference(pillarDates[i])); calInfo->unSeasonalizedZeroRates.push_back(zcCurve->zeroRate(pillarDates[i], 0 * Days, false, false, false)); + calInfo->times.push_back(inflationYearFraction(curve_->frequency(), false, curve_->dayCounter(), curve_->baseDate(), pillarDates[i])); Real cpi = 0.0; Real unSeasonalizedCpi = 0.0; try { From 5c47f789ce648d440d19f8242ec2db38dd51bc3f Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:55:06 +0200 Subject: [PATCH 08/15] Convert YoY to ZC Need to work on the code a bit more --- OREData/ored/marketdata/inflationcurve.cpp | 73 +++++++++++++++++++--- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/OREData/ored/marketdata/inflationcurve.cpp b/OREData/ored/marketdata/inflationcurve.cpp index 71e5acb71a..867e4d4f13 100644 --- a/OREData/ored/marketdata/inflationcurve.cpp +++ b/OREData/ored/marketdata/inflationcurve.cpp @@ -90,8 +90,7 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'"); if ((md->instrumentType() == MarketDatum::InstrumentType::ZC_INFLATIONSWAP || - (md->instrumentType() == MarketDatum::InstrumentType::YY_INFLATIONSWAP && - config->type() == InflationCurveConfig::Type::YY))) { + (md->instrumentType() == MarketDatum::InstrumentType::YY_INFLATIONSWAP))) { QuantLib::ext::shared_ptr q = QuantLib::ext::dynamic_pointer_cast(md); if (q) { @@ -117,12 +116,15 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& } } - // do we have all quotes and do we derive yoy quotes from zc ? + // do we have all quotes and do we derive yoy quotes from zc or zc from yoy? for (Size i = 0; i < strQuotes.size(); ++i) { QL_REQUIRE(!quotes[i].empty(), "quote " << strQuotes[i] << " not found in market data."); - QL_REQUIRE(isZc[i] == isZc[0], "mixed zc and yoy quotes"); } bool derive_yoy_from_zc = (config->type() == InflationCurveConfig::Type::YY && isZc[0]); + bool derive_zc_from_yoy = (config->type() == InflationCurveConfig::Type::ZC && !isZc[0]); + if (derive_zc_from_yoy) { + WLOG(config->curveID() << ": Curve type is ZC but YY benchmarks has been passed in, will convert YY benchmarks to ZC benchmarks."); + } // construct seasonality QuantLib::ext::shared_ptr seasonality; @@ -179,12 +181,68 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& interpolatedIndex_ = conv->interpolated(); CPI::InterpolationType observationInterpolation = interpolatedIndex_ ? CPI::Linear : CPI::Flat; QuantLib::ext::shared_ptr zc_to_yoy_conversion_index; - if (config->type() == InflationCurveConfig::Type::ZC || derive_yoy_from_zc) { + if (config->type() == InflationCurveConfig::Type::ZC || derive_yoy_from_zc || derive_zc_from_yoy) { // ZC Curve std::vector> instruments; QuantLib::ext::shared_ptr index = conv->index(); + QuantLib::Date baseDate = QuantExt::ZeroInflation::curveBaseDate( + config->useLastAvailableFixingAsBaseDate(), asof, curveObsLag, config->frequency(), index); + + // Check if YoY quotes need conversion to ZC + if (derive_zc_from_yoy) { + std::vector> yoyInstruments; + std::vector yoyPillarDates; + for (Size i = 0; i < strQuotes.size(); i++) { + if (isZc[i]) { + continue; + } + Date maturity = swapStart + terms[i]; + QuantLib::ext::shared_ptr yoyIndex = + QuantLib::ext::make_shared(index); + + Date yoyStart = conv->fixCalendar().advance(maturity, -1 * Years, conv->fixConvention()); + Date yoyMaturity = conv->fixCalendar().advance(yoyStart, 1 * Years, conv->fixConvention()); + QuantLib::ext::shared_ptr tmp = + QuantLib::ext::make_shared( + quotes[i], conv->observationLag(), yoyMaturity, conv->fixCalendar(), conv->fixConvention(), + conv->dayCounter(), yoyIndex, observationInterpolation, nominalTs, yoyStart); + tmp->unregisterWith(Settings::instance().evaluationDate()); + yoyInstruments.push_back(tmp); + yoyPillarDates.push_back(tmp->pillarDate()); + WLOG("YoY Pillar " << i << " - Start: " << yoyStart << ", Maturity: " << yoyMaturity + << ", YearOnYearRate: " << quotes[i]->value()); + } + Real baseRate = yoyInstruments.front()->quote()->value(); + curve_ = QuantLib::ext::make_shared>( + asof, baseDate, baseRate, config->lag(), config->frequency(), config->dayCounter(), yoyInstruments, + seasonality); + auto yoyCurve = QuantLib::ext::dynamic_pointer_cast(curve_); + Handle yoyHandle(yoyCurve); + auto yoyIndex = QuantLib::ext::make_shared(index, yoyHandle); + for (Size i = 0; i < yoyPillarDates.size(); i++) { + WLOG("Date: " << yoyPillarDates[i] + << ", forwardCpi: " << yoyIndex->forwardCpi(yoyPillarDates[i], true) << " (" + << yoyIndex->forwardCpi(yoyPillarDates[i], false) << ")"); + } + for (Size i = 0; i < yoyInstruments.size(); i++) { + Date maturity = swapStart + terms[i]; + Rate converted_yoy_to_zc = yoyIndex->impliedZeroRate(yoyPillarDates[i], conv->dayCounter()); + WLOG(maturity << " -- " << converted_yoy_to_zc); + QuantLib::ext::shared_ptr instrument = + QuantLib::ext::make_shared( + Handle(QuantLib::ext::make_shared(converted_yoy_to_zc)), + conv->observationLag(), maturity, conv->fixCalendar(), conv->fixConvention(), + conv->dayCounter(), index, observationInterpolation, nominalTs, swapStart); + instrument->unregisterWith(Settings::instance().evaluationDate()); + instruments.push_back(instrument); + } + } + for (Size i = 0; i < strQuotes.size(); ++i) { // QL conventions do not incorporate settlement delay => patch here once QL is patched + if (!isZc[i]) { + continue; + } Date maturity = swapStart + terms[i]; QuantLib::ext::shared_ptr instrument = QuantLib::ext::make_shared( @@ -196,9 +254,8 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& instrument->unregisterWith(Settings::instance().evaluationDate()); instruments.push_back(instrument); } - QuantLib::Date baseDate = QuantExt::ZeroInflation::curveBaseDate( - config->useLastAvailableFixingAsBaseDate(), asof, curveObsLag, config->frequency(), index); - + + if (config->interpolationVariable() == InflationCurveConfig::InterpolationVariable::ZeroRate) { curve_ = QuantLib::ext::make_shared>( From c9373e4002d9f0d7e16ceb735426ca1ca6969a2b Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:55:54 +0200 Subject: [PATCH 09/15] Implement `YoYInflationIndexWrapper::forwardCpi` --- .../qle/indexes/inflationindexwrapper.cpp | 21 +++++++++++++++++++ .../qle/indexes/inflationindexwrapper.hpp | 1 + 2 files changed, 22 insertions(+) diff --git a/QuantExt/qle/indexes/inflationindexwrapper.cpp b/QuantExt/qle/indexes/inflationindexwrapper.cpp index eab9ef7ba0..29007a846f 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.cpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.cpp @@ -66,4 +66,25 @@ Real YoYInflationIndexWrapper::forecastFixing(const Date& fixingDate) const { return (f1 - f0) / f0; } +Real YoYInflationIndexWrapper::forwardCpi(const Date& fixingDate, bool adjustForSeasonality) const { + Date fixingDateMinusOneYear = fixingDate - 1 * Years; + Real pastFixing; + if (needsForecast(fixingDateMinusOneYear)) { + // recursive call if the fixingDateMinusOneYear is a point on the curve + // do not adjust for seasonality when retrieving fixings from the curve + pastFixing = forwardCpi(fixingDateMinusOneYear, false); + } + else { + pastFixing = CPI::laggedFixing(zeroIndex_, fixingDateMinusOneYear, 0 * Days, CPI::Flat); + } + + Rate yoyRate = fixing(fixingDate); + if (!adjustForSeasonality) { + return pastFixing * (1 + yoyRate); + } + Handle ts = yoyInflationTermStructure(); + Rate yoyRateDeseasonalized = ts->seasonality()->deseasonalisedYoYRate(fixingDate, yoyRate, *ts.currentLink()); + return pastFixing * (1 + yoyRateDeseasonalized); +} + } // namespace QuantExt diff --git a/QuantExt/qle/indexes/inflationindexwrapper.hpp b/QuantExt/qle/indexes/inflationindexwrapper.hpp index 86e338585a..156adebb87 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.hpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.hpp @@ -73,6 +73,7 @@ class YoYInflationIndexWrapper : public YoYInflationIndex { /*! \warning the forecastTodaysFixing parameter (required by the Index interface) is currently ignored. */ Rate fixing(const Date& fixingDate, bool forecastTodaysFixing = false) const override; const QuantLib::ext::shared_ptr zeroIndex() const { return zeroIndex_; } + Real forwardCpi(const Date& fixingDate, bool adjustForSeasonality) const; private: Rate forecastFixing(const Date& fixingDate) const; From 9cbf490eb53505b676ef4cf305882037ed7fe004 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:56:19 +0200 Subject: [PATCH 10/15] Implement `YoYInflationIndexWrapper::impliedZeroRate` --- QuantExt/qle/indexes/inflationindexwrapper.cpp | 9 +++++++++ QuantExt/qle/indexes/inflationindexwrapper.hpp | 1 + 2 files changed, 10 insertions(+) diff --git a/QuantExt/qle/indexes/inflationindexwrapper.cpp b/QuantExt/qle/indexes/inflationindexwrapper.cpp index 29007a846f..5383b4227a 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.cpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.cpp @@ -87,4 +87,13 @@ Real YoYInflationIndexWrapper::forwardCpi(const Date& fixingDate, bool adjustFor return pastFixing * (1 + yoyRateDeseasonalized); } +Rate YoYInflationIndexWrapper::impliedZeroRate(const Date& to, const DayCounter& dc) const { + Date baseDate = yoyInflationTermStructure()->baseDate(); + Real baseFixing = CPI::laggedFixing(zeroIndex_, baseDate, 0 * Days, CPI::Flat); + Real toCpi = forwardCpi(to, false); + Time t = dc.yearFraction(baseDate, to); + t = t == 0 ? 1 : t; + return std::pow(toCpi / baseFixing, 1.0 / t) - 1; +} + } // namespace QuantExt diff --git a/QuantExt/qle/indexes/inflationindexwrapper.hpp b/QuantExt/qle/indexes/inflationindexwrapper.hpp index 156adebb87..cb3be93a39 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.hpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.hpp @@ -74,6 +74,7 @@ class YoYInflationIndexWrapper : public YoYInflationIndex { Rate fixing(const Date& fixingDate, bool forecastTodaysFixing = false) const override; const QuantLib::ext::shared_ptr zeroIndex() const { return zeroIndex_; } Real forwardCpi(const Date& fixingDate, bool adjustForSeasonality) const; + Rate impliedZeroRate(const Date& to, const DayCounter& dc) const; private: Rate forecastFixing(const Date& fixingDate) const; From b5d8d6f02fde80e9efd557786bb830266f781593 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:15:16 +0200 Subject: [PATCH 11/15] Rename function parameter for clarity --- QuantExt/qle/indexes/inflationindexwrapper.cpp | 5 +++-- QuantExt/qle/indexes/inflationindexwrapper.hpp | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/QuantExt/qle/indexes/inflationindexwrapper.cpp b/QuantExt/qle/indexes/inflationindexwrapper.cpp index 5383b4227a..dac29928a3 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.cpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.cpp @@ -66,7 +66,7 @@ Real YoYInflationIndexWrapper::forecastFixing(const Date& fixingDate) const { return (f1 - f0) / f0; } -Real YoYInflationIndexWrapper::forwardCpi(const Date& fixingDate, bool adjustForSeasonality) const { +Real YoYInflationIndexWrapper::forwardCpi(const Date& fixingDate, bool removeSeasonality) const { Date fixingDateMinusOneYear = fixingDate - 1 * Years; Real pastFixing; if (needsForecast(fixingDateMinusOneYear)) { @@ -79,7 +79,8 @@ Real YoYInflationIndexWrapper::forwardCpi(const Date& fixingDate, bool adjustFor } Rate yoyRate = fixing(fixingDate); - if (!adjustForSeasonality) { + if (!removeSeasonality) { + // check if date is after baseDate but before any helpers return pastFixing * (1 + yoyRate); } Handle ts = yoyInflationTermStructure(); diff --git a/QuantExt/qle/indexes/inflationindexwrapper.hpp b/QuantExt/qle/indexes/inflationindexwrapper.hpp index cb3be93a39..84f3ba635b 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.hpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.hpp @@ -73,7 +73,7 @@ class YoYInflationIndexWrapper : public YoYInflationIndex { /*! \warning the forecastTodaysFixing parameter (required by the Index interface) is currently ignored. */ Rate fixing(const Date& fixingDate, bool forecastTodaysFixing = false) const override; const QuantLib::ext::shared_ptr zeroIndex() const { return zeroIndex_; } - Real forwardCpi(const Date& fixingDate, bool adjustForSeasonality) const; + Real forwardCpi(const Date& fixingDate, bool removeSeasonality) const; Rate impliedZeroRate(const Date& to, const DayCounter& dc) const; private: From 6edbd099a45a4a317dec917e338f1f2cf71f4976 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:34:59 +0200 Subject: [PATCH 12/15] Add `firstPillarDate_` as an attribute --- OREData/ored/marketdata/inflationcurve.cpp | 2 +- QuantExt/qle/indexes/inflationindexwrapper.cpp | 6 +++--- QuantExt/qle/indexes/inflationindexwrapper.hpp | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/OREData/ored/marketdata/inflationcurve.cpp b/OREData/ored/marketdata/inflationcurve.cpp index 867e4d4f13..d3eb8217ad 100644 --- a/OREData/ored/marketdata/inflationcurve.cpp +++ b/OREData/ored/marketdata/inflationcurve.cpp @@ -218,7 +218,7 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& seasonality); auto yoyCurve = QuantLib::ext::dynamic_pointer_cast(curve_); Handle yoyHandle(yoyCurve); - auto yoyIndex = QuantLib::ext::make_shared(index, yoyHandle); + auto yoyIndex = QuantLib::ext::make_shared(index, yoyHandle, yoyInstruments.front()->earliestDate()); for (Size i = 0; i < yoyPillarDates.size(); i++) { WLOG("Date: " << yoyPillarDates[i] << ", forwardCpi: " << yoyIndex->forwardCpi(yoyPillarDates[i], true) << " (" diff --git a/QuantExt/qle/indexes/inflationindexwrapper.cpp b/QuantExt/qle/indexes/inflationindexwrapper.cpp index dac29928a3..bfdf71b150 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.cpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.cpp @@ -34,15 +34,15 @@ Rate ZeroInflationIndexWrapper::fixing(const Date& fixingDate, bool /*forecastTo } YoYInflationIndexWrapper::YoYInflationIndexWrapper(const QuantLib::ext::shared_ptr zeroIndex, - const Handle& ts) - : YoYInflationIndex(zeroIndex, ts), zeroIndex_(zeroIndex) { + const Handle& ts, const Date& firstPillarDate) + : YoYInflationIndex(zeroIndex, ts), zeroIndex_(zeroIndex), firstPillarDate_(firstPillarDate) { registerWith(zeroIndex_); } YoYInflationIndexWrapper::YoYInflationIndexWrapper(const QuantLib::ext::shared_ptr zeroIndex, const bool interpolated, const Handle& ts) : YoYInflationIndex(zeroIndex, interpolated, ts), - zeroIndex_(zeroIndex) { + zeroIndex_(zeroIndex), firstPillarDate_(Date()) { registerWith(zeroIndex_); } diff --git a/QuantExt/qle/indexes/inflationindexwrapper.hpp b/QuantExt/qle/indexes/inflationindexwrapper.hpp index 84f3ba635b..fcb251e679 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.hpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.hpp @@ -64,7 +64,8 @@ class ZeroInflationIndexWrapper : public ZeroInflationIndex { class YoYInflationIndexWrapper : public YoYInflationIndex { public: YoYInflationIndexWrapper(const QuantLib::ext::shared_ptr zeroIndex, - const Handle& ts = Handle()); + const Handle& ts = Handle(), + const Date& firstPillarDate = Date()); [[deprecated]] YoYInflationIndexWrapper(const QuantLib::ext::shared_ptr zeroIndex, const bool interpolated, @@ -79,6 +80,7 @@ class YoYInflationIndexWrapper : public YoYInflationIndex { private: Rate forecastFixing(const Date& fixingDate) const; const QuantLib::ext::shared_ptr zeroIndex_; + const Date& firstPillarDate_; }; } // namespace QuantExt From 46d30fb1c52da27b6a2e9bf3e38590470c114e5e Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:37:02 +0200 Subject: [PATCH 13/15] Mimic zero rate short end extrapolation --- QuantExt/qle/indexes/inflationindexwrapper.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/QuantExt/qle/indexes/inflationindexwrapper.cpp b/QuantExt/qle/indexes/inflationindexwrapper.cpp index bfdf71b150..b386c6eda8 100644 --- a/QuantExt/qle/indexes/inflationindexwrapper.cpp +++ b/QuantExt/qle/indexes/inflationindexwrapper.cpp @@ -67,6 +67,17 @@ Real YoYInflationIndexWrapper::forecastFixing(const Date& fixingDate) const { } Real YoYInflationIndexWrapper::forwardCpi(const Date& fixingDate, bool removeSeasonality) const { + Handle ts = yoyInflationTermStructure(); + if (ts->baseDate() < fixingDate && firstPillarDate_ > fixingDate) { + //need to mimic flat extrapolation in zero rate here + Real firstPillarCpi = forwardCpi(firstPillarDate_, true); + Real baseCpi = CPI::laggedFixing(zeroIndex_, ts->baseDate(), 0 * Days, CPI::Flat); + Time timeBaseToFirstPillar = ts->dayCounter().yearFraction(ts->baseDate(), firstPillarDate_); + Rate zr = std::pow(firstPillarCpi / baseCpi, 1.0 / timeBaseToFirstPillar) - 1; + Rate zrDeseasonalized = ts->seasonality()->correctZeroRate(fixingDate, zr, *ts.currentLink()); + Time timeBaseToFixingDate = ts->dayCounter().yearFraction(ts->baseDate(), fixingDate); + return baseCpi * std::pow(1 + zrDeseasonalized, timeBaseToFixingDate); + } Date fixingDateMinusOneYear = fixingDate - 1 * Years; Real pastFixing; if (needsForecast(fixingDateMinusOneYear)) { @@ -83,7 +94,6 @@ Real YoYInflationIndexWrapper::forwardCpi(const Date& fixingDate, bool removeSea // check if date is after baseDate but before any helpers return pastFixing * (1 + yoyRate); } - Handle ts = yoyInflationTermStructure(); Rate yoyRateDeseasonalized = ts->seasonality()->deseasonalisedYoYRate(fixingDate, yoyRate, *ts.currentLink()); return pastFixing * (1 + yoyRateDeseasonalized); } From a1ca75abbc3d0a6533a4f73274f1c5c50c56b573 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:37:15 +0200 Subject: [PATCH 14/15] Be more specific in log --- OREData/ored/marketdata/inflationcurve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OREData/ored/marketdata/inflationcurve.cpp b/OREData/ored/marketdata/inflationcurve.cpp index d3eb8217ad..9430b16820 100644 --- a/OREData/ored/marketdata/inflationcurve.cpp +++ b/OREData/ored/marketdata/inflationcurve.cpp @@ -221,7 +221,7 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& auto yoyIndex = QuantLib::ext::make_shared(index, yoyHandle, yoyInstruments.front()->earliestDate()); for (Size i = 0; i < yoyPillarDates.size(); i++) { WLOG("Date: " << yoyPillarDates[i] - << ", forwardCpi: " << yoyIndex->forwardCpi(yoyPillarDates[i], true) << " (" + << ", forwardCpi: " << yoyIndex->forwardCpi(yoyPillarDates[i], true) << " (Seasonalized " << yoyIndex->forwardCpi(yoyPillarDates[i], false) << ")"); } for (Size i = 0; i < yoyInstruments.size(); i++) { From 961d99b3343c8e96e074181dc9a623f0a2446f47 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:33:21 +0200 Subject: [PATCH 15/15] Update log messages a bit --- OREData/ored/marketdata/inflationcurve.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/OREData/ored/marketdata/inflationcurve.cpp b/OREData/ored/marketdata/inflationcurve.cpp index 9430b16820..15d6fa1098 100644 --- a/OREData/ored/marketdata/inflationcurve.cpp +++ b/OREData/ored/marketdata/inflationcurve.cpp @@ -209,7 +209,7 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& tmp->unregisterWith(Settings::instance().evaluationDate()); yoyInstruments.push_back(tmp); yoyPillarDates.push_back(tmp->pillarDate()); - WLOG("YoY Pillar " << i << " - Start: " << yoyStart << ", Maturity: " << yoyMaturity + DLOG("Building YoY Pillar " << terms[i] << " - Start: " << yoyStart << ", Maturity: " << yoyMaturity << ", YearOnYearRate: " << quotes[i]->value()); } Real baseRate = yoyInstruments.front()->quote()->value(); @@ -219,15 +219,13 @@ InflationCurve::InflationCurve(Date asof, InflationCurveSpec spec, const Loader& auto yoyCurve = QuantLib::ext::dynamic_pointer_cast(curve_); Handle yoyHandle(yoyCurve); auto yoyIndex = QuantLib::ext::make_shared(index, yoyHandle, yoyInstruments.front()->earliestDate()); - for (Size i = 0; i < yoyPillarDates.size(); i++) { - WLOG("Date: " << yoyPillarDates[i] - << ", forwardCpi: " << yoyIndex->forwardCpi(yoyPillarDates[i], true) << " (Seasonalized " - << yoyIndex->forwardCpi(yoyPillarDates[i], false) << ")"); - } for (Size i = 0; i < yoyInstruments.size(); i++) { + DLOG("YoY Pillar " << terms[i] << " (CPI: " << yoyIndex->forwardCpi(yoyPillarDates[i], true) + << ", Seasonalized: " << yoyIndex->forwardCpi(yoyPillarDates[i], false) + << ") --> Converted to ZC benchmark using implied zero rate " + << yoyIndex->impliedZeroRate(yoyPillarDates[i], conv->dayCounter())); Date maturity = swapStart + terms[i]; Rate converted_yoy_to_zc = yoyIndex->impliedZeroRate(yoyPillarDates[i], conv->dayCounter()); - WLOG(maturity << " -- " << converted_yoy_to_zc); QuantLib::ext::shared_ptr instrument = QuantLib::ext::make_shared( Handle(QuantLib::ext::make_shared(converted_yoy_to_zc)),