• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lballabio / QuantLib / 16835272840

08 Aug 2025 04:20PM UTC coverage: 73.869% (+0.03%) from 73.843%
16835272840

Pull #2249

github

web-flow
Merge 2904a5015 into e2c40014f
Pull Request #2249: Add `cmake_runners-latest-matrix.yml` workflow

56680 of 76730 relevant lines covered (73.87%)

8799355.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.55
/ql/termstructures/iterativebootstrap.hpp
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2

3
/*
4
 Copyright (C) 2008, 2011, 2015 Ferdinando Ametrano
5
 Copyright (C) 2007 Chris Kenyon
6
 Copyright (C) 2007 StatPro Italia srl
7
 Copyright (C) 2015 Paolo Mazzocchi
8

9
 This file is part of QuantLib, a free-software/open-source library
10
 for financial quantitative analysts and developers - http://quantlib.org/
11

12
 QuantLib is free software: you can redistribute it and/or modify it
13
 under the terms of the QuantLib license.  You should have received a
14
 copy of the license along with this program; if not, please email
15
 <quantlib-dev@lists.sf.net>. The license is also available online at
16
 <http://quantlib.org/license.shtml>.
17

18
 This program is distributed in the hope that it will be useful, but WITHOUT
19
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
20
 FOR A PARTICULAR PURPOSE.  See the license for more details.
21
*/
22

23
/*! \file iterativebootstrap.hpp
24
    \brief universal piecewise-term-structure boostrapper.
25
*/
26

27
#ifndef quantlib_iterative_bootstrap_hpp
28
#define quantlib_iterative_bootstrap_hpp
29

30
#include <ql/termstructures/bootstraphelper.hpp>
31
#include <ql/math/interpolations/linearinterpolation.hpp>
32
#include <ql/math/solvers1d/finitedifferencenewtonsafe.hpp>
33
#include <ql/math/solvers1d/brent.hpp>
34
#include <ql/utilities/dataformatters.hpp>
35

36
namespace QuantLib {
37

38
namespace detail {
39

40
    /*! If \c dontThrow is \c true in IterativeBootstrap and on a given pillar the bootstrap fails when
41
        searching for a helper root between \c xMin and \c xMax, we use this function to return the value that
42
        gives the minimum absolute helper error in the interval between \c xMin and \c xMax inclusive.
43
    */
44
    template <class Fn>
45
    Real dontThrowFallback(const Fn& error, Real xMin, Real xMax, Size steps) {
4✔
46

47
        QL_REQUIRE(xMin < xMax, "Expected xMin to be less than xMax");
4✔
48

49
        // Set the initial value of the result to xMin and store the absolute bootstrap error at xMin
50
        Real result = xMin;
51
        Real absError = std::abs(error(xMin));
4✔
52
        Real minError = absError;
53

54
        // Step out to xMax
55
        Real stepSize = (xMax - xMin) / steps;
4✔
56
        for (Size i = 0; i < steps; i++) {
12✔
57

58
            // Get absolute bootstrap error at updated x value
59
            xMin += stepSize;
8✔
60
            absError = std::abs(error(xMin));
8✔
61

62
            // If this absolute bootstrap error is less than the minimum, update result and minError
63
            if (absError < minError) {
8✔
64
                result = xMin;
65
                minError = absError;
66
            }
67
        }
68

69
        return result;
4✔
70
    }
71

72
}
73

74
    //! Universal piecewise-term-structure boostrapper.
75
    template <class Curve>
76
    class IterativeBootstrap {
77
        typedef typename Curve::traits_type Traits;
78
        typedef typename Curve::interpolator_type Interpolator;
79
      public:
80
        /*! Constructor
81
            \param accuracy       Accuracy for the bootstrap stopping criterion. If it is set to
82
                                  \c Null<Real>(), its value is taken from the termstructure's accuracy.
83
            \param minValue       Allow to override the initial minimum value coming from traits.
84
            \param maxValue       Allow to override the initial maximum value coming from traits.
85
            \param maxAttempts    Number of attempts on each iteration. A number greater than 1 implies retries.
86
            \param maxFactor      Factor for max value retry on each iteration if there is a failure.
87
            \param minFactor      Factor for min value retry on each iteration if there is a failure.
88
            \param dontThrow      If set to \c true, the bootstrap doesn't throw and returns a <em>fall back</em>
89
                                  result.
90
            \param dontThrowSteps If \p dontThrow is \c true, this gives the number of steps to use when searching
91
                                  for a fallback curve pillar value that gives the minimum bootstrap helper error.
92
        */
93
        IterativeBootstrap(Real accuracy = Null<Real>(),
94
                           Real minValue = Null<Real>(),
95
                           Real maxValue = Null<Real>(),
96
                           Size maxAttempts = 1,
97
                           Real maxFactor = 2.0,
98
                           Real minFactor = 2.0,
99
                           bool dontThrow = false,
100
                           Size dontThrowSteps = 10,
101
                           Size maxEvaluations = MAX_FUNCTION_EVALUATIONS);
102
        void setup(Curve* ts);
103
        void calculate() const;
104
      private:
105
        void initialize() const;
106
        Real accuracy_;
107
        Real minValue_, maxValue_;
108
        Size maxAttempts_;
109
        Real maxFactor_;
110
        Real minFactor_;
111
        bool dontThrow_;
112
        Size dontThrowSteps_;
113
        Curve* ts_;
114
        Size n_ = 0;
115
        Brent firstSolver_;
116
        FiniteDifferenceNewtonSafe solver_;
117
        mutable bool initialized_ = false, validCurve_ = false, loopRequired_;
118
        mutable Size firstAliveHelper_ = 0, alive_ = 0;
119
    };
120

121

122
    // template definitions
123

124
    template <class Curve>
125
    IterativeBootstrap<Curve>::IterativeBootstrap(Real accuracy,
226✔
126
                                                  Real minValue,
127
                                                  Real maxValue,
128
                                                  Size maxAttempts,
129
                                                  Real maxFactor,
130
                                                  Real minFactor,
131
                                                  bool dontThrow,
132
                                                  Size dontThrowSteps,
133
                                                  Size maxEvaluations)
134
    : accuracy_(accuracy), minValue_(minValue), maxValue_(maxValue), maxAttempts_(maxAttempts),
226✔
135
      maxFactor_(maxFactor), minFactor_(minFactor), dontThrow_(dontThrow),
226✔
136
      dontThrowSteps_(dontThrowSteps), ts_(nullptr), loopRequired_(Interpolator::global) {
226✔
137
        QL_REQUIRE(maxFactor_ >= 1.0, "Expected that maxFactor would be at least 1.0 but got " << maxFactor_);
226✔
138
        QL_REQUIRE(minFactor_ >= 1.0, "Expected that minFactor would be at least 1.0 but got " << minFactor_);
226✔
139
        firstSolver_.setMaxEvaluations(maxEvaluations);
140
        solver_.setMaxEvaluations(maxEvaluations);
141
    }
226✔
142

143
    template <class Curve>
144
    void IterativeBootstrap<Curve>::setup(Curve* ts) {
227✔
145
        ts_ = ts;
227✔
146
        n_ = ts_->instruments_.size();
227✔
147
        QL_REQUIRE(n_ > 0, "no bootstrap helpers given");
227✔
148
        for (Size j=0; j<n_; ++j)
3,606✔
149
            ts_->registerWithObservables(ts_->instruments_[j]);
6,758✔
150

151
        // do not initialize yet: instruments could be invalid here
152
        // but valid later when bootstrapping is actually required
153
    }
227✔
154

155
    template <class Curve>
156
    void IterativeBootstrap<Curve>::initialize() const {
240✔
157
        // ensure helpers are sorted
158
        std::sort(ts_->instruments_.begin(), ts_->instruments_.end(),
240✔
159
                  detail::BootstrapHelperSorter());
160
        // skip expired helpers
161
        Date firstDate = Traits::initialDate(ts_);
240✔
162
        QL_REQUIRE(ts_->instruments_[n_-1]->pillarDate()>firstDate,
240✔
163
                   "all instruments expired");
164
        firstAliveHelper_ = 0;
240✔
165
        while (ts_->instruments_[firstAliveHelper_]->pillarDate() <= firstDate)
240✔
166
            ++firstAliveHelper_;
×
167
        alive_ = n_-firstAliveHelper_;
240✔
168
        Size nodes = alive_+1;
240✔
169
        QL_REQUIRE(nodes >= Interpolator::requiredPoints,
240✔
170
                   "not enough alive instruments: " << alive_ <<
171
                   " provided, " << Interpolator::requiredPoints-1 <<
172
                   " required");
173

174
        // calculate dates and times
175
        std::vector<Date>& dates = ts_->dates_;
240✔
176
        std::vector<Time>& times = ts_->times_;
240✔
177
        dates.resize(alive_+1);
240✔
178
        times.resize(alive_+1);
240✔
179
        dates[0] = firstDate;
240✔
180
        times[0] = ts_->timeFromReference(dates[0]);
240✔
181

182
        Date maxDate = firstDate;
240✔
183
        // pillar counter: i
184
        // helper counter: j
185
        for (Size i=1, j=firstAliveHelper_; j<n_; ++i, ++j) {
4,003✔
186
            const auto& helper = ts_->instruments_[j];
3,763✔
187
            dates[i] = helper->pillarDate();
3,763✔
188
            times[i] = ts_->timeFromReference(dates[i]);
3,763✔
189
            // check for duplicated pillars
190
            QL_REQUIRE(dates[i-1]!=dates[i],
3,763✔
191
                       "more than one instrument with pillar " << dates[i]);
192

193
            Date latestRelevantDate = helper->latestRelevantDate();
3,763✔
194
            // check that the helper is really extending the curve, i.e. that
195
            // pillar-sorted helpers are also sorted by latestRelevantDate
196
            QL_REQUIRE(latestRelevantDate > maxDate,
3,763✔
197
                       io::ordinal(j+1) << " instrument (pillar: " <<
198
                       dates[i] << ") has latestRelevantDate (" <<
199
                       latestRelevantDate << ") before or equal to "
200
                       "previous instrument's latestRelevantDate (" <<
201
                       maxDate << ")");
202
            maxDate = std::max(dates[i], latestRelevantDate);
3,763✔
203

204
            // when a pillar date is before the last relevant date the
205
            // convergence loop is required even if the Interpolator is local
206
            if (dates[i] < latestRelevantDate)
3,763✔
207
                loopRequired_ = true;
22✔
208
        }
209
        ts_->maxDate_ = maxDate;
240✔
210

211
        // set initial guess only if the current curve cannot be used as guess
212
        if (!validCurve_ || ts_->data_.size()!=alive_+1) {
240✔
213
            // ts_->data_[0] is the only relevant item,
214
            // but reasonable numbers might be needed for the whole data vector
215
            // because, e.g., of interpolation's early checks
216
            ts_->data_ = std::vector<Real>(alive_+1, Traits::initialValue(ts_));
196✔
217
            validCurve_ = false;
196✔
218
        }
219
        initialized_ = true;
240✔
220
    }
240✔
221

222
    template <class Curve>
223
    void IterativeBootstrap<Curve>::calculate() const {
250✔
224

225
        // we might have to call initialize even if the curve is initialized
226
        // and not moving, just because helpers might be date relative and change
227
        // with evaluation date change.
228
        // anyway it makes little sense to use date relative helpers with a
229
        // non-moving curve if the evaluation date changes
230
        if (!initialized_ || ts_->moving_)
250✔
231
            initialize();
240✔
232

233
        // setup helpers
234
        for (Size j=firstAliveHelper_; j<n_; ++j) {
4,155✔
235
            const auto& helper = ts_->instruments_[j];
3,905✔
236
            // check for valid quote
237
            QL_REQUIRE(helper->quote()->isValid(),
3,905✔
238
                       io::ordinal(j + 1) << " instrument (maturity: " <<
239
                       helper->maturityDate() << ", pillar: " <<
240
                       helper->pillarDate() << ") has an invalid quote");
241
            // don't try this at home!
242
            // This call creates helpers, and removes "const".
243
            // There is a significant interaction with observability.
244
            helper->setTermStructure(const_cast<Curve*>(ts_));
3,905✔
245
        }
246

247
        const std::vector<Time>& times = ts_->times_;
250✔
248
        const std::vector<Real>& data = ts_->data_;
249
        Real accuracy = accuracy_ != Null<Real>() ? accuracy_ : ts_->accuracy_;
250✔
250

251
        Size maxIterations = Traits::maxIterations()-1;
252

253
        // there might be a valid curve state to use as guess
254
        bool validData = validCurve_;
250✔
255
        std::vector<Real> previousData;
256

257
        for (Size iteration=0; ; ++iteration) {
131✔
258
            if (loopRequired_ && validData)
381✔
259
                previousData = ts_->data_;
132✔
260

261
            // Store min value and max value at each pillar so that we can expand search if necessary.
262
            std::vector<Real> minValues(alive_+1, Null<Real>());
384✔
263
            std::vector<Real> maxValues(alive_+1, Null<Real>());
384✔
264
            std::vector<Size> attempts(alive_+1, 1);
384✔
265

266
            for (Size i=1, j=firstAliveHelper_; j<n_; ++i, ++j) { // pillar loop
5,614✔
267

268
                // shorter aliases for readability and to avoid duplication
269
                Real& min = minValues[i];
5,237✔
270
                Real& max = maxValues[i];
271

272
                // bracket root and calculate guess
273
                if (min == Null<Real>()) {
5,237✔
274
                    // First attempt; we take min and max either from
275
                    // explicit constructor parameter or from traits
276
                    min = (minValue_ != Null<Real>() ? minValue_ :
5,209✔
277
                           Traits::minValueAfter(i, ts_, validData, firstAliveHelper_));
5,209✔
278
                    max = (maxValue_ != Null<Real>() ? maxValue_ :
10,414✔
279
                           Traits::maxValueAfter(i, ts_, validData, firstAliveHelper_));
5,205✔
280
                } else {
281
                    // Extending a previous attempt.  A negative min
282
                    // is enlarged; a positive one is shrunk towards 0.
283
                    min = (min < 0.0 ? Real(min * minFactor_) : Real(min / minFactor_));
28✔
284
                    // The opposite holds for the max.
285
                    max = (max > 0.0 ? Real(max * maxFactor_) : Real(max / maxFactor_));
28✔
286
                }
287
                Real guess = Traits::guess(i, ts_, validData, firstAliveHelper_);
5,237✔
288

289
                // adjust guess if needed
290
                if (guess >= max)
5,237✔
291
                    guess = max - (max - min) / 5.0;
×
292
                else if (guess <= min)
5,237✔
293
                    guess = min + (max - min) / 5.0;
10✔
294

295
                // extend interpolation if needed
296
                if (!validData) {
5,237✔
297
                    try { // extend interpolation a point at a time
298
                          // including the pillar to be boostrapped
299
                        ts_->interpolation_ = ts_->interpolator_.interpolate(
2,885✔
300
                            times.begin(), times.begin()+i+1, data.begin());
3,097✔
301
                    } catch (...) {
×
302
                        if (!Interpolator::global)
303
                            throw; // no chance to fix it in a later iteration
×
304

305
                        // otherwise use Linear while the target
306
                        // interpolation is not usable yet
307
                        ts_->interpolation_ = Linear().interpolate(
×
308
                            times.begin(), times.begin()+i+1, data.begin());
×
309
                    }
310
                    ts_->interpolation_.update();
2,885✔
311
                }
312

313
                const auto& helper = ts_->instruments_[j];
5,237✔
314
                auto error = [&](Rate guess) {
89,261✔
315
                    Traits::updateGuess(ts_->data_, guess, i);
37,326✔
316
                    ts_->interpolation_.update();
37,326✔
317
                    return helper->quoteError();
37,326✔
318
                };
319
                try {
320
                    if (validData)
5,237✔
321
                        solver_.solve(error, accuracy, guess, min, max);
2,352✔
322
                    else
323
                        firstSolver_.solve(error, accuracy, guess, min, max);
2,885✔
324
                } catch (std::exception &e) {
44✔
325
                    if (validCurve_) {
36✔
326
                        // the previous curve state might have been a
327
                        // bad guess, so we retry without using it.
328
                        // This would be tricky to do here (we're
329
                        // inside multiple nested for loops, we need
330
                        // to re-initialize...), so we invalidate the
331
                        // curve, make a recursive call and then exit.
332
                        validCurve_ = initialized_ = false;
1✔
333
                        calculate();
1✔
334
                        return;
335
                    }
336

337
                    // If we have more attempts left on this iteration, try again. Note that the max and min
338
                    // bounds will be widened on the retry.
339
                    if (attempts[i] < maxAttempts_) {
35✔
340
                        attempts[i]++;
28✔
341
                        i--;
28✔
342
                        j--;
28✔
343
                        continue;
344
                    }
345

346
                    if (dontThrow_) {
7✔
347
                        // Use the fallback value
348
                        ts_->data_[i] = detail::dontThrowFallback(error, min, max, dontThrowSteps_);
4✔
349

350
                        // Remember to update the interpolation. If we don't and we are on the last "i", we will still
351
                        // have the last attempted value in the solver being used in ts_->interpolation_.
352
                        ts_->interpolation_.update();
4✔
353
                    } else {
354
                        QL_FAIL(io::ordinal(iteration + 1) << " iteration: failed "
9✔
355
                                "at " << io::ordinal(i) << " alive instrument, "
356
                                "pillar " << helper->pillarDate() <<
357
                                ", maturity " << helper->maturityDate() <<
358
                                ", reference date " << ts_->dates_[0] <<
359
                                ": " << e.what());
360
                    }
361
                }
362
            }
363

364
            if (!loopRequired_)
377✔
365
                 break;
366

367
            // exit condition
368
            Real change = 0;
154✔
369
            if (validData) {
154✔
370
                for (Size i=1; i<=alive_; ++i)
1,489✔
371
                    change = std::max(change, std::fabs(data[i]-previousData[i]));
1,584✔
372
                if (change<=accuracy)  // convergence reached
132✔
373
                    break;
374
            }
375

376
            // If we hit the max number of iterations and dontThrow is true, just use what we have
377
            if (iteration == maxIterations) {
131✔
378
                if (dontThrow_) {
×
379
                    break;
380
                } else {
381
                    QL_FAIL("convergence not reached after " << iteration <<
×
382
                            " iterations; last improvement " << change <<
383
                            ", required accuracy " << accuracy);
384
                }
385
            }
386

387
            validData = true;
388
        }
389
        validCurve_ = true;
246✔
390
    }
391

392
}
393

394
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc