Coverage Report

Created: 2025-08-11 06:28

/src/quantlib/ql/termstructures/yield/fittedbonddiscountcurve.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) 2009 Ferdinando Ametrano
5
 Copyright (C) 2007 Allen Kuo
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/cashflows/cashflows.hpp>
22
#include <ql/math/optimization/constraint.hpp>
23
#include <ql/math/optimization/costfunction.hpp>
24
#include <ql/math/optimization/simplex.hpp>
25
#include <ql/pricingengines/bond/bondfunctions.hpp>
26
#include <ql/termstructures/yield/fittedbonddiscountcurve.hpp>
27
#include <ql/time/daycounters/simpledaycounter.hpp>
28
#include <ql/utilities/dataformatters.hpp>
29
#include <utility>
30
31
using std::vector;
32
33
namespace QuantLib {
34
35
    class FittedBondDiscountCurve::FittingMethod::FittingCost
36
        : public CostFunction {
37
        friend class FittedBondDiscountCurve::FittingMethod;
38
      public:
39
        explicit FittingCost(
40
                       FittedBondDiscountCurve::FittingMethod* fittingMethod);
41
        Real value(const Array& x) const override;
42
        Array values(const Array& x) const override;
43
44
      private:
45
        FittedBondDiscountCurve::FittingMethod* fittingMethod_;
46
    };
47
48
49
    FittedBondDiscountCurve::FittedBondDiscountCurve(
50
        Natural settlementDays,
51
        const Calendar& calendar,
52
        vector<ext::shared_ptr<BondHelper> > bondHelpers,
53
        const DayCounter& dayCounter,
54
        const FittingMethod& fittingMethod,
55
        Real accuracy,
56
        Size maxEvaluations,
57
        Array guess,
58
        Real simplexLambda,
59
        Size maxStationaryStateIterations)
60
0
    : YieldTermStructure(settlementDays, calendar, dayCounter), accuracy_(accuracy),
61
0
      maxEvaluations_(maxEvaluations), simplexLambda_(simplexLambda),
62
0
      maxStationaryStateIterations_(maxStationaryStateIterations), guessSolution_(std::move(guess)),
63
0
      bondHelpers_(std::move(bondHelpers)), fittingMethod_(fittingMethod) {
64
0
        fittingMethod_->curve_ = this;
65
0
        setup();
66
0
    }
Unexecuted instantiation: QuantLib::FittedBondDiscountCurve::FittedBondDiscountCurve(unsigned int, QuantLib::Calendar const&, std::__1::vector<boost::shared_ptr<QuantLib::BondHelper>, std::__1::allocator<boost::shared_ptr<QuantLib::BondHelper> > >, QuantLib::DayCounter const&, QuantLib::FittedBondDiscountCurve::FittingMethod const&, double, unsigned long, QuantLib::Array, double, unsigned long)
Unexecuted instantiation: QuantLib::FittedBondDiscountCurve::FittedBondDiscountCurve(unsigned int, QuantLib::Calendar const&, std::__1::vector<boost::shared_ptr<QuantLib::BondHelper>, std::__1::allocator<boost::shared_ptr<QuantLib::BondHelper> > >, QuantLib::DayCounter const&, QuantLib::FittedBondDiscountCurve::FittingMethod const&, double, unsigned long, QuantLib::Array, double, unsigned long)
67
68
69
    FittedBondDiscountCurve::FittedBondDiscountCurve(
70
        const Date& referenceDate,
71
        vector<ext::shared_ptr<BondHelper> > bondHelpers,
72
        const DayCounter& dayCounter,
73
        const FittingMethod& fittingMethod,
74
        Real accuracy,
75
        Size maxEvaluations,
76
        Array guess,
77
        Real simplexLambda,
78
        Size maxStationaryStateIterations)
79
0
    : YieldTermStructure(referenceDate, Calendar(), dayCounter), accuracy_(accuracy),
80
0
      maxEvaluations_(maxEvaluations), simplexLambda_(simplexLambda),
81
0
      maxStationaryStateIterations_(maxStationaryStateIterations), guessSolution_(std::move(guess)),
82
0
      bondHelpers_(std::move(bondHelpers)), fittingMethod_(fittingMethod) {
83
84
0
        fittingMethod_->curve_ = this;
85
0
        setup();
86
0
    }
Unexecuted instantiation: QuantLib::FittedBondDiscountCurve::FittedBondDiscountCurve(QuantLib::Date const&, std::__1::vector<boost::shared_ptr<QuantLib::BondHelper>, std::__1::allocator<boost::shared_ptr<QuantLib::BondHelper> > >, QuantLib::DayCounter const&, QuantLib::FittedBondDiscountCurve::FittingMethod const&, double, unsigned long, QuantLib::Array, double, unsigned long)
Unexecuted instantiation: QuantLib::FittedBondDiscountCurve::FittedBondDiscountCurve(QuantLib::Date const&, std::__1::vector<boost::shared_ptr<QuantLib::BondHelper>, std::__1::allocator<boost::shared_ptr<QuantLib::BondHelper> > >, QuantLib::DayCounter const&, QuantLib::FittedBondDiscountCurve::FittingMethod const&, double, unsigned long, QuantLib::Array, double, unsigned long)
87
88
    FittedBondDiscountCurve::FittedBondDiscountCurve(
89
                            Natural settlementDays,
90
                            const Calendar& calendar,
91
                            const FittingMethod& fittingMethod,
92
                            Array parameters,
93
                            Date maxDate,
94
                            const DayCounter& dayCounter)
95
0
    : YieldTermStructure(settlementDays, calendar, dayCounter), accuracy_(1e-10),
96
0
      maxEvaluations_(0), guessSolution_(std::move(parameters)),
97
0
      maxDate_(maxDate), fittingMethod_(fittingMethod) {
98
99
0
        fittingMethod_->curve_ = this;
100
0
        setup();
101
0
    }
Unexecuted instantiation: QuantLib::FittedBondDiscountCurve::FittedBondDiscountCurve(unsigned int, QuantLib::Calendar const&, QuantLib::FittedBondDiscountCurve::FittingMethod const&, QuantLib::Array, QuantLib::Date, QuantLib::DayCounter const&)
Unexecuted instantiation: QuantLib::FittedBondDiscountCurve::FittedBondDiscountCurve(unsigned int, QuantLib::Calendar const&, QuantLib::FittedBondDiscountCurve::FittingMethod const&, QuantLib::Array, QuantLib::Date, QuantLib::DayCounter const&)
102
103
    FittedBondDiscountCurve::FittedBondDiscountCurve(
104
                                const Date& referenceDate,
105
                                const FittingMethod& fittingMethod,
106
                                Array parameters,
107
                                Date maxDate,
108
                                const DayCounter& dayCounter)
109
0
    : YieldTermStructure(referenceDate, Calendar(), dayCounter), accuracy_(1e-10),
110
0
      maxEvaluations_(0), guessSolution_(std::move(parameters)),
111
0
      maxDate_(maxDate), fittingMethod_(fittingMethod) {
112
113
0
        fittingMethod_->curve_ = this;
114
0
        setup();
115
0
    }
Unexecuted instantiation: QuantLib::FittedBondDiscountCurve::FittedBondDiscountCurve(QuantLib::Date const&, QuantLib::FittedBondDiscountCurve::FittingMethod const&, QuantLib::Array, QuantLib::Date, QuantLib::DayCounter const&)
Unexecuted instantiation: QuantLib::FittedBondDiscountCurve::FittedBondDiscountCurve(QuantLib::Date const&, QuantLib::FittedBondDiscountCurve::FittingMethod const&, QuantLib::Array, QuantLib::Date, QuantLib::DayCounter const&)
116
117
118
0
    void FittedBondDiscountCurve::resetGuess(const Array& guess) {
119
0
        QL_REQUIRE(guess.empty() || guess.size() == fittingMethod_->size(), "guess is of wrong size");
120
0
        guessSolution_ = guess;
121
0
        update();
122
0
    }
123
124
125
0
    void FittedBondDiscountCurve::performCalculations() const {
126
127
0
        if (maxEvaluations_!= 0) {
128
            // we need to fit, so we require helpers
129
0
            QL_REQUIRE(!bondHelpers_.empty(), "no bond helpers given");
130
0
        }
131
132
0
        if (maxEvaluations_ == 0) {
133
            // no fit, but we need either an explicit max date or
134
            // helpers from which to deduce it
135
0
            QL_REQUIRE(maxDate_ != Date() || !bondHelpers_.empty(),
136
0
                       "no bond helpers or max date given");
137
0
        }
138
139
0
        if (!bondHelpers_.empty()) {
140
0
            maxDate_ = Date::minDate();
141
0
            Date refDate = referenceDate();
142
143
            // double check bond quotes still valid and/or instruments not expired
144
0
            for (Size i=0; i<bondHelpers_.size(); ++i) {
145
0
                ext::shared_ptr<Bond> bond = bondHelpers_[i]->bond();
146
0
                QL_REQUIRE(bondHelpers_[i]->quote()->isValid(),
147
0
                           io::ordinal(i+1) << " bond (maturity: " <<
148
0
                           bond->maturityDate() << ") has an invalid price quote");
149
0
                Date bondSettlement = bond->settlementDate();
150
0
                QL_REQUIRE(bondSettlement>=refDate,
151
0
                           io::ordinal(i+1) << " bond settlemente date (" <<
152
0
                           bondSettlement << ") before curve reference date (" <<
153
0
                           refDate << ")");
154
0
                QL_REQUIRE(BondFunctions::isTradable(*bond, bondSettlement),
155
0
                           io::ordinal(i+1) << " bond non tradable at " <<
156
0
                           bondSettlement << " settlement date (maturity"
157
0
                           " being " << bond->maturityDate() << ")");
158
0
                maxDate_ = std::max(maxDate_, bondHelpers_[i]->pillarDate());
159
0
                bondHelpers_[i]->setTermStructure(
160
0
                                                  const_cast<FittedBondDiscountCurve*>(this));
161
0
            }
162
0
        }
163
164
0
        fittingMethod_->init();
165
0
        fittingMethod_->calculate();
166
0
    }
167
168
169
    FittedBondDiscountCurve::FittingMethod::FittingMethod(
170
        bool constrainAtZero,
171
        const Array& weights,
172
        ext::shared_ptr<OptimizationMethod> optimizationMethod,
173
        Array l2,
174
        const Real minCutoffTime,
175
        const Real maxCutoffTime,
176
        Constraint constraint)
177
0
    : constrainAtZero_(constrainAtZero), weights_(weights), l2_(std::move(l2)),
178
0
      calculateWeights_(weights.empty()), optimizationMethod_(std::move(optimizationMethod)),
179
0
      constraint_(std::move(constraint)),
180
0
      minCutoffTime_(minCutoffTime), maxCutoffTime_(maxCutoffTime) {
181
0
        if (constraint_.empty())
182
0
            constraint_ = NoConstraint();
183
0
    }
184
185
0
    void FittedBondDiscountCurve::FittingMethod::init() {
186
187
0
        if (curve_->maxEvaluations_ == 0)
188
0
            return; // we can skip the rest
189
190
        // yield conventions
191
0
        DayCounter yieldDC = curve_->dayCounter();
192
0
        Compounding yieldComp = Compounded;
193
0
        Frequency yieldFreq = Annual;
194
195
0
        Size n = curve_->bondHelpers_.size();
196
0
        costFunction_ = ext::make_shared<FittingCost>(this);
197
198
0
        for (auto& bondHelper : curve_->bondHelpers_) {
199
0
            bondHelper->setTermStructure(curve_);
200
0
        }
201
202
0
        if (calculateWeights_) {
203
0
            if (weights_.empty())
204
0
                weights_ = Array(n);
205
206
0
            Real squaredSum = 0.0;
207
0
            for (Size i=0; i<curve_->bondHelpers_.size(); ++i) {
208
0
                ext::shared_ptr<Bond> bond = curve_->bondHelpers_[i]->bond();
209
210
0
                Real amount = curve_->bondHelpers_[i]->quote()->value();
211
0
                Bond::Price price(amount, curve_->bondHelpers_[i]->priceType());
212
213
0
                Date bondSettlement = bond->settlementDate();
214
0
                Rate ytm = BondFunctions::yield(*bond, price,
215
0
                                                yieldDC, yieldComp, yieldFreq,
216
0
                                                bondSettlement);
217
218
0
                Time dur = BondFunctions::duration(*bond, ytm,
219
0
                                                   yieldDC, yieldComp, yieldFreq,
220
0
                                                   Duration::Modified,
221
0
                                                   bondSettlement);
222
0
                weights_[i] = 1.0/dur;
223
0
                squaredSum += weights_[i]*weights_[i];
224
0
            }
225
0
            weights_ /= std::sqrt(squaredSum);
226
0
        }
227
228
0
        QL_REQUIRE(weights_.size() == n,
229
0
                   "Given weights do not cover all boostrapping helpers");
230
231
0
        if (!l2_.empty()) {
232
0
            QL_REQUIRE(l2_.size() == size(),
233
0
                       "Given penalty factors do not cover all parameters");
234
235
0
            QL_REQUIRE(!curve_->guessSolution_.empty(), "L2 penalty requires a guess");
236
0
        }
237
0
    }
238
239
0
    void FittedBondDiscountCurve::FittingMethod::calculate() {
240
241
0
        if (curve_->maxEvaluations_ == 0)
242
0
        {
243
            // Don't calculate, simply use the given parameters to
244
            // provide a fitted curve.  This turns the instance into
245
            // an evaluator of the parametric curve, for example
246
            // allowing to use the parameters for a credit spread
247
            // curve calculated with bonds in one currency to be
248
            // coupled to a discount curve in another currency.
249
250
0
            QL_REQUIRE(curve_->guessSolution_.size() == size(),
251
0
                       "wrong number of parameters");
252
253
0
            solution_ = curve_->guessSolution_;
254
255
0
            numberOfIterations_ = 0;
256
0
            costValue_ = Null<Real>();
257
0
            errorCode_ = EndCriteria::None;
258
259
0
            return;
260
0
        }
261
262
0
        FittingCost& costFunction = *costFunction_;
263
264
        // start with the guess solution, if it exists
265
0
        Array x(size(), 0.0);
266
0
        if (!curve_->guessSolution_.empty()) {
267
0
            QL_REQUIRE(curve_->guessSolution_.size() == size(), "wrong size for guess");
268
0
            x = curve_->guessSolution_;
269
0
        }
270
271
        // workaround for backwards compatibility
272
0
        ext::shared_ptr<OptimizationMethod> optimization = optimizationMethod_;
273
0
        if (!optimization) {
274
0
            optimization = ext::make_shared<Simplex>(curve_->simplexLambda_);
275
0
        }
276
0
        Problem problem(costFunction, constraint_, x);
277
278
0
        Real rootEpsilon = curve_->accuracy_;
279
0
        Real functionEpsilon =  curve_->accuracy_;
280
0
        Real gradientNormEpsilon = curve_->accuracy_;
281
282
0
        EndCriteria endCriteria(curve_->maxEvaluations_,
283
0
                                curve_->maxStationaryStateIterations_,
284
0
                                rootEpsilon,
285
0
                                functionEpsilon,
286
0
                                gradientNormEpsilon);
287
288
0
        errorCode_ = optimization->minimize(problem,endCriteria);
289
0
        solution_ = problem.currentValue();
290
291
0
        numberOfIterations_ = problem.functionEvaluation();
292
0
        costValue_ = problem.functionValue();
293
294
        // save the results as the guess solution, in case of recalculation
295
0
        curve_->guessSolution_ = solution_;
296
0
    }
297
298
299
    FittedBondDiscountCurve::FittingMethod::FittingCost::FittingCost(
300
                        FittedBondDiscountCurve::FittingMethod* fittingMethod)
301
0
    : fittingMethod_(fittingMethod) {}
302
303
304
    Real FittedBondDiscountCurve::FittingMethod::FittingCost::value(
305
0
                                                       const Array& x) const {
306
0
        Real squaredError = 0.0;
307
0
        Array vals = values(x);
308
0
        for (Real val : vals) {
309
0
            squaredError += val;
310
0
        }
311
0
        return squaredError;
312
0
    }
313
314
0
    Array FittedBondDiscountCurve::FittingMethod::FittingCost::values(const Array &x) const {
315
0
        Size n = fittingMethod_->curve_->bondHelpers_.size();
316
0
        Size N = fittingMethod_->l2_.size();
317
318
        // set solution so that fittingMethod_->curve_ represents the current trial
319
        // the final solution will be set in FittingMethod::calculate() later on
320
0
        fittingMethod_->solution_ = x;
321
322
0
        Array values(n + N);
323
0
        for (Size i=0; i<n; ++i) {
324
0
            ext::shared_ptr<BondHelper> helper = fittingMethod_->curve_->bondHelpers_[i];
325
0
            Real error = helper->impliedQuote() - helper->quote()->value();
326
0
            Real weightedError = fittingMethod_->weights_[i] * error;
327
0
            values[i] = weightedError * weightedError;
328
0
        }
329
330
0
        if (N != 0) {
331
0
            for (Size i = 0; i < N; ++i) {
332
0
                Real error = x[i] - fittingMethod_->curve_->guessSolution_[i];
333
0
                values[i + n] = fittingMethod_->l2_[i] * error * error;
334
0
            }
335
0
        }
336
0
        return values;
337
0
    }
338
339
}