/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> × = 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 |