Coverage Report

Created: 2025-08-05 06:45

/src/quantlib/ql/termstructures/globalbootstrap.hpp
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) 2019 SoftSolutions! S.r.l.
5
6
 This file is part of QuantLib, a free-software/open-source library
7
 for financial quantitative analysts and developers - http://quantlib.org/
8
9
 QuantLib is free software: you can redistribute it and/or modify it
10
 under the terms of the QuantLib license.  You should have received a
11
 copy of the license along with this program; if not, please email
12
 <quantlib-dev@lists.sf.net>. The license is also available online at
13
 <http://quantlib.org/license.shtml>.
14
15
 This program is distributed in the hope that it will be useful, but WITHOUT
16
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
 FOR A PARTICULAR PURPOSE.  See the license for more details.
18
*/
19
20
/*! \file globalbootstrap.hpp
21
    \brief global bootstrap, with additional restrictions
22
*/
23
24
#ifndef quantlib_global_bootstrap_hpp
25
#define quantlib_global_bootstrap_hpp
26
27
#include <ql/functional.hpp>
28
#include <ql/math/interpolations/linearinterpolation.hpp>
29
#include <ql/math/optimization/levenbergmarquardt.hpp>
30
#include <ql/termstructures/bootstraphelper.hpp>
31
#include <ql/utilities/dataformatters.hpp>
32
#include <algorithm>
33
#include <utility>
34
35
namespace QuantLib {
36
37
class AdditionalBootstrapVariables {
38
  public:
39
0
    virtual ~AdditionalBootstrapVariables() = default;
40
    // Initialize variables to initial guesses and return them.
41
    virtual Array initialize(bool validData) = 0;
42
    // Update variables to given values.
43
    virtual void update(const Array& x) = 0;
44
};
45
46
/*! Global boostrapper, with additional restrictions
47
48
  The additionalDates functor must return a set of additional dates to add to the
49
  interpolation grid. These dates must only depend on the global evaluation date.
50
51
  The additionalPenalties functor must yield at least as many values such that
52
53
  number of (usual, alive) rate helpers + number of additional values >= number of data points - 1
54
55
  (note that the data points contain t=0). These values are treated as additional
56
  error terms in the optimization. The usual rate helpers return quoteError here.
57
  All error terms are equally weighted.
58
59
  The additionalHelpers are registered with the curve like the usual rate helpers,
60
  but no pillar dates or error terms are added for them. Pillars and error terms
61
  have to be added by additionalDates and additionalPenalties.
62
63
  The additionalVariables interface manages a set of additional variables to add
64
  to the optimization. This is useful to optimize model parameters used by rate
65
  helpers, for example, convexity adjustments for futures. See SimpleQuoteVariables
66
  for a concrete implementation of this interface.
67
68
  WARNING: This class is known to work with Traits Discount, ZeroYield, Forward,
69
  i.e. the usual IR curves traits in QL. It requires Traits::transformDirect()
70
  and Traits::transformInverse() to be implemented. Also, check the usage of
71
  Traits::updateGuess(), Traits::guess() in this class.
72
*/
73
template <class Curve> class GlobalBootstrap {
74
    typedef typename Curve::traits_type Traits;             // ZeroYield, Discount, ForwardRate
75
    typedef typename Curve::interpolator_type Interpolator; // Linear, LogLinear, ...
76
    typedef std::function<Array(const std::vector<Time>&, const std::vector<Real>&)>
77
        AdditionalPenalties;
78
79
  public:
80
    GlobalBootstrap(Real accuracy = Null<Real>(),
81
                    ext::shared_ptr<OptimizationMethod> optimizer = nullptr,
82
                    ext::shared_ptr<EndCriteria> endCriteria = nullptr);
83
    GlobalBootstrap(std::vector<ext::shared_ptr<typename Traits::helper> > additionalHelpers,
84
                    std::function<std::vector<Date>()> additionalDates,
85
                    AdditionalPenalties additionalPenalties,
86
                    Real accuracy = Null<Real>(),
87
                    ext::shared_ptr<OptimizationMethod> optimizer = nullptr,
88
                    ext::shared_ptr<EndCriteria> endCriteria = nullptr,
89
                    ext::shared_ptr<AdditionalBootstrapVariables> additionalVariables = nullptr);
90
    GlobalBootstrap(std::vector<ext::shared_ptr<typename Traits::helper> > additionalHelpers,
91
                    std::function<std::vector<Date>()> additionalDates,
92
                    std::function<Array()> additionalPenalties,
93
                    Real accuracy = Null<Real>(),
94
                    ext::shared_ptr<OptimizationMethod> optimizer = nullptr,
95
                    ext::shared_ptr<EndCriteria> endCriteria = nullptr,
96
                    ext::shared_ptr<AdditionalBootstrapVariables> additionalVariables = nullptr);
97
    void setup(Curve *ts);
98
    void calculate() const;
99
100
  private:
101
    void initialize() const;
102
    Curve *ts_;
103
    Real accuracy_;
104
    ext::shared_ptr<OptimizationMethod> optimizer_;
105
    ext::shared_ptr<EndCriteria> endCriteria_;
106
    mutable std::vector<ext::shared_ptr<typename Traits::helper> > additionalHelpers_;
107
    std::function<std::vector<Date>()> additionalDates_;
108
    AdditionalPenalties additionalPenalties_;
109
    ext::shared_ptr<AdditionalBootstrapVariables> additionalVariables_;
110
    mutable bool initialized_ = false, validCurve_ = false;
111
    mutable Size firstHelper_, numberHelpers_;
112
    mutable Size firstAdditionalHelper_, numberAdditionalHelpers_;
113
};
114
115
// template definitions
116
117
template <class Curve>
118
GlobalBootstrap<Curve>::GlobalBootstrap(
119
    Real accuracy,
120
    ext::shared_ptr<OptimizationMethod> optimizer,
121
    ext::shared_ptr<EndCriteria> endCriteria)
122
: ts_(nullptr), accuracy_(accuracy), optimizer_(std::move(optimizer)),
123
  endCriteria_(std::move(endCriteria)) {}
124
125
template <class Curve>
126
GlobalBootstrap<Curve>::GlobalBootstrap(
127
    std::vector<ext::shared_ptr<typename Traits::helper> > additionalHelpers,
128
    std::function<std::vector<Date>()> additionalDates,
129
    AdditionalPenalties additionalPenalties,
130
    Real accuracy,
131
    ext::shared_ptr<OptimizationMethod> optimizer,
132
    ext::shared_ptr<EndCriteria> endCriteria,
133
    ext::shared_ptr<AdditionalBootstrapVariables> additionalVariables)
134
: ts_(nullptr), accuracy_(accuracy), optimizer_(std::move(optimizer)),
135
  endCriteria_(std::move(endCriteria)), additionalHelpers_(std::move(additionalHelpers)),
136
  additionalDates_(std::move(additionalDates)), additionalPenalties_(std::move(additionalPenalties)),
137
  additionalVariables_(std::move(additionalVariables)) {}
138
139
template <class Curve>
140
GlobalBootstrap<Curve>::GlobalBootstrap(
141
    std::vector<ext::shared_ptr<typename Traits::helper> > additionalHelpers,
142
    std::function<std::vector<Date>()> additionalDates,
143
    std::function<Array()> additionalPenalties,
144
    Real accuracy,
145
    ext::shared_ptr<OptimizationMethod> optimizer,
146
    ext::shared_ptr<EndCriteria> endCriteria,
147
    ext::shared_ptr<AdditionalBootstrapVariables> additionalVariables)
148
: GlobalBootstrap(std::move(additionalHelpers), std::move(additionalDates),
149
                  additionalPenalties
150
                    ? [f=std::move(additionalPenalties)](const std::vector<Time>&, const std::vector<Real>&) {
151
                        return f();
152
                    }
153
                    : AdditionalPenalties(),
154
                  accuracy, std::move(optimizer), std::move(endCriteria),
155
                  std::move(additionalVariables)) {}
156
157
template <class Curve> void GlobalBootstrap<Curve>::setup(Curve *ts) {
158
    ts_ = ts;
159
    for (Size j = 0; j < ts_->instruments_.size(); ++j)
160
        ts_->registerWithObservables(ts_->instruments_[j]);
161
    for (Size j = 0; j < additionalHelpers_.size(); ++j)
162
        ts_->registerWithObservables(additionalHelpers_[j]);
163
164
    // setup optimizer and EndCriteria
165
    Real accuracy = accuracy_ != Null<Real>() ? accuracy_ : ts_->accuracy_;
166
    if (!optimizer_) {
167
        optimizer_ = ext::make_shared<LevenbergMarquardt>(accuracy, accuracy, accuracy);
168
    }
169
    if (!endCriteria_) {
170
        endCriteria_ = ext::make_shared<EndCriteria>(1000, 10, accuracy, accuracy, accuracy);
171
    }
172
173
    // do not initialize yet: instruments could be invalid here
174
    // but valid later when bootstrapping is actually required
175
}
176
177
template <class Curve> void GlobalBootstrap<Curve>::initialize() const {
178
179
    // ensure helpers are sorted
180
    std::sort(ts_->instruments_.begin(), ts_->instruments_.end(), detail::BootstrapHelperSorter());
181
    std::sort(additionalHelpers_.begin(), additionalHelpers_.end(), detail::BootstrapHelperSorter());
182
183
    // skip expired helpers
184
    const Date firstDate = Traits::initialDate(ts_);
185
186
    firstHelper_ = 0;
187
    if (!ts_->instruments_.empty()) {
188
        while (firstHelper_ < ts_->instruments_.size() && ts_->instruments_[firstHelper_]->pillarDate() <= firstDate)
189
            ++firstHelper_;
190
    }
191
    numberHelpers_ = ts_->instruments_.size() - firstHelper_;
192
193
    // skip expired additional helpers
194
    firstAdditionalHelper_ = 0;
195
    if (!additionalHelpers_.empty()) {
196
        while (firstAdditionalHelper_ < additionalHelpers_.size() &&
197
               additionalHelpers_[firstAdditionalHelper_]->pillarDate() <= firstDate)
198
            ++firstAdditionalHelper_;
199
    }
200
    numberAdditionalHelpers_ = additionalHelpers_.size() - firstAdditionalHelper_;
201
202
    // skip expired additional dates
203
    std::vector<Date> additionalDates;
204
    if (additionalDates_)
205
        additionalDates = additionalDates_();
206
    if (!additionalDates.empty()) {
207
        additionalDates.erase(
208
            std::remove_if(additionalDates.begin(), additionalDates.end(),
209
                           [=](const Date& date) { return date <= firstDate; }),
210
            additionalDates.end()
211
        );
212
    }
213
    const Size numberAdditionalDates = additionalDates.size();
214
215
    QL_REQUIRE(numberHelpers_ + numberAdditionalDates >= Interpolator::requiredPoints - 1,
216
               "not enough alive instruments (" << numberHelpers_ << ") + additional dates (" << numberAdditionalDates
217
                                                << ") = " << numberHelpers_ + numberAdditionalDates << " provided, "
218
                                                << Interpolator::requiredPoints - 1 << " required");
219
220
    // calculate dates and times
221
    std::vector<Date> &dates = ts_->dates_;
222
    std::vector<Time> &times = ts_->times_;
223
224
    // first populate the dates vector and make sure they are sorted and there are no duplicates
225
    dates.clear();
226
    dates.push_back(firstDate);
227
    for (Size j = 0; j < numberHelpers_; ++j)
228
        dates.push_back(ts_->instruments_[firstHelper_ + j]->pillarDate());
229
    dates.insert(dates.end(), additionalDates.begin(), additionalDates.end());
230
    std::sort(dates.begin(), dates.end());
231
    auto it = std::unique(dates.begin(), dates.end());
232
    QL_REQUIRE(it == dates.end(), "duplicate dates among alive instruments and additional dates");
233
234
    // build times vector
235
    times.clear();
236
    for (auto& date : dates)
237
        times.push_back(ts_->timeFromReference(date));
238
239
    // determine maxDate
240
    Date maxDate = dates.back();
241
    for (Size j = 0; j < numberHelpers_; ++j) {
242
        maxDate = std::max(ts_->instruments_[firstHelper_ + j]->latestRelevantDate(), maxDate);
243
    }
244
    ts_->maxDate_ = maxDate;
245
246
    // set initial guess only if the current curve cannot be used as guess
247
    if (!validCurve_ || ts_->data_.size() != dates.size()) {
248
        // ts_->data_[0] is the only relevant item,
249
        // but reasonable numbers might be needed for the whole data vector
250
        // because, e.g., of interpolation's early checks
251
        ts_->data_ = std::vector<Real>(dates.size(), Traits::initialValue(ts_));
252
        validCurve_ = false;
253
    }
254
    initialized_ = true;
255
}
256
257
template <class Curve> void GlobalBootstrap<Curve>::calculate() const {
258
259
    // we might have to call initialize even if the curve is initialized
260
    // and not moving, just because helpers might be date relative and change
261
    // with evaluation date change.
262
    // anyway it makes little sense to use date relative helpers with a
263
    // non-moving curve if the evaluation date changes
264
    if (!initialized_ || ts_->moving_)
265
        initialize();
266
267
    // setup helpers
268
    for (Size j = 0; j < numberHelpers_; ++j) {
269
        const ext::shared_ptr<typename Traits::helper> &helper = ts_->instruments_[firstHelper_ + j];
270
        // check for valid quote
271
        QL_REQUIRE(helper->quote()->isValid(), io::ordinal(j + 1)
272
                                                   << " instrument (maturity: " << helper->maturityDate()
273
                                                   << ", pillar: " << helper->pillarDate() << ") has an invalid quote");
274
        // don't try this at home!
275
        // This call creates helpers, and removes "const".
276
        // There is a significant interaction with observability.
277
        helper->setTermStructure(const_cast<Curve *>(ts_));
278
    }
279
280
    // setup additional helpers
281
    for (Size j = 0; j < numberAdditionalHelpers_; ++j) {
282
        const ext::shared_ptr<typename Traits::helper> &helper = additionalHelpers_[firstAdditionalHelper_ + j];
283
        QL_REQUIRE(helper->quote()->isValid(), io::ordinal(j + 1)
284
                                                   << " additional instrument (maturity: " << helper->maturityDate()
285
                                                   << ") has an invalid quote");
286
        helper->setTermStructure(const_cast<Curve *>(ts_));
287
    }
288
289
    // setup interpolation
290
    if (!validCurve_) {
291
        ts_->interpolation_ =
292
            ts_->interpolator_.interpolate(ts_->times_.begin(), ts_->times_.end(), ts_->data_.begin());
293
    }
294
295
    // Setup initial guess. We have guesses for the curve values first (numberPillars),
296
    // followed by guesses for the additional variables.
297
    const Size numberPillars = ts_->times_.size() - 1;
298
    Array additionalGuesses;
299
    if (additionalVariables_) {
300
        additionalGuesses = additionalVariables_->initialize(validCurve_);
301
    }
302
    Array guess(numberPillars + additionalGuesses.size());
303
    for (Size i = 0; i < numberPillars; ++i) {
304
        // just pass zero as the first alive helper, it's not used in the standard QL traits anyway
305
        // update ts_->data_ since Traits::guess() usually depends on previous values
306
        Traits::updateGuess(ts_->data_, Traits::guess(i + 1, ts_, validCurve_, 0), i + 1);
307
        guess[i] = Traits::transformInverse(ts_->data_[i + 1], i + 1, ts_);
308
    }
309
    std::copy(additionalGuesses.begin(), additionalGuesses.end(), guess.begin() + numberPillars);
310
311
    // setup cost function
312
    SimpleCostFunction cost([&](const Array& x) {
313
        // x has the same layout as guess above: the first numberPillars values go into
314
        // the curve, while the rest are new values for the additional variables.
315
        for (Size i = 0; i < numberPillars; ++i) {
316
            Traits::updateGuess(ts_->data_, Traits::transformDirect(x[i], i + 1, ts_), i + 1);
317
        }
318
        ts_->interpolation_.update();
319
        if (additionalVariables_) {
320
            additionalVariables_->update(Array(x.begin() + numberPillars, x.end()));
321
        }
322
323
        Array additionalErrors;
324
        if (additionalPenalties_) {
325
            additionalErrors = additionalPenalties_(ts_->times_, ts_->data_);
326
        }
327
        Array result(numberHelpers_ + additionalErrors.size());
328
        std::transform(ts_->instruments_.begin() + firstHelper_, ts_->instruments_.end(),
329
                       result.begin(),
330
                       [](const auto& helper) { return helper->quoteError(); });
331
        std::copy(additionalErrors.begin(), additionalErrors.end(),
332
                  result.begin() + numberHelpers_);
333
        return result;
334
    });
335
336
    // setup problem
337
    NoConstraint noConstraint;
338
    Problem problem(cost, noConstraint, guess);
339
340
    // run optimization
341
    EndCriteria::Type endType = optimizer_->minimize(problem, *endCriteria_);
342
343
    // check the end criteria
344
    QL_REQUIRE(EndCriteria::succeeded(endType),
345
               "global bootstrap failed to minimize to required accuracy: " << endType);
346
347
    // set valid flag
348
    validCurve_ = true;
349
}
350
351
} // namespace QuantLib
352
353
#endif