Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 67 additions & 31 deletions OREData/ored/marketdata/inflationcurve.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
Copyright (C) 2016 Quaternion Risk Management Ltd
Copyright (C) 2026 Skandinaviska Enskilda Banken AB (publ)
All rights reserved.

This file is part of ORE, a free-software/open-source library
Expand Down Expand Up @@ -31,8 +32,10 @@
#include <ql/termstructures/inflation/piecewiseyoyinflationcurve.hpp>
#include <ql/termstructures/inflation/piecewisezeroinflationcurve.hpp>
#include <ql/time/daycounters/actual365fixed.hpp>
#include <qle/termstructures/inflation/mixedinflationhelpers.hpp>
#include <qle/termstructures/inflation/piecewisecpiinflationcurve.hpp>
#include <qle/utilities/inflation.hpp>
#include <ql/cashflows/fixedratecoupon.hpp>

using namespace QuantLib;
using namespace std;
Expand Down Expand Up @@ -265,7 +268,7 @@ InflationCurve::CurveBuildResults
const Handle<YieldTermStructure>& nominalTs,
const QuantLib::ext::shared_ptr<Seasonality>& seasonality) const {
CurveBuildResults results;
std::vector<QuantLib::ext::shared_ptr<QuantExt::ZeroInflationTraits::helper>> helpers;
std::vector<QuantLib::ext::shared_ptr<QuantLib::ZeroInflationTraits::helper>> helpers;

// Shall we allow different indices in the segments?
// For now we require all segments to use the same index.
Expand Down Expand Up @@ -293,36 +296,69 @@ InflationCurve::CurveBuildResults
<< " not found in market data for date " << asof);
QL_REQUIRE(md->asofDate() == asof,
"MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
QL_REQUIRE(md->instrumentType() == MarketDatum::InstrumentType::ZC_INFLATIONSWAP,
QL_REQUIRE(md->instrumentType() == MarketDatum::InstrumentType::ZC_INFLATIONSWAP ||
md->instrumentType() == MarketDatum::InstrumentType::YY_INFLATIONSWAP,
"MarketDatum " << md << " is not a valid inflation swap quote");
auto zcq = QuantLib::ext::dynamic_pointer_cast<ZcInflationSwapQuote>(md);
QL_REQUIRE(zcq, "Could not cast to ZcInflationSwapQuote, internal error.");
CPI::InterpolationType observationInterpolation = convention->interpolated() ? CPI::Linear : CPI::Flat;
Date maturity = swapStart + zcq->term();
results.latestMaturity =
results.latestMaturity == Date() ? maturity : std::max(results.latestMaturity, maturity);
DLOG("Zero inflation swap " << zcq->name() << " maturity " << maturity << " term " << zcq->term()
<< " quote " << zcq->quote()->value());
auto instrument = QuantLib::ext::make_shared<ZeroCouponInflationSwapHelper>(
zcq->quote(), convention->observationLag(), swapStart, maturity, convention->fixCalendar(),
convention->fixConvention(), convention->dayCounter(), index, observationInterpolation);

// Unregister with inflation index. See PR #326 on github for details.
instrument->unregisterWithAll();
instrument->registerWith(zcq->quote());

helpers.push_back(instrument);
results.pillarDates.push_back(instrument->pillarDate());
results.mdQuoteLabels.push_back(md->name());
results.mdQuoteValues.push_back(md->quote()->value());
results.rateHelperTypes.push_back("ZeroCouponInflation");
results.cashflowGenerators.push_back(
std::function<std::vector<TradeCashflowReportData>()>([instrument, index, asof, nominalTs]() {
return getCashflowReportData(
{instrument->swap()->leg(0), instrument->swap()->leg(1)}, {false, true}, {1.0, 1.0},
index->currency().code(), {index->currency().code(), index->currency().code()}, asof,
{*nominalTs, *nominalTs}, {1.0, 1.0}, {}, {}, {"Interest", ""}, {1.0E6, 1.0E6});
}));

if (md->instrumentType() == MarketDatum::InstrumentType::ZC_INFLATIONSWAP) {
auto zcq = QuantLib::ext::dynamic_pointer_cast<ZcInflationSwapQuote>(md);
QL_REQUIRE(zcq, "Could not cast to ZcInflationSwapQuote, internal error.");
CPI::InterpolationType observationInterpolation = convention->interpolated() ? CPI::Linear : CPI::Flat;
Date maturity = swapStart + zcq->term();
results.latestMaturity =
results.latestMaturity == Date() ? maturity : std::max(results.latestMaturity, maturity);
DLOG("Zero inflation swap " << zcq->name() << " maturity " << maturity << " term " << zcq->term()
<< " quote " << zcq->quote()->value());
auto instrument = QuantLib::ext::make_shared<ZeroCouponInflationSwapHelper>(
zcq->quote(), convention->observationLag(), swapStart, maturity, convention->fixCalendar(),
convention->fixConvention(), convention->dayCounter(), index, observationInterpolation);

// Unregister with inflation index. See PR #326 on github for details.
instrument->unregisterWithAll();
instrument->registerWith(zcq->quote());

helpers.push_back(instrument);
results.pillarDates.push_back(instrument->pillarDate());
results.mdQuoteLabels.push_back(md->name());
results.mdQuoteValues.push_back(md->quote()->value());
results.rateHelperTypes.push_back("ZeroCouponInflation");
results.cashflowGenerators.push_back(
std::function<std::vector<TradeCashflowReportData>()>([instrument, index, asof, nominalTs]() {
return getCashflowReportData(
{instrument->swap()->leg(0), instrument->swap()->leg(1)}, {false, true}, {1.0, 1.0},
index->currency().code(), {index->currency().code(), index->currency().code()}, asof,
{*nominalTs, *nominalTs}, {1.0, 1.0}, {}, {}, {"Interest", ""}, {1.0E6, 1.0E6});
}));
} else {
auto yyq = QuantLib::ext::dynamic_pointer_cast<YoYInflationSwapQuote>(md);
QL_REQUIRE(yyq, "Could not cast to YoYInflationSwapQuote, internal error.");
Date maturity = swapStart + yyq->term();
results.latestMaturity =
results.latestMaturity == Date() ? maturity : std::max(results.latestMaturity, maturity);
auto instrument = QuantLib::ext::make_shared<QuantExt::MixedYearOnYearInflationSwapHelper>(
yyq->quote(), convention->observationLag(), maturity, convention->fixCalendar(),
convention->fixConvention(), convention->dayCounter(), index,
convention->interpolated() ? CPI::Linear : CPI::Flat);
DLOG("YoY inflation swap " << yyq->name() << " maturity " << maturity << " term " << yyq->term()
<< " quote " << yyq->quote()->value());

// Unregister with inflation index (and evaluationDate). See PR #326 on github for details.
instrument->unregisterWithAll();
instrument->registerWith(yyq->quote());

helpers.push_back(instrument);
results.pillarDates.push_back(instrument->pillarDate());
results.mdQuoteLabels.push_back(md->name());
results.mdQuoteValues.push_back(md->quote()->value());
results.rateHelperTypes.push_back("MixedYearOnYearInflation");
results.cashflowGenerators.push_back(
std::function<std::vector<TradeCashflowReportData>()>([instrument, index, asof, nominalTs]() {
return getCashflowReportData({instrument->swap()->leg(0), instrument->swap()->leg(1)},
{false, true}, {1.0, 1.0}, index->currency().code(),
{index->currency().code(), index->currency().code()}, asof,
{*nominalTs, *nominalTs}, {1.0, 1.0}, {}, {});
}));
}
}
}
auto curveObsLag = obsLagFromSegment != 0 * Days ? obsLagFromSegment : config->lag();
Expand Down Expand Up @@ -362,7 +398,7 @@ InflationCurve::CurveBuildResults
const QuantLib::ext::shared_ptr<Seasonality>& seasonality, bool deriveFromZC,
const QuantLib::ext::shared_ptr<InflationTermStructure>& zcCurve) const {
CurveBuildResults results;
std::vector<QuantLib::ext::shared_ptr<QuantExt::YoYInflationTraits::helper>> helpers;
std::vector<QuantLib::ext::shared_ptr<QuantLib::YoYInflationTraits::helper>> helpers;

// Shall we allow different indices in the segments?
// For now we require all segments to use the same index.
Expand Down
2 changes: 2 additions & 0 deletions QuantExt/qle/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ termstructures/iborfallbackcurve.cpp
termstructures/immfraratehelper.cpp
termstructures/inflation/constantcpivolatility.cpp
termstructures/inflation/cpicurve.cpp
termstructures/inflation/mixedinflationhelpers.cpp
termstructures/inflation/cpivolatilitystructure.cpp
termstructures/interpolateddiscountcurve2.cpp
termstructures/oiscapfloorhelper.cpp
Expand Down Expand Up @@ -994,6 +995,7 @@ termstructures/inflation/cpipricevolatilitysurface.hpp
termstructures/inflation/cpivolatilitystructure.hpp
termstructures/inflation/inflationtraits.hpp
termstructures/inflation/interpolatedcpiinflationcurve.hpp
termstructures/inflation/mixedinflationhelpers.hpp
termstructures/inflation/piecewisecpiinflationcurve.hpp
termstructures/interpolatedcorrelationcurve.hpp
termstructures/interpolatedcpivolatilitysurface.hpp
Expand Down
1 change: 1 addition & 0 deletions QuantExt/qle/quantext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@
#include <qle/termstructures/inflation/cpivolatilitystructure.hpp>
#include <qle/termstructures/inflation/inflationtraits.hpp>
#include <qle/termstructures/inflation/interpolatedcpiinflationcurve.hpp>
#include <qle/termstructures/inflation/mixedinflationhelpers.hpp>
#include <qle/termstructures/inflation/piecewisecpiinflationcurve.hpp>
#include <qle/termstructures/interpolatedcorrelationcurve.hpp>
#include <qle/termstructures/interpolatedcpivolatilitysurface.hpp>
Expand Down
118 changes: 118 additions & 0 deletions QuantExt/qle/termstructures/inflation/mixedinflationhelpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
Copyright (C) 2026 Skandinaviska Enskilda Banken AB (publ)
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 <http://opensourcerisk.org>

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.
*/

#include <qle/indexes/inflationindexwrapper.hpp>
#include <qle/termstructures/inflation/mixedinflationhelpers.hpp>

#include <ql/indexes/inflationindex.hpp>
#include <ql/pricingengines/swap/discountingswapengine.hpp>
#include <ql/utilities/null_deleter.hpp>
#include <ql/termstructures/yield/flatforward.hpp>
#include <ql/termstructures/inflationtermstructure.hpp>
#include <ql/termstructures/bootstraphelper.hpp>
#include <utility>
#include <boost/smart_ptr/make_shared_object.hpp>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <ql/errors.hpp>
#include <ql/handle.hpp>
#include <ql/instruments/swap.hpp>
#include <ql/instruments/yearonyearinflationswap.hpp>
#include <ql/quote.hpp>
#include <ql/termstructures/yieldtermstructure.hpp>
#include <ql/time/businessdayconvention.hpp>
#include <ql/time/calendar.hpp>
#include <ql/time/calendars/nullcalendar.hpp>
#include <ql/time/date.hpp>
#include <ql/time/dategenerationrule.hpp>
#include <ql/time/daycounter.hpp>
#include <ql/time/daycounters/actual365fixed.hpp>
#include <ql/time/period.hpp>
#include <ql/time/schedule.hpp>
#include <ql/time/timeunit.hpp>
#include <ql/types.hpp>

namespace QuantExt {

using namespace QuantLib;

MixedYearOnYearInflationSwapHelper::MixedYearOnYearInflationSwapHelper(
const Handle<Quote>& quote, const Period& swapObsLag, const Date& maturity, Calendar calendar,
BusinessDayConvention paymentConvention, DayCounter dayCounter, const ext::shared_ptr<ZeroInflationIndex>& zii,
CPI::InterpolationType interpolation)
: RelativeDateBootstrapHelper<ZeroInflationTermStructure>(quote, false), swapObsLag_(swapObsLag),
maturity_(maturity), calendar_(std::move(calendar)), paymentConvention_(paymentConvention),
dayCounter_(std::move(dayCounter)), interpolation_(interpolation),
nominalTermStructure_(std::move( // any nominal term structure will give the same result;
// when calculating the fair rate, the equal discount factors
// for the payments on the two legs will cancel out.
Handle<YieldTermStructure>(ext::make_shared<FlatForward>(0, NullCalendar(), 0.0, Actual365Fixed())))) {

auto ziiClone = zii->clone(termStructureHandle_);
ziiClone->unregisterWith(termStructureHandle_);

//! Cannot use wrapper in YoYInflationIndex because the forecastFixing doesn't use the underlyingIndex, whereas
//! the YoYInflationIndexWrapper correctly calculates the past (f0) and future cpi (f1) values through the logic
//! in ZeroInflationIndex.
yii_ = ext::make_shared<YoYInflationIndexWrapper>(ziiClone);

if (detail::CPI::isInterpolated(interpolation_, yii_)) {
Period pShift(yii_->frequency());
QL_REQUIRE(swapObsLag_ - pShift >= zii->availabilityLag(),
"inconsistency between swap observation lag "
<< swapObsLag_ << ", index period " << pShift << " and index availability "
<< zii->availabilityLag() << ": need (obsLag-index period) >= availLag");
}

registerWith(yii_);
registerWith(nominalTermStructure_);
MixedYearOnYearInflationSwapHelper::initializeDates();
}

Real MixedYearOnYearInflationSwapHelper::impliedQuote() const {
yyiis_->deepUpdate();
return yyiis_->fairRate();
}

void MixedYearOnYearInflationSwapHelper::initializeDates() {
//! The start date of the swap is calculated by advance back one year from maturity date.
//! The latest date is then calculated by advancing one year from start.
Date startDate = calendar_.advance(maturity_, Period(-1, Years), paymentConvention_);
Date endDate = calendar_.advance(startDate, Period(1, Years), paymentConvention_);
Schedule schedule =
Schedule({startDate, endDate}); // the schedule is simple enough and they are the same for both legs

yyiis_ = ext::make_shared<YearOnYearInflationSwap>(
Swap::Payer, 1000000.0, schedule, quote().empty() || !quote()->isValid() ? 0.0 : quote()->value(), dayCounter_,
schedule, yii_, swapObsLag_, interpolation_, 0.0, dayCounter_, calendar_, paymentConvention_);
//! The swap schedule defines cashflow timing and payment dates. For bootstrapping, the dates need to take the
//! observation lag into account to be inline with the logic of ZeroCouponInflationSwapHelper.
auto fixingPeriod = inflationPeriod(maturity_ - swapObsLag_, yii_->frequency());
earliestDate_ = latestDate_ = fixingPeriod.first;
yyiis_->setPricingEngine(ext::make_shared<DiscountingSwapEngine>(nominalTermStructure_));
}

void MixedYearOnYearInflationSwapHelper::setTermStructure(ZeroInflationTermStructure* z) {
bool observer = false;

ext::shared_ptr<ZeroInflationTermStructure> temp(z, null_deleter());
termStructureHandle_.linkTo(std::move(temp), observer);

RelativeDateBootstrapHelper<ZeroInflationTermStructure>::setTermStructure(z);
}

} // namespace QuantExt
61 changes: 61 additions & 0 deletions QuantExt/qle/termstructures/inflation/mixedinflationhelpers.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
Copyright (C) 2026 Skandinaviska Enskilda Banken AB (publ)
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 <http://opensourcerisk.org>

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.
*/

#pragma once

#include <ql/instruments/yearonyearinflationswap.hpp>
#include <ql/termstructures/bootstraphelper.hpp>
#include <ql/termstructures/inflationtermstructure.hpp>

namespace QuantExt {

//! Year-on-year inflation-swap helper using a zero inflation curve as bootstrap target.
class MixedYearOnYearInflationSwapHelper
: public QuantLib::RelativeDateBootstrapHelper<QuantLib::ZeroInflationTermStructure> {
public:
MixedYearOnYearInflationSwapHelper(const QuantLib::Handle<QuantLib::Quote>& quote,
const QuantLib::Period& swapObsLag,
const QuantLib::Date& maturity,
QuantLib::Calendar calendar,
QuantLib::BusinessDayConvention paymentConvention,
QuantLib::DayCounter dayCounter,
const QuantLib::ext::shared_ptr<QuantLib::ZeroInflationIndex>& zii,
QuantLib::CPI::InterpolationType interpolation);

void setTermStructure(QuantLib::ZeroInflationTermStructure*) override;
QuantLib::Real impliedQuote() const override;

QuantLib::ext::shared_ptr<QuantLib::YearOnYearInflationSwap> swap() const { return yyiis_; }

protected:
void initializeDates() override;

private:
QuantLib::Period swapObsLag_;
QuantLib::Date maturity_;
QuantLib::Calendar calendar_;
QuantLib::BusinessDayConvention paymentConvention_;
QuantLib::DayCounter dayCounter_;
QuantLib::ext::shared_ptr<QuantLib::YoYInflationIndex> yii_;
QuantLib::CPI::InterpolationType interpolation_;
QuantLib::ext::shared_ptr<QuantLib::YearOnYearInflationSwap> yyiis_;
QuantLib::Handle<QuantLib::YieldTermStructure> nominalTermStructure_;
QuantLib::RelinkableHandle<QuantLib::ZeroInflationTermStructure> termStructureHandle_;
};

} // namespace QuantExt