Coverage Report

Created: 2026-06-23 06:40

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