Coverage Report

Created: 2025-08-05 06:45

/src/quantlib/ql/indexes/inflationindex.cpp
Line
Count
Source (jump to first uncovered line)
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2007 Chris Kenyon
5
 Copyright (C) 2021 Ralf Konrad Eckel
6
7
 This file is part of QuantLib, a free-software/open-source library
8
 for financial quantitative analysts and developers - http://quantlib.org/
9
10
 QuantLib is free software: you can redistribute it and/or modify it
11
 under the terms of the QuantLib license.  You should have received a
12
 copy of the license along with this program; if not, please email
13
 <quantlib-dev@lists.sf.net>. The license is also available online at
14
 <http://quantlib.org/license.shtml>.
15
16
 This program is distributed in the hope that it will be useful, but WITHOUT
17
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18
 FOR A PARTICULAR PURPOSE.  See the license for more details.
19
*/
20
21
#include <ql/indexes/inflationindex.hpp>
22
#include <ql/termstructures/inflationtermstructure.hpp>
23
#include <ql/time/calendars/nullcalendar.hpp>
24
#include <utility>
25
26
namespace QuantLib {
27
28
    Real CPI::laggedFixing(const ext::shared_ptr<ZeroInflationIndex>& index,
29
                           const Date& date,
30
                           const Period& observationLag,
31
0
                           CPI::InterpolationType interpolationType) {
32
33
0
        switch (interpolationType) {
34
0
          case AsIndex:
35
0
          case Flat: {
36
0
              auto fixingPeriod = inflationPeriod(date - observationLag, index->frequency());
37
0
              return index->fixing(fixingPeriod.first);
38
0
          }
39
0
          case Linear: {
40
0
              auto fixingPeriod = inflationPeriod(date - observationLag, index->frequency());
41
0
              auto interpolationPeriod = inflationPeriod(date, index->frequency());
42
43
0
              auto I0 = index->fixing(fixingPeriod.first);
44
45
0
              if (date == interpolationPeriod.first) {
46
                  // special case; no interpolation.  This avoids asking for
47
                  // the fixing at the end of the period, which might need a
48
                  // forecast curve to be set.
49
0
                  return I0;
50
0
              }
51
52
0
              static const auto oneDay = Period(1, Days);
53
54
0
              auto I1 = index->fixing(fixingPeriod.second + oneDay);
55
56
0
              return I0 + (I1 - I0) * (date - interpolationPeriod.first) /
57
0
                  (Real)((interpolationPeriod.second + oneDay) - interpolationPeriod.first);
58
0
          }
59
0
          default:
60
0
            QL_FAIL("unknown CPI interpolation type: " << int(interpolationType));
61
0
        }
62
0
    }
63
64
65
    Real CPI::laggedYoYRate(const ext::shared_ptr<YoYInflationIndex>& index,
66
                            const Date& date,
67
                            const Period& observationLag,
68
0
                            CPI::InterpolationType interpolationType) {
69
70
0
        switch (interpolationType) {
71
0
          case AsIndex: {
72
0
              return index->fixing(date - observationLag);
73
0
          }
74
0
          case Flat: {
75
0
              auto fixingPeriod = inflationPeriod(date - observationLag, index->frequency());
76
0
              return index->fixing(fixingPeriod.first);
77
0
          }
78
0
          case Linear: {
79
0
              if (index->ratio() && !index->needsForecast(date)) {
80
                  // in the case of a ratio, the convention seems to be to interpolate
81
                  // the underlying index fixings first, then take the ratio.  This is
82
                  // not the same as taking the ratios and then interpolate, which is
83
                  // equivalent to what the else clause does.
84
                  // However, we can only do this if the fixings we need are in the past,
85
                  // because forecasts need to be done through the YoY forecast curve,
86
                  // and not the underlying index.
87
88
0
                  auto underlying = index->underlyingIndex();
89
0
                  Rate Z1 = CPI::laggedFixing(underlying, date, observationLag, interpolationType);
90
0
                  Rate Z0 = CPI::laggedFixing(underlying, date - 1*Years, observationLag, interpolationType);
91
92
0
                  return Z1/Z0 - 1.0;
93
94
0
              } else {
95
0
                  static const auto oneDay = Period(1, Days);
96
97
0
                  auto fixingPeriod = inflationPeriod(date - observationLag, index->frequency());
98
0
                  auto interpolationPeriod = inflationPeriod(date, index->frequency());
99
100
0
                  auto Y0 = index->fixing(fixingPeriod.first);
101
102
0
                  if (date == interpolationPeriod.first) {
103
                      // special case; no interpolation anyway.
104
0
                      return Y0;
105
0
                  }
106
107
0
                  auto Y1 = index->fixing(fixingPeriod.second + oneDay);
108
109
0
                  return Y0 + (Y1 - Y0) * (date - interpolationPeriod.first) /
110
0
                      (Real)((interpolationPeriod.second + oneDay) - interpolationPeriod.first);
111
0
              }
112
0
          }
113
0
          default:
114
0
            QL_FAIL("unknown CPI interpolation type: " << int(interpolationType));
115
0
        }
116
0
    }
117
118
119
    InflationIndex::InflationIndex(std::string familyName,
120
                                   Region region,
121
                                   bool revised,
122
                                   Frequency frequency,
123
                                   const Period& availabilityLag,
124
                                   Currency currency)
125
0
    : familyName_(std::move(familyName)), region_(std::move(region)), revised_(revised),
126
0
      frequency_(frequency), availabilityLag_(availabilityLag), currency_(std::move(currency)) {
127
0
        name_ = region_.name() + " " + familyName_;
128
0
        registerWith(Settings::instance().evaluationDate());
129
0
        registerWith(notifier());
130
0
    }
131
132
0
    Calendar InflationIndex::fixingCalendar() const {
133
0
        static NullCalendar c;
134
0
        return c;
135
0
    }
136
137
    void InflationIndex::addFixing(const Date& fixingDate,
138
                                   Real fixing,
139
0
                                   bool forceOverwrite) {
140
141
0
        std::pair<Date,Date> lim = inflationPeriod(fixingDate, frequency_);
142
0
        Size n = static_cast<QuantLib::Size>(lim.second - lim.first) + 1;
143
0
        std::vector<Date> dates(n);
144
0
        std::vector<Rate> rates(n);
145
0
        for (Size i=0; i<n; ++i) {
146
0
            dates[i] = lim.first + i;
147
0
            rates[i] = fixing;
148
0
        }
149
150
0
        Index::addFixings(dates.begin(), dates.end(),
151
0
                          rates.begin(), forceOverwrite);
152
0
    }
153
154
    ZeroInflationIndex::ZeroInflationIndex(const std::string& familyName,
155
                                           const Region& region,
156
                                           bool revised,
157
                                           Frequency frequency,
158
                                           const Period& availabilityLag,
159
                                           const Currency& currency,
160
                                           Handle<ZeroInflationTermStructure> zeroInflation)
161
0
    : InflationIndex(familyName, region, revised, frequency, availabilityLag, currency),
162
0
      zeroInflation_(std::move(zeroInflation)) {
163
0
        registerWith(zeroInflation_);
164
0
    }
165
166
    Real ZeroInflationIndex::fixing(const Date& fixingDate,
167
0
                                    bool /*forecastTodaysFixing*/) const {
168
0
        if (!needsForecast(fixingDate)) {
169
0
            const Real I1 = pastFixing(fixingDate);
170
0
            QL_REQUIRE(I1 != Null<Real>(),
171
0
                       "Missing " << name() << " fixing for "
172
0
                       << inflationPeriod(fixingDate, frequency_).first);
173
174
0
            return I1;
175
0
        } else {
176
0
            return forecastFixing(fixingDate);
177
0
        }
178
0
    }
179
180
0
    Real ZeroInflationIndex::pastFixing(const Date& fixingDate) const {
181
0
        const auto p = inflationPeriod(fixingDate, frequency_);
182
0
        const auto& ts = timeSeries();
183
0
        return ts[p.first];
184
0
    }
185
186
0
    Date ZeroInflationIndex::lastFixingDate() const {
187
0
        const auto& fixings = timeSeries();
188
0
        QL_REQUIRE(!fixings.empty(), "no fixings stored for " << name());
189
        // attribute fixing to first day of the underlying period
190
0
        return inflationPeriod(fixings.lastDate(), frequency_).first;
191
0
    }
192
193
0
    bool ZeroInflationIndex::needsForecast(const Date& fixingDate) const {
194
195
0
        Date today = Settings::instance().evaluationDate();
196
197
0
        auto latestPossibleHistoricalFixingPeriod =
198
0
            inflationPeriod(today - availabilityLag_, frequency_);
199
200
        // Zero-index fixings are always non-interpolated.
201
0
        auto fixingPeriod = inflationPeriod(fixingDate, frequency_);
202
0
        Date latestNeededDate = fixingPeriod.first;
203
204
0
        if (latestNeededDate < latestPossibleHistoricalFixingPeriod.first) {
205
            // the fixing date is well before the availability lag, so
206
            // we know that fixings must be provided.
207
0
            return false;
208
0
        } else if (latestNeededDate > latestPossibleHistoricalFixingPeriod.second) {
209
            // the fixing can't be available yet
210
0
            return true;
211
0
        } else {
212
            // we're not sure, but the fixing might be there so we check.
213
0
            Real f = timeSeries()[latestNeededDate];
214
0
            return (f == Null<Real>());
215
0
        }
216
0
    }
217
218
219
0
    Real ZeroInflationIndex::forecastFixing(const Date& fixingDate) const {
220
        // the term structure is relative to the fixing value at the base date.
221
0
        Date baseDate = zeroInflation_->baseDate();
222
0
        QL_REQUIRE(!needsForecast(baseDate),
223
0
                   name() << " index fixing at base date " << baseDate << " is not available");
224
0
        Real baseFixing = fixing(baseDate);
225
226
0
        std::pair<Date, Date> fixingPeriod = inflationPeriod(fixingDate, frequency_);
227
228
0
        Date firstDateInPeriod = fixingPeriod.first;
229
0
        Rate Z1 = zeroInflation_->zeroRate(firstDateInPeriod, Period(0,Days), false);
230
0
        Time t1 = inflationYearFraction(frequency_, false, zeroInflation_->dayCounter(),
231
0
                                        baseDate, firstDateInPeriod);
232
0
        return baseFixing * std::pow(1.0 + Z1, t1);
233
0
    }
234
235
236
    ext::shared_ptr<ZeroInflationIndex> ZeroInflationIndex::clone(
237
0
                          const Handle<ZeroInflationTermStructure>& h) const {
238
0
        return ext::make_shared<ZeroInflationIndex>(
239
0
            familyName_, region_, revised_, frequency_, availabilityLag_, currency_, h);
240
0
    }
241
242
243
    YoYInflationIndex::YoYInflationIndex(const ext::shared_ptr<ZeroInflationIndex>& underlyingIndex,
244
                                         Handle<YoYInflationTermStructure> yoyInflation)
245
0
    : InflationIndex("YYR_" + underlyingIndex->familyName(), underlyingIndex->region(),
246
0
                     underlyingIndex->revised(), underlyingIndex->frequency(),
247
0
                     underlyingIndex->availabilityLag(), underlyingIndex->currency()),
248
0
      interpolated_(false), ratio_(true), underlyingIndex_(underlyingIndex),
249
0
      yoyInflation_(std::move(yoyInflation)) {
250
0
        registerWith(underlyingIndex_);
251
0
        registerWith(yoyInflation_);
252
0
    }
253
254
    YoYInflationIndex::YoYInflationIndex(const ext::shared_ptr<ZeroInflationIndex>& underlyingIndex,
255
                                         bool interpolated,
256
                                         Handle<YoYInflationTermStructure> yoyInflation)
257
0
    : YoYInflationIndex(underlyingIndex, std::move(yoyInflation)) {
258
0
        interpolated_ = interpolated;
259
0
    }
260
261
    YoYInflationIndex::YoYInflationIndex(const std::string& familyName,
262
                                         const Region& region,
263
                                         bool revised,
264
                                         Frequency frequency,
265
                                         const Period& availabilityLag,
266
                                         const Currency& currency,
267
                                         Handle<YoYInflationTermStructure> yoyInflation)
268
0
    : InflationIndex(familyName, region, revised, frequency, availabilityLag, currency),
269
0
      interpolated_(false), ratio_(false), yoyInflation_(std::move(yoyInflation)) {
270
0
        registerWith(yoyInflation_);
271
0
    }
272
273
    YoYInflationIndex::YoYInflationIndex(const std::string& familyName,
274
                                         const Region& region,
275
                                         bool revised,
276
                                         bool interpolated,
277
                                         Frequency frequency,
278
                                         const Period& availabilityLag,
279
                                         const Currency& currency,
280
                                         Handle<YoYInflationTermStructure> yoyInflation)
281
0
    : YoYInflationIndex(familyName, region, revised, frequency, availabilityLag, currency, std::move(yoyInflation)) {
282
0
        interpolated_ = interpolated;
283
0
    }
284
285
286
    Rate YoYInflationIndex::fixing(const Date& fixingDate,
287
0
                                   bool /*forecastTodaysFixing*/) const {
288
0
        if (needsForecast(fixingDate)) {
289
0
            return forecastFixing(fixingDate);
290
0
        } else {
291
0
            return pastFixing(fixingDate);
292
0
        }
293
0
    }
294
295
0
    Date YoYInflationIndex::lastFixingDate() const {
296
0
        if (ratio()) {
297
0
            return underlyingIndex_->lastFixingDate();
298
0
        } else {
299
0
            const auto& fixings = timeSeries();
300
0
            QL_REQUIRE(!fixings.empty(), "no fixings stored for " << name());
301
            // attribute fixing to first day of the underlying period
302
0
            return inflationPeriod(fixings.lastDate(), frequency_).first;
303
0
        }
304
0
    }
305
306
0
    bool YoYInflationIndex::needsForecast(const Date& fixingDate) const {
307
0
        Date today = Settings::instance().evaluationDate();
308
309
0
        auto fixingPeriod = inflationPeriod(fixingDate, frequency_);
310
0
        Date latestNeededDate;
311
0
        if (!interpolated() || fixingDate == fixingPeriod.first)
312
0
            latestNeededDate = fixingPeriod.first;
313
0
        else
314
0
            latestNeededDate = fixingPeriod.second + 1;
315
316
0
        if (ratio()) {
317
0
            return underlyingIndex_->needsForecast(latestNeededDate);
318
0
        } else {
319
0
            auto latestPossibleHistoricalFixingPeriod =
320
0
                inflationPeriod(today - availabilityLag_, frequency_);
321
322
0
            if (latestNeededDate < latestPossibleHistoricalFixingPeriod.first) {
323
                // the fixing date is well before the availability lag, so
324
                // we know that fixings must be provided.
325
0
                return false;
326
0
            } else if (latestNeededDate > latestPossibleHistoricalFixingPeriod.second) {
327
                // the fixing can't be available yet
328
0
                return true;
329
0
            } else {
330
                // we're not sure, but the fixing might be there so we check.
331
0
                Real f = timeSeries()[latestNeededDate];
332
0
                return (f == Null<Real>());
333
0
            }
334
0
        }
335
0
    }
336
337
0
    Real YoYInflationIndex::pastFixing(const Date& fixingDate) const {
338
0
        if (ratio()) {
339
340
0
            auto interpolationType = interpolated() ? CPI::Linear : CPI::Flat;
341
342
0
            Rate pastFixing = CPI::laggedFixing(underlyingIndex_, fixingDate, Period(0, Months), interpolationType);
343
0
            Rate previousFixing = CPI::laggedFixing(underlyingIndex_, fixingDate - 1*Years, Period(0, Months), interpolationType);
344
345
0
            return pastFixing/previousFixing - 1.0;
346
347
0
        } else {  // NOT ratio
348
349
0
            const auto& ts = timeSeries();
350
0
            auto [periodStart, periodEnd] = inflationPeriod(fixingDate, frequency_);
351
352
0
            Rate YY0 = ts[periodStart];
353
0
            QL_REQUIRE(YY0 != Null<Rate>(),
354
0
                       "Missing " << name() << " fixing for " << periodStart);
355
356
0
            if (!interpolated() || /* degenerate case */ fixingDate == periodStart) {
357
358
0
                return YY0;
359
360
0
            } else {
361
362
0
                Real dp = periodEnd + 1 - periodStart;
363
0
                Real dl = fixingDate - periodStart;
364
0
                Rate YY1 = ts[periodEnd+1];
365
0
                QL_REQUIRE(YY1 != Null<Rate>(),
366
0
                           "Missing " << name() << " fixing for " << periodEnd+1);
367
0
                return YY0 + (YY1 - YY0) * dl / dp;
368
369
0
            }
370
0
        }
371
0
    }
372
373
0
    Real YoYInflationIndex::forecastFixing(const Date& fixingDate) const {
374
375
0
        Date d;
376
0
        if (interpolated()) {
377
0
            d = fixingDate;
378
0
        } else {
379
            // if the value is not interpolated use the starting value
380
            // by internal convention this will be consistent
381
0
            std::pair<Date,Date> fixingPeriod = inflationPeriod(fixingDate, frequency_);
382
0
            d = fixingPeriod.first;
383
0
        }
384
0
        return yoyInflation_->yoyRate(d,0*Days);
385
0
    }
386
387
    ext::shared_ptr<YoYInflationIndex> YoYInflationIndex::clone(
388
0
                           const Handle<YoYInflationTermStructure>& h) const {
389
0
        QL_DEPRECATED_DISABLE_WARNING
390
0
        if (ratio_) {
391
            // NOLINTNEXTLINE(modernize-make-shared)
392
0
            return ext::shared_ptr<YoYInflationIndex>(
393
0
                new YoYInflationIndex(underlyingIndex_, interpolated_, h));
394
0
        } else {
395
            // NOLINTNEXTLINE(modernize-make-shared)
396
0
            return ext::shared_ptr<YoYInflationIndex>(
397
0
                new YoYInflationIndex(familyName_, region_, revised_,
398
0
                                      interpolated_, frequency_,
399
0
                                      availabilityLag_, currency_, h));
400
0
        }
401
0
        QL_DEPRECATED_ENABLE_WARNING
402
0
    }
403
404
405
    CPI::InterpolationType
406
0
    detail::CPI::effectiveInterpolationType(const QuantLib::CPI::InterpolationType& type) {
407
0
        if (type == QuantLib::CPI::AsIndex) {
408
0
            return QuantLib::CPI::Flat;
409
0
        } else {
410
0
            return type;
411
0
        }
412
0
    }
413
414
    CPI::InterpolationType
415
    detail::CPI::effectiveInterpolationType(const QuantLib::CPI::InterpolationType& type,
416
0
                                            const ext::shared_ptr<YoYInflationIndex>& index) {
417
0
        if (type == QuantLib::CPI::AsIndex) {
418
0
            return index->interpolated() ? QuantLib::CPI::Linear : QuantLib::CPI::Flat;
419
0
        } else {
420
0
            return type;
421
0
        }
422
0
    }
423
424
0
    bool detail::CPI::isInterpolated(const QuantLib::CPI::InterpolationType& type) {
425
0
        return detail::CPI::effectiveInterpolationType(type) == QuantLib::CPI::Linear;
426
0
    }
427
428
    bool detail::CPI::isInterpolated(const QuantLib::CPI::InterpolationType& type,
429
0
                                     const ext::shared_ptr<YoYInflationIndex>& index) {
430
0
        return detail::CPI::effectiveInterpolationType(type, index) == QuantLib::CPI::Linear;
431
0
    }
432
433
}