Skip to content

Commit

Permalink
Fix implementation of needsForecast for inflation indexes
Browse files Browse the repository at this point in the history
  • Loading branch information
lballabio committed Sep 17, 2024
1 parent 6de585e commit 6e91afa
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 54 deletions.
57 changes: 36 additions & 21 deletions ql/indexes/inflationindex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,27 +139,20 @@ namespace QuantLib {

bool ZeroInflationIndex::needsForecast(const Date& fixingDate) const {

// Stored fixings are always non-interpolated.
// If an interpolated fixing is required then
// the availability lag + one inflation period
// must have passed to use historical fixings
// (because you need the next one to interpolate).
// The interpolation is calculated (linearly) on demand.

Date today = Settings::instance().evaluationDate();
Date todayMinusLag = today - availabilityLag_;

Date historicalFixingKnown =
inflationPeriod(todayMinusLag, frequency_).first-1;
auto latestPossibleHistoricalFixingPeriod =
inflationPeriod(today - availabilityLag_, frequency_);

// Zero-index fixings are always non-interpolated.
Date latestNeededDate = fixingDate;

if (latestNeededDate <= historicalFixingKnown) {
if (latestNeededDate < latestPossibleHistoricalFixingPeriod.first) {
// the fixing date is well before the availability lag, so
// we know that fixings were provided.
// we know that fixings must be provided.
return false;
} else if (latestNeededDate > today) {
// the fixing can't be available, no matter what's in the
// time series
} else if (latestNeededDate > latestPossibleHistoricalFixingPeriod.second) {
// the fixing can't be available yet
return true;
} else {
// we're not sure, but the fixing might be there so we
Expand Down Expand Up @@ -233,14 +226,36 @@ namespace QuantLib {

bool YoYInflationIndex::needsForecast(const Date& fixingDate) const {
Date today = Settings::instance().evaluationDate();
auto [periodStart, periodEnd] = inflationPeriod(today - availabilityLag_, frequency_);
Date lastFix = periodStart-1;

Date flatMustForecastOn = lastFix+1;
Date interpMustForecastOn = lastFix+1 - Period(frequency_);
auto fixingPeriod = inflationPeriod(fixingDate, frequency_);
Date latestNeededDate;
if (!interpolated() || fixingDate == fixingPeriod.first)
latestNeededDate = fixingPeriod.first;
else
latestNeededDate = fixingPeriod.second + 1;

return (interpolated() && fixingDate >= interpMustForecastOn)
|| (!interpolated() && fixingDate >= flatMustForecastOn);
if (ratio()) {
return underlyingIndex_->needsForecast(latestNeededDate);
} else {
auto latestPossibleHistoricalFixingPeriod =
inflationPeriod(today - availabilityLag_, frequency_);

if (latestNeededDate < latestPossibleHistoricalFixingPeriod.first) {
// the fixing date is well before the availability lag, so
// we know that fixings must be provided.
return false;
} else if (latestNeededDate > latestPossibleHistoricalFixingPeriod.second) {
// the fixing can't be available yet
return true;
} else {
// we're not sure, but the fixing might be there so we
// check. Todo: check which fixings are not possible, to
// avoid using fixings in the future
Date first = Date(1, latestNeededDate.month(), latestNeededDate.year());
Real f = timeSeries()[first];
return (f == Null<Real>());
}
}
}

Real YoYInflationIndex::pastFixing(const Date& fixingDate) const {
Expand Down
13 changes: 3 additions & 10 deletions ql/indexes/inflationindex.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,16 @@ namespace QuantLib {
the Index interface) is currently ignored.
*/
Real fixing(const Date& fixingDate, bool forecastTodaysFixing = false) const override;
//! returns a past fixing at the given date
Real pastFixing(const Date& fixingDate) const override;
//@}
//! \name Other methods
//@{
Date lastFixingDate() const;
Handle<ZeroInflationTermStructure> zeroInflationTermStructure() const;
ext::shared_ptr<ZeroInflationIndex> clone(const Handle<ZeroInflationTermStructure>& h) const;
bool needsForecast(const Date& fixingDate) const;
//@}
private:
bool needsForecast(const Date& fixingDate) const;
Real forecastFixing(const Date& fixingDate) const;
Handle<ZeroInflationTermStructure> zeroInflation_;
};
Expand Down Expand Up @@ -205,30 +204,24 @@ namespace QuantLib {
the Index interface) is currently ignored.
*/
Rate fixing(const Date& fixingDate, bool forecastTodaysFixing = false) const override;

/*! returns a past fixing at the given date
* \warning This is only supported for flat YOY indices providing their own timeseries
* via the `addFixing` or `addFixings` method,
* aka where ratio() == interpolated() == false.
*/
Real pastFixing(const Date& fixingDate) const override;
//@}

//! \name Inspectors
//! \name Other methods
//@{
bool interpolated() const;
bool ratio() const;
ext::shared_ptr<ZeroInflationIndex> underlyingIndex() const;
Handle<YoYInflationTermStructure> yoyInflationTermStructure() const;

ext::shared_ptr<YoYInflationIndex> clone(const Handle<YoYInflationTermStructure>& h) const;
bool needsForecast(const Date& fixingDate) const;
//@}

protected:
bool interpolated_;

private:
bool needsForecast(const Date& fixingDate) const;
Rate forecastFixing(const Date& fixingDate) const;
bool ratio_;
ext::shared_ptr<ZeroInflationIndex> underlyingIndex_;
Expand Down
148 changes: 125 additions & 23 deletions test-suite/inflation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -733,35 +733,47 @@ BOOST_AUTO_TEST_CASE(testSeasonalityCorrection) {
BOOST_AUTO_TEST_CASE(testZeroIndexFutureFixing) {
BOOST_TEST_MESSAGE("Testing that zero inflation indices forecast future fixings...");

// we create an index without a term structure, so
// it won't be able to forecast fixings
EUHICP euhicp;

Date sample_date = Date(1,December,2013);
Real sample_fixing = 117.48;
euhicp.addFixing(sample_date, sample_fixing);
// let's say we're at some point in April 2024...
Settings::instance().evaluationDate() = {10, April, 2024};

// fixing date in the past
Date evaluationDate = euhicp.fixingCalendar().adjust(sample_date + 2*Weeks);
Settings::instance().evaluationDate() = evaluationDate;
Real fixing = euhicp.fixing(sample_date);
if (std::fabs(fixing - sample_fixing) > 1e-12)
// ..and the last available fixing is February 2024, we don't have March yet
euhicp.addFixing({1,December,2023}, 100.0);
euhicp.addFixing({1,January,2024}, 100.1);
euhicp.addFixing({1,February,2024}, 100.2);

// Asking for the February fixing works, it's stored
Real fixing = euhicp.fixing({1,February,2024});
Real expected = 100.2;
if (std::fabs(fixing - expected) > 1e-12)
BOOST_ERROR("Failed to retrieve correct fixing: "
<< "\n returned: " << fixing
<< "\n expected: " << sample_fixing);
<< "\n expected: " << expected);

// fixing date in the future
evaluationDate = euhicp.fixingCalendar().adjust(sample_date - 2*Weeks);
Settings::instance().evaluationDate() = evaluationDate;
bool retrieved = false;
try {
fixing = euhicp.fixing(sample_date);
// the above should throw for lack of a forecast curve, so
// this shouldn't be executed and retrieved should stay false
retrieved = true;
} catch (Error&) {}

if (retrieved)
BOOST_ERROR("Retrieved future fixing: "
<< "\n returned: " << fixing);
// Asking for the March fixing doesn't (because we can't forecast)
BOOST_CHECK_EXCEPTION(euhicp.fixing({1,March,2024}), Error,
ExpectedErrorMessage("empty Handle"));

// but it works once it gets published:
euhicp.addFixing({1,March,2024}, 100.3);
fixing = euhicp.fixing({1,March,2024});
expected = 100.3;
if (std::fabs(fixing - expected) > 1e-12)
BOOST_ERROR("Failed to retrieve correct fixing: "
<< "\n returned: " << fixing
<< "\n expected: " << expected);

// On the other hand, April would be forecast...
BOOST_CHECK_EXCEPTION(euhicp.fixing({1,April,2024}), Error,
ExpectedErrorMessage("empty Handle"));

// ...even if it's stored:
euhicp.addFixing({1,April,2024}, 100.4);
BOOST_CHECK_EXCEPTION(euhicp.fixing({1,April,2024}), Error,
ExpectedErrorMessage("empty Handle"));
}

BOOST_AUTO_TEST_CASE(testInterpolatedZeroTermStructure) {
Expand Down Expand Up @@ -840,6 +852,48 @@ BOOST_AUTO_TEST_CASE(testQuotedYYIndex) {
}
}

BOOST_AUTO_TEST_CASE(testQuotedYYIndexFutureFixing) {
BOOST_TEST_MESSAGE("Testing that quoted year-on-year inflation indices forecast future fixings...");

// we create indexes without a term structure, so
// they won't be able to forecast fixings
YYEUHICP quoted_flat(false);
YYEUHICP quoted_linear(true);

// let's say we're at some point in April 2024...
Settings::instance().evaluationDate() = {10, April, 2024};

// ..and the last available fixing is February 2024, we don't have March yet
quoted_flat.addFixing({1,December,2023}, 100.0);
quoted_flat.addFixing({1,January,2024}, 100.1);
quoted_flat.addFixing({1,February,2024}, 100.2);

// mid-January fixing: ok for both flat and interpolated
BOOST_CHECK_NO_THROW(quoted_flat.fixing({15,January,2024}));
BOOST_CHECK_NO_THROW(quoted_linear.fixing({15,January,2024}));

// mid-February fixing: ok for flat, interpolated needs March
BOOST_CHECK_NO_THROW(quoted_flat.fixing({15,February,2024}));
BOOST_CHECK_EXCEPTION(quoted_linear.fixing({15,February,2024}), Error,
ExpectedErrorMessage("empty Handle"));

// but February 1st works (special case, March would have null
// weight in the interpolation)
BOOST_CHECK_NO_THROW(quoted_linear.fixing({1,February,2024}));

// both ok after March is published:
quoted_flat.addFixing({1,March,2024}, 100.3);
BOOST_CHECK_NO_THROW(quoted_flat.fixing({15,February,2024}));
BOOST_CHECK_NO_THROW(quoted_linear.fixing({15,February,2024}));

// April can't be available now, both fail even if it's stored:
quoted_flat.addFixing({1,April,2024}, 100.4);
BOOST_CHECK_EXCEPTION(quoted_flat.fixing({1,April,2024}), Error,
ExpectedErrorMessage("empty Handle"));
BOOST_CHECK_EXCEPTION(quoted_linear.fixing({1,April,2024}), Error,
ExpectedErrorMessage("empty Handle"));
}

BOOST_AUTO_TEST_CASE(testRatioYYIndex) {
BOOST_TEST_MESSAGE("Testing ratio year-on-year inflation indices...");

Expand Down Expand Up @@ -958,6 +1012,54 @@ BOOST_AUTO_TEST_CASE(testRatioYYIndex) {
}
}

BOOST_AUTO_TEST_CASE(testRatioYYIndexFutureFixing) {
BOOST_TEST_MESSAGE("Testing that ratio year-on-year inflation indices forecast future fixings...");

// we create indexes without a term structure, so
// they won't be able to forecast fixings
auto euhicp = ext::make_shared<EUHICP>();
YoYInflationIndex ratio_flat(euhicp, false);
YoYInflationIndex ratio_linear(euhicp, true);

// let's say we're at some point in April 2024...
Settings::instance().evaluationDate() = {10, April, 2024};

// ..and the last available fixing is February 2024, we don't have March yet
euhicp->addFixing({1,December,2022}, 98.0);
euhicp->addFixing({1,January,2023}, 98.1);
euhicp->addFixing({1,February,2023}, 98.2);
euhicp->addFixing({1,March,2023}, 98.3);
// ...
euhicp->addFixing({1,December,2023}, 100.0);
euhicp->addFixing({1,January,2024}, 100.1);
euhicp->addFixing({1,February,2024}, 100.2);

// mid-January fixing: ok for both flat and interpolated
BOOST_CHECK_NO_THROW(ratio_flat.fixing({15,January,2024}));
BOOST_CHECK_NO_THROW(ratio_linear.fixing({15,January,2024}));

// mid-February fixing: ok for flat, interpolated needs March
BOOST_CHECK_NO_THROW(ratio_flat.fixing({15,February,2024}));
BOOST_CHECK_EXCEPTION(ratio_linear.fixing({15,February,2024}), Error,
ExpectedErrorMessage("empty Handle"));

// but February 1st works (special case, March would have null
// weight in the interpolation)
BOOST_CHECK_NO_THROW(ratio_linear.fixing({1,February,2024}));

// both ok after March is published:
euhicp->addFixing({1,March,2024}, 100.3);
BOOST_CHECK_NO_THROW(ratio_flat.fixing({15,February,2024}));
BOOST_CHECK_NO_THROW(ratio_linear.fixing({15,February,2024}));

// April can't be available now, both fail even if it's stored:
euhicp->addFixing({1,April,2024}, 100.4);
BOOST_CHECK_EXCEPTION(ratio_flat.fixing({1,April,2024}), Error,
ExpectedErrorMessage("empty Handle"));
BOOST_CHECK_EXCEPTION(ratio_linear.fixing({1,April,2024}), Error,
ExpectedErrorMessage("empty Handle"));
}

BOOST_AUTO_TEST_CASE(testYYTermStructure) {
BOOST_TEST_MESSAGE("Testing year-on-year inflation term structure...");

Expand Down

0 comments on commit 6e91afa

Please sign in to comment.