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

lballabio / QuantLib / 18902330216

29 Oct 2025 08:56AM UTC coverage: 74.321% (+0.4%) from 73.914%
18902330216

Pull #2344

github

web-flow
Merge a8095fd90 into d823f4ecb
Pull Request #2344: add multicurve bootstrap

100 of 104 new or added lines in 8 files covered. (96.15%)

216 existing lines in 13 files now uncovered.

57073 of 76793 relevant lines covered (74.32%)

8779123.65 hits per line

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

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

3
/*
4
 Copyright (C) 2020, 2025 Marcin Rybacki
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
 <https://www.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 ultimateforwardtermstructure.hpp
21
    \brief Ultimate Forward Rate term structure
22
*/
23

24
#ifndef quantlib_ultimate_forward_term_structure_hpp
25
#define quantlib_ultimate_forward_term_structure_hpp
26

27
#include <ql/math/rounding.hpp>
28
#include <ql/optional.hpp>
29
#include <ql/quote.hpp>
30
#include <ql/termstructures/yield/zeroyieldstructure.hpp>
31
#include <utility>
32

33
namespace QuantLib {
34

35
    //! Ultimate forward term structure
36

37
    /*! Dutch regulatory term structure for pension funds with a
38
        parametrized extrapolation mechanism designed for
39
        discounting long dated liabilities.
40

41
        Relevant documentation can be found on the Dutch Central
42
        Bank website:
43

44
        FTK term structure documentation (Financieel toetsingskader):
45
        https://www.dnb.nl/media/4lmprzrk/vaststelling_methode_rentetermijnstructuur_ftk.pdf
46

47
        UFR 2013-2019 term structure documentation:
48
        https://www.dnb.nl/media/0vmbxaf4/methodologie-dnb.pdf
49

50
        UFR 2023 term structure documentation (p.46):
51
        https://www.tweedekamer.nl/downloads/document?id=2022D50944
52

53
        Optionally, computed zero rates may be rounded.
54
        The specified number of decimal places will affect the rate
55
        in decimal format; for example, rounding a rate of 1.5555%
56
        to 5 decimal places results in 0.015555 becoming 0.01556, or 1.556%.
57

58
        This term structure will remain linked to the original
59
        structure, i.e., any changes in the latter will be
60
        reflected in this structure as well.
61

62
        \ingroup yieldtermstructures
63

64
        \test
65
        - the correctness of the returned zero rates is tested by
66
          checking them against reference values obtained
67
          from the official source.
68
        - extrapolated forward is validated.
69
        - rates on the cut-off point are checked against those
70
          implied by the base curve.
71
        - inspectors are tested against the base curve.
72
        - incorrect input for cut-off point should raise an exception.
73
        - observability against changes in the underlying term
74
          structure and the additional components is checked.
75
        - rounding of output rate with predefined compounding.
76
    */
77

78
    class UltimateForwardTermStructure : public ZeroYieldStructure {
79
      public:
80
        UltimateForwardTermStructure(Handle<YieldTermStructure>,
81
                                     Handle<Quote> lastLiquidForwardRate,
82
                                     Handle<Quote> ultimateForwardRate,
83
                                     const Period& firstSmoothingPoint,
84
                                     Real alpha,
85
                                     const ext::optional<Integer>& roundingDigits = ext::nullopt,
86
                                     Compounding compounding = Compounded,
87
                                     Frequency frequency = Annual);
88
        //! \name YieldTermStructure interface
89
        //@{
90
        DayCounter dayCounter() const override;
91
        Calendar calendar() const override;
92
        Natural settlementDays() const override;
93
        const Date& referenceDate() const override;
94
        Date maxDate() const override;
95
        //@}
96
        //! \name Observer interface
97
        //@{
98
        void update() override;
99
        //@}
100
      protected:
101
        //! returns the UFR extended zero yield rate
102
        Rate zeroYieldImpl(Time) const override;
103
        //@}
104
      private:
105
        //! applies rounding on zero rate with required compounding
106
        Rate applyRounding(Rate r, Time t) const;
107
        //@}
108

109
        Handle<YieldTermStructure> originalCurve_;
110
        Handle<Quote> llfr_;
111
        Handle<Quote> ufr_;
112
        Period fsp_;
113
        Real alpha_;
114
        ext::optional<Integer> roundingDigits_;
115
        Compounding compounding_;
116
        Frequency frequency_;
117
    };
118

119
    // inline definitions
120

121
    inline UltimateForwardTermStructure::UltimateForwardTermStructure(
9✔
122
        Handle<YieldTermStructure> h,
123
        Handle<Quote> lastLiquidForwardRate,
124
        Handle<Quote> ultimateForwardRate,
125
        const Period& firstSmoothingPoint,
126
        Real alpha,
127
        const ext::optional<Integer>& roundingDigits,
128
        Compounding compounding,
129
        Frequency frequency)
9✔
130
    : originalCurve_(std::move(h)), llfr_(std::move(lastLiquidForwardRate)),
9✔
131
      ufr_(std::move(ultimateForwardRate)), fsp_(firstSmoothingPoint), alpha_(alpha),
9✔
132
      roundingDigits_(roundingDigits), compounding_(compounding), frequency_(frequency) {
20✔
133
        QL_REQUIRE(fsp_.length() > 0,
13✔
134
                   "first smoothing point must be a period with positive length");
135
        if (!originalCurve_.empty())
7✔
136
            enableExtrapolation(originalCurve_->allowsExtrapolation());
7✔
137
        registerWith(originalCurve_);
14✔
138
        registerWith(llfr_);
14✔
139
        registerWith(ufr_);
9✔
140
    }
11✔
141

142
    inline DayCounter UltimateForwardTermStructure::dayCounter() const {
73✔
143
        return originalCurve_->dayCounter();
73✔
144
    }
145

UNCOV
146
    inline Calendar UltimateForwardTermStructure::calendar() const {
×
UNCOV
147
        return originalCurve_->calendar();
×
148
    }
149

UNCOV
150
    inline Natural UltimateForwardTermStructure::settlementDays() const {
×
UNCOV
151
        return originalCurve_->settlementDays();
×
152
    }
153

154
    inline const Date& UltimateForwardTermStructure::referenceDate() const {
126✔
155
        return originalCurve_->referenceDate();
126✔
156
    }
157

158
    inline Date UltimateForwardTermStructure::maxDate() const { return Date::maxDate(); }
2✔
159

160
    inline void UltimateForwardTermStructure::update() {
2✔
161
        if (!originalCurve_.empty()) {
2✔
162
            YieldTermStructure::update();
2✔
163
            enableExtrapolation(originalCurve_->allowsExtrapolation());
2✔
164
        } else {
165
            /* The implementation inherited from YieldTermStructure
166
               asks for our reference date, which we don't have since
167
               the original curve is still not set. Therefore, we skip
168
               over that and just call the base-class behavior. */
169
            // NOLINTNEXTLINE(bugprone-parent-virtual-call)
UNCOV
170
            TermStructure::update();
×
171
        }
172
    }
2✔
173

174
    inline Rate UltimateForwardTermStructure::applyRounding(Rate r, Time t) const {
49✔
175
        if (!roundingDigits_.has_value()) {
49✔
176
            return r;
177
        }
178
        // Input rate is continuously compounded by definition.
179
        // Hence, in case this is also the selected compounding method for rounding,
180
        // it is not required to calculate equivalent rates, and rounding
181
        // may be applied directly.
182
        Rate equivalentRate = compounding_ == Continuous ?
20✔
183
                                  r :
184
                                  InterestRate(r, dayCounter(), Continuous, NoFrequency)
30✔
185
                                      .equivalentRate(compounding_, frequency_, t);
40✔
186
        Rate rounded = ClosestRounding(*roundingDigits_)(equivalentRate);
20✔
187
        return compounding_ == Continuous ?
20✔
188
                   rounded :
189
                   InterestRate(rounded, dayCounter(), compounding_, frequency_)
40✔
190
                       .equivalentRate(Continuous, NoFrequency, t);
40✔
191
    }
192

193
    inline Rate UltimateForwardTermStructure::zeroYieldImpl(Time t) const {
49✔
194
        Time cutOffTime = originalCurve_->timeFromReference(referenceDate() + fsp_);
98✔
195
        Time deltaT = t - cutOffTime;
49✔
196
        /* If time to maturity (T) exceeds the cut-off point (T_c),
197
           i.e. the first smoothing point, the forward rate f is
198
           extrapolated as follows:
199

200
           f(t,T_c,T) = UFR(t) + (LLFR(t) - UFR(t)) * B(T-T_c),
201

202
           where:
203
           UFR(t) - Ultimate Forward Rate quote,
204
           LLFR(t) - Last Liquid Forward Rate quote,
205
           B(t-T_c) = [1 - exp(-a * (T-T_c))] / [a * (T-T_c)],
206
           with a being the growth factor (alpha). */
207
        if (deltaT > 0.0) {
49✔
208
            InterestRate baseRate = originalCurve_->zeroRate(cutOffTime, Continuous, NoFrequency);
33✔
209
            Real beta = (1.0 - std::exp(-alpha_ * deltaT)) / (alpha_ * deltaT);
33✔
210
            Rate extrapolatedForward = ufr_->value() + (llfr_->value() - ufr_->value()) * beta;
33✔
211
            return applyRounding((cutOffTime * baseRate + deltaT * extrapolatedForward) / t, t);
33✔
212
        }
213
        return applyRounding(originalCurve_->zeroRate(t, Continuous, NoFrequency), t);
32✔
214
    }
215
}
216

217
#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

© 2025 Coveralls, Inc