diff --git a/OREAnalytics/orea/app/marketcalibrationreport.cpp b/OREAnalytics/orea/app/marketcalibrationreport.cpp index 70e44d3c2e..2139d3afa8 100644 --- a/OREAnalytics/orea/app/marketcalibrationreport.cpp +++ b/OREAnalytics/orea/app/marketcalibrationreport.cpp @@ -264,7 +264,9 @@ 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 38b734a246..15d6fa1098 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,66 @@ 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()); + DLOG("Building YoY Pillar " << terms[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, yoyInstruments.front()->earliestDate()); + 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()); + 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 +252,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>( @@ -213,7 +268,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>( @@ -360,13 +415,17 @@ 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 { 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..9b724183e2 100644 --- a/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp +++ b/OREData/ored/marketdata/todaysmarketcalibrationinfo.hpp @@ -84,7 +84,9 @@ struct InflationCurveCalibrationInfo { struct ZeroInflationCurveCalibrationInfo : public InflationCurveCalibrationInfo { double baseCpi = 0.0; std::vector zeroRates; + std::vector unSeasonalizedZeroRates; std::vector forwardCpis; + std::vector unSeasonalizedForwardCpis; }; struct YoYInflationCurveCalibrationInfo : public InflationCurveCalibrationInfo { diff --git a/QuantExt/qle/indexes/inflationindexwrapper.cpp b/QuantExt/qle/indexes/inflationindexwrapper.cpp index eab9ef7ba0..b386c6eda8 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_); } @@ -66,4 +66,45 @@ Real YoYInflationIndexWrapper::forecastFixing(const Date& fixingDate) const { return (f1 - f0) / f0; } +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)) { + // 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 (!removeSeasonality) { + // check if date is after baseDate but before any helpers + return pastFixing * (1 + yoyRate); + } + Rate yoyRateDeseasonalized = ts->seasonality()->deseasonalisedYoYRate(fixingDate, yoyRate, *ts.currentLink()); + 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 86e338585a..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, @@ -73,10 +74,13 @@ 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 removeSeasonality) const; + Rate impliedZeroRate(const Date& to, const DayCounter& dc) const; private: Rate forecastFixing(const Date& fixingDate) const; const QuantLib::ext::shared_ptr zeroIndex_; + const Date& firstPillarDate_; }; } // namespace QuantExt diff --git a/QuantLib b/QuantLib index cd55083cce..2f8a8be62c 160000 --- a/QuantLib +++ b/QuantLib @@ -1 +1 @@ -Subproject commit cd55083ccee60c721f570cd4e2b00df0ce657d10 +Subproject commit 2f8a8be62ce96a4ff16cef1ed8711174c3a0e216