From 40dd57856a57028053239b80b356a04e122afcc7 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:36:47 +0200 Subject: [PATCH 1/4] Add `applySeasonality` parameter to `zeroRate` Add the possibility to not account for the seasonality factor when calling `zeroRate` on an inflation term structure, in analytics --- ql/termstructures/inflationtermstructure.cpp | 5 +++-- ql/termstructures/inflationtermstructure.hpp | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ql/termstructures/inflationtermstructure.cpp b/ql/termstructures/inflationtermstructure.cpp index 649738cd51d..194f3fd0654 100644 --- a/ql/termstructures/inflationtermstructure.cpp +++ b/ql/termstructures/inflationtermstructure.cpp @@ -228,7 +228,8 @@ namespace QuantLib { Rate ZeroInflationTermStructure::zeroRate(const Date &d, const Period& instObsLag, bool forceLinearInterpolation, - bool extrapolate) const { + bool extrapolate, + bool applySeasonality) const { Period useLag = instObsLag; if (instObsLag == Period(-1,Days)) { @@ -256,7 +257,7 @@ namespace QuantLib { zeroRate = zeroRateImpl(t); } - if (hasSeasonality()) { + if (hasSeasonality() && applySeasonality) { zeroRate = seasonality()->correctZeroRate(d-useLag, zeroRate, *this); } return zeroRate; diff --git a/ql/termstructures/inflationtermstructure.hpp b/ql/termstructures/inflationtermstructure.hpp index 3f91712f465..42de8978a2d 100644 --- a/ql/termstructures/inflationtermstructure.hpp +++ b/ql/termstructures/inflationtermstructure.hpp @@ -242,7 +242,8 @@ namespace QuantLib { */ Rate zeroRate(const Date& d, const Period& instObsLag = Period(-1,Days), bool forceLinearInterpolation = false, - bool extrapolate = false) const; + bool extrapolate = false, + bool applySeasonality = true) const; //! zero-coupon inflation rate. /*! \warning Since inflation is highly linked to dates (lags, interpolation, months for seasonality, etc) this From 55967e649a54a2c6ce6ba19963bb82bd409a0191 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:39:26 +0200 Subject: [PATCH 2/4] Add `applySeasonality` as a parameter --- ql/indexes/inflationindex.cpp | 12 ++++++++---- ql/indexes/inflationindex.hpp | 7 +++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ql/indexes/inflationindex.cpp b/ql/indexes/inflationindex.cpp index 0c69c32be2a..123adc1194c 100644 --- a/ql/indexes/inflationindex.cpp +++ b/ql/indexes/inflationindex.cpp @@ -201,6 +201,11 @@ namespace QuantLib { Real ZeroInflationIndex::fixing(const Date& fixingDate, bool /*forecastTodaysFixing*/) const { + return fixing(fixingDate, false, true); + } + + Real ZeroInflationIndex::fixing(const Date& fixingDate, + bool /*forecastTodaysFixing*/, bool applySeasonality) const { if (!needsForecast(fixingDate)) { const Real I1 = pastFixing(fixingDate); QL_REQUIRE(I1 != Null(), @@ -209,7 +214,7 @@ namespace QuantLib { return I1; } else { - return forecastFixing(fixingDate); + return forecastFixing(fixingDate, applySeasonality); } } @@ -254,8 +259,7 @@ namespace QuantLib { } } - - Real ZeroInflationIndex::forecastFixing(const Date& fixingDate) const { + Real ZeroInflationIndex::forecastFixing(const Date& fixingDate, bool applySeasonality) const { // the term structure is relative to the fixing value at the base date. Date baseDate = zeroInflation_->baseDate(); QL_REQUIRE(!needsForecast(baseDate), @@ -265,7 +269,7 @@ namespace QuantLib { std::pair fixingPeriod = inflationPeriod(fixingDate, frequency_); Date firstDateInPeriod = fixingPeriod.first; - Rate Z1 = zeroInflation_->zeroRate(firstDateInPeriod, Period(0,Days), false); + Rate Z1 = zeroInflation_->zeroRate(firstDateInPeriod, Period(0,Days), false, false, applySeasonality); Time t1 = inflationYearFraction(frequency_, false, zeroInflation_->dayCounter(), baseDate, firstDateInPeriod); return baseFixing * std::pow(1.0 + Z1, t1); diff --git a/ql/indexes/inflationindex.hpp b/ql/indexes/inflationindex.hpp index 148ce6b01fc..c6dd549bc9f 100644 --- a/ql/indexes/inflationindex.hpp +++ b/ql/indexes/inflationindex.hpp @@ -167,7 +167,10 @@ namespace QuantLib { /*! \warning the forecastTodaysFixing parameter (required by the Index interface) is currently ignored. */ - Real fixing(const Date& fixingDate, bool forecastTodaysFixing = false) const override; + Real fixing(const Date& fixingDate, bool forecastTodaysFixing = false) const override; // required + Real fixing(const Date& fixingDate, + bool forecastTodaysFixing, + bool applySeasonality) const; // overload Real pastFixing(const Date& fixingDate) const override; //@} //! \name Other methods @@ -178,7 +181,7 @@ namespace QuantLib { bool needsForecast(const Date& fixingDate) const; //@} private: - Real forecastFixing(const Date& fixingDate) const; + Real forecastFixing(const Date& fixingDate, bool applySeasonality = true) const; Handle zeroInflation_; }; From 52ab22856ae8dac12b9a739dea85764c2c63a641 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:44:16 +0200 Subject: [PATCH 3/4] Calculate factor before from base daate --- ql/termstructures/inflation/seasonality.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ql/termstructures/inflation/seasonality.cpp b/ql/termstructures/inflation/seasonality.cpp index d9865907a2e..ffaec30c1c7 100644 --- a/ql/termstructures/inflation/seasonality.cpp +++ b/ql/termstructures/inflation/seasonality.cpp @@ -248,8 +248,8 @@ namespace QuantLib { Time timeFromCurveBase = dc.yearFraction(curveBaseDate, p.first); f = std::pow(seasonalityAt, 1 / timeFromCurveBase); } else { - Rate factor1Ybefore = this->seasonalityFactor(atDate - Period(1, Years)); - f = factorAt / factor1Ybefore; + Rate factorBefore = this->seasonalityFactor(curveBaseDate); + f = factorAt / factorBefore; } return (rate + 1) / f - 1; From 2f8a8be62ce96a4ff16cef1ed8711174c3a0e216 Mon Sep 17 00:00:00 2001 From: s2505g <119352036+oszette@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:45:20 +0200 Subject: [PATCH 4/4] Year DC can return 0 We still want to compound it once eventhough Year returns 0 --- ql/instruments/zerocouponinflationswap.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ql/instruments/zerocouponinflationswap.cpp b/ql/instruments/zerocouponinflationswap.cpp index 31bebd2b7f3..3c6ed60cdd8 100644 --- a/ql/instruments/zerocouponinflationswap.cpp +++ b/ql/instruments/zerocouponinflationswap.cpp @@ -93,6 +93,8 @@ namespace QuantLib { inflationYearFraction(infIndex_->frequency(), detail::CPI::isInterpolated(observationInterpolation_), dayCounter_, baseDate_, obsDate_); + // Mimic yearly compounding 1/1 DC with Year (which can return zero). + T = T == 0 ? 1 : T; // N.B. the -1.0 is because swaps only exchange growth, not notionals as well Real fixedAmount = nominal * (std::pow(1.0 + fixedRate, T) - 1.0); @@ -134,7 +136,8 @@ namespace QuantLib { inflationYearFraction(infIndex_->frequency(), detail::CPI::isInterpolated(observationInterpolation_), dayCounter_, baseDate_, obsDate_); - + // Year DC can return zero which is not wanted + T = T == 0 ? 1 : T; return std::pow(growth,1.0/T) - 1.0; // we cannot use this simple definition because