Skip to content

Commit 0f8041b

Browse files
authored
Enable rounding in UFR interpolation (#2342)
2 parents 84b0e8e + bccac7e commit 0f8041b

File tree

2 files changed

+97
-32
lines changed

2 files changed

+97
-32
lines changed

ql/termstructures/yield/ultimateforwardtermstructure.hpp

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
22

33
/*
4-
Copyright (C) 2020 Marcin Rybacki
4+
Copyright (C) 2020, 2025 Marcin Rybacki
55
66
This file is part of QuantLib, a free-software/open-source library
77
for financial quantitative analysts and developers - http://quantlib.org/
@@ -24,6 +24,8 @@
2424
#ifndef quantlib_ultimate_forward_term_structure_hpp
2525
#define quantlib_ultimate_forward_term_structure_hpp
2626

27+
#include <ql/math/rounding.hpp>
28+
#include <ql/optional.hpp>
2729
#include <ql/quote.hpp>
2830
#include <ql/termstructures/yield/zeroyieldstructure.hpp>
2931
#include <utility>
@@ -40,13 +42,18 @@ namespace QuantLib {
4042
Bank website:
4143
4244
FTK term structure documentation (Financieel toetsingskader):
43-
https://www.toezicht.dnb.nl/binaries/50-212329.pdf
45+
https://www.dnb.nl/media/4lmprzrk/vaststelling_methode_rentetermijnstructuur_ftk.pdf
4446
45-
UFR 2015 term structure documentation:
46-
https://www.toezicht.dnb.nl/binaries/50-234028.pdf
47+
UFR 2013-2019 term structure documentation:
48+
https://www.dnb.nl/media/0vmbxaf4/methodologie-dnb.pdf
4749
48-
UFR 2019 term structure documentation:
49-
https://www.rijksoverheid.nl/documenten/kamerstukken/2019/06/11/advies-commissie-parameters
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%.
5057
5158
This term structure will remain linked to the original
5259
structure, i.e., any changes in the latter will be
@@ -65,6 +72,7 @@ namespace QuantLib {
6572
- incorrect input for cut-off point should raise an exception.
6673
- observability against changes in the underlying term
6774
structure and the additional components is checked.
75+
- rounding of output rate with predefined compounding.
6876
*/
6977

7078
class UltimateForwardTermStructure : public ZeroYieldStructure {
@@ -73,7 +81,10 @@ namespace QuantLib {
7381
Handle<Quote> lastLiquidForwardRate,
7482
Handle<Quote> ultimateForwardRate,
7583
const Period& firstSmoothingPoint,
76-
Real alpha);
84+
Real alpha,
85+
const ext::optional<Integer>& roundingDigits = ext::nullopt,
86+
Compounding compounding = Compounded,
87+
Frequency frequency = Annual);
7788
//! \name YieldTermStructure interface
7889
//@{
7990
DayCounter dayCounter() const override;
@@ -91,11 +102,18 @@ namespace QuantLib {
91102
Rate zeroYieldImpl(Time) const override;
92103
//@}
93104
private:
105+
//! applies rounding on zero rate with required compounding
106+
Rate applyRounding(Rate r, Time t) const;
107+
//@}
108+
94109
Handle<YieldTermStructure> originalCurve_;
95110
Handle<Quote> llfr_;
96111
Handle<Quote> ufr_;
97112
Period fsp_;
98113
Real alpha_;
114+
ext::optional<Integer> roundingDigits_;
115+
Compounding compounding_;
116+
Frequency frequency_;
99117
};
100118

101119
// inline definitions
@@ -105,9 +123,13 @@ namespace QuantLib {
105123
Handle<Quote> lastLiquidForwardRate,
106124
Handle<Quote> ultimateForwardRate,
107125
const Period& firstSmoothingPoint,
108-
Real alpha)
126+
Real alpha,
127+
const ext::optional<Integer>& roundingDigits,
128+
Compounding compounding,
129+
Frequency frequency)
109130
: originalCurve_(std::move(h)), llfr_(std::move(lastLiquidForwardRate)),
110-
ufr_(std::move(ultimateForwardRate)), fsp_(firstSmoothingPoint), alpha_(alpha) {
131+
ufr_(std::move(ultimateForwardRate)), fsp_(firstSmoothingPoint), alpha_(alpha),
132+
roundingDigits_(roundingDigits), compounding_(compounding), frequency_(frequency) {
111133
QL_REQUIRE(fsp_.length() > 0,
112134
"first smoothing point must be a period with positive length");
113135
if (!originalCurve_.empty())
@@ -149,6 +171,25 @@ namespace QuantLib {
149171
}
150172
}
151173

174+
inline Rate UltimateForwardTermStructure::applyRounding(Rate r, Time t) const {
175+
if (!roundingDigits_.has_value()) {
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 ?
183+
r :
184+
InterestRate(r, dayCounter(), Continuous, NoFrequency)
185+
.equivalentRate(compounding_, frequency_, t);
186+
Rate rounded = ClosestRounding(*roundingDigits_)(equivalentRate);
187+
return compounding_ == Continuous ?
188+
rounded :
189+
InterestRate(rounded, dayCounter(), compounding_, frequency_)
190+
.equivalentRate(Continuous, NoFrequency, t);
191+
}
192+
152193
inline Rate UltimateForwardTermStructure::zeroYieldImpl(Time t) const {
153194
Time cutOffTime = originalCurve_->timeFromReference(referenceDate() + fsp_);
154195
Time deltaT = t - cutOffTime;
@@ -167,9 +208,9 @@ namespace QuantLib {
167208
InterestRate baseRate = originalCurve_->zeroRate(cutOffTime, Continuous, NoFrequency);
168209
Real beta = (1.0 - std::exp(-alpha_ * deltaT)) / (alpha_ * deltaT);
169210
Rate extrapolatedForward = ufr_->value() + (llfr_->value() - ufr_->value()) * beta;
170-
return (cutOffTime * baseRate + deltaT * extrapolatedForward) / t;
211+
return applyRounding((cutOffTime * baseRate + deltaT * extrapolatedForward) / t, t);
171212
}
172-
return originalCurve_->zeroRate(t, Continuous, NoFrequency);
213+
return applyRounding(originalCurve_->zeroRate(t, Continuous, NoFrequency), t);
173214
}
174215
}
175216

test-suite/ultimateforwardtermstructure.cpp

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -137,44 +137,68 @@ Rate calculateExtrapolatedForward(Time t, Time fsp, Rate llfr, Rate ufr, Real al
137137
return ufr + (llfr - ufr) * beta;
138138
}
139139

140-
141-
BOOST_AUTO_TEST_CASE(testDutchCentralBankRates) {
142-
BOOST_TEST_MESSAGE("Testing DNB replication of UFR zero annually compounded rates...");
143-
140+
void checkDutchBankRates(const std::vector<Datum> expectedRates,
141+
const ext::optional<Integer>& rounding = ext::nullopt,
142+
Compounding compounding = Compounded,
143+
Frequency frequency = Annual,
144+
Real tolerance = 1.0e-4) {
144145
CommonVars vars;
145146

146147
ext::shared_ptr<Quote> llfr = calculateLLFR(vars.ftkCurveHandle, vars.fsp);
147148

148-
ext::shared_ptr<YieldTermStructure> ufrTs(
149-
new UltimateForwardTermStructure(vars.ftkCurveHandle, Handle<Quote>(llfr),
150-
Handle<Quote>(vars.ufrRate), vars.fsp, vars.alpha));
151-
152-
// Official annually compounded zero rates published
153-
// by the Dutch Central Bank: https://statistiek.dnb.nl/
154-
Datum expectedZeroes[] = {{10, Years, 0.00477}, {20, Years, 0.01004}, {30, Years, 0.01223},
155-
{40, Years, 0.01433}, {50, Years, 0.01589}, {60, Years, 0.01702},
156-
{70, Years, 0.01785}, {80, Years, 0.01849}, {90, Years, 0.01899},
157-
{100, Years, 0.01939}};
149+
ext::shared_ptr<YieldTermStructure> ufrTs(new UltimateForwardTermStructure(
150+
vars.ftkCurveHandle, Handle<Quote>(llfr), Handle<Quote>(vars.ufrRate), vars.fsp, vars.alpha,
151+
rounding, compounding, frequency));
158152

159-
Real tolerance = 1.0e-4;
160-
Size nRates = std::size(expectedZeroes);
153+
Size nRates = std::size(expectedRates);
161154

162155
for (Size i = 0; i < nRates; ++i) {
163-
Period p = expectedZeroes[i].n * expectedZeroes[i].units;
156+
Period p = expectedRates[i].n * expectedRates[i].units;
164157
Date maturity = vars.settlement + p;
165158

166-
Rate actual = ufrTs->zeroRate(maturity, vars.dayCount, Compounded, Annual).rate();
167-
Rate expected = expectedZeroes[i].rate;
159+
Rate actual = ufrTs->zeroRate(maturity, vars.dayCount, compounding, frequency).rate();
160+
Rate expected = expectedRates[i].rate;
168161

169162
if (std::fabs(actual - expected) > tolerance)
170163
BOOST_ERROR("unable to reproduce zero yield rate from the UFR curve\n"
171-
<< std::setprecision(5)
172-
<< " calculated: " << actual << "\n"
164+
<< std::setprecision(5) << " calculated: " << actual << "\n"
173165
<< " expected: " << expected << "\n"
174166
<< " tenor: " << p << "\n");
175167
}
176168
}
177169

170+
BOOST_AUTO_TEST_CASE(testDutchCentralBankRates) {
171+
BOOST_TEST_MESSAGE("Testing DNB replication of UFR zero annually compounded rates...");
172+
173+
std::vector<Datum> expectedRates{
174+
{10, Years, 0.00477}, {20, Years, 0.01004}, {30, Years, 0.01223}, {40, Years, 0.01433},
175+
{50, Years, 0.01589}, {60, Years, 0.01702}, {70, Years, 0.01785}, {80, Years, 0.01849},
176+
{90, Years, 0.01899}, {100, Years, 0.01939}};
177+
checkDutchBankRates(expectedRates);
178+
}
179+
180+
BOOST_AUTO_TEST_CASE(testDutchCentralBankRatesWithRounding) {
181+
BOOST_TEST_MESSAGE(
182+
"Testing DNB replication of UFR zero annually compounded rates with rounding...");
183+
std::vector<Datum> expectedRates{{10, Years, 0.005}, {20, Years, 0.01}, {30, Years, 0.012},
184+
{40, Years, 0.014}, {50, Years, 0.016}, {60, Years, 0.017},
185+
{70, Years, 0.018}, {80, Years, 0.018}, {90, Years, 0.019},
186+
{100, Years, 0.019}};
187+
188+
checkDutchBankRates(expectedRates, 3, Compounded, Annual, 1.e-12);
189+
}
190+
191+
BOOST_AUTO_TEST_CASE(testDutchCentralBankRatesWithRoundingAndContinuousCompounding) {
192+
BOOST_TEST_MESSAGE(
193+
"Testing DNB replication of UFR zero continuously compounded rates with rounding...");
194+
std::vector<Datum> expectedRates{
195+
{10, Years, 0.00477}, {20, Years, 0.01002}, {30, Years, 0.01211}, {40, Years, 0.01417},
196+
{50, Years, 0.01571}, {60, Years, 0.01683}, {70, Years, 0.01766}, {80, Years, 0.01829},
197+
{90, Years, 0.01878}, {100, Years, 0.01917}};
198+
199+
checkDutchBankRates(expectedRates, 5, Continuous, NoFrequency, 1.e-12);
200+
}
201+
178202
BOOST_AUTO_TEST_CASE(testExtrapolatedForward) {
179203
BOOST_TEST_MESSAGE("Testing continuous forward rates in extrapolation region...");
180204

0 commit comments

Comments
 (0)