1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2/*
3 Copyright (C) 2020 Marcin Rybacki
4
5 This file is part of QuantLib, a free-software/open-source library
6 for financial quantitative analysts and developers - http://quantlib.org/
7
8 QuantLib is free software: you can redistribute it and/or modify it
9 under the terms of the QuantLib license. You should have received a
10 copy of the license along with this program; if not, please email
11 <quantlib-dev@lists.sf.net>. The license is also available online at
12 <http://quantlib.org/license.shtml>.
13
14 This program is distributed in the hope that it will be useful, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
19#include "ultimateforwardtermstructure.hpp"
20#include "utilities.hpp"
21#include <ql/currencies/europe.hpp>
22#include <ql/indexes/iborindex.hpp>
23#include <ql/math/interpolations/loginterpolation.hpp>
24#include <ql/termstructures/yield/piecewiseyieldcurve.hpp>
25#include <ql/termstructures/yield/ratehelpers.hpp>
26#include <ql/termstructures/yield/ultimateforwardtermstructure.hpp>
27#include <ql/termstructures/yieldtermstructure.hpp>
28#include <ql/time/calendars/nullcalendar.hpp>
29#include <ql/time/daycounters/simpledaycounter.hpp>
30
31using namespace QuantLib;
32using namespace boost::unit_test_framework;
33
34namespace ultimate_forward_term_structure_test {
35
36 struct Datum {
37 Integer n;
38 TimeUnit units;
39 Rate rate;
40 };
41
42 struct LLFRWeight {
43 Time ttm;
44 Real weight;
45 };
46
47 struct CommonVars {
48 Date today, settlement;
49 Calendar calendar;
50 Natural settlementDays;
51 Currency ccy;
52 BusinessDayConvention businessConvention;
53 DayCounter dayCount;
54 Frequency fixedFrequency;
55 Period floatingTenor;
56
57 ext::shared_ptr<IborIndex> index;
58 RelinkableHandle<YieldTermStructure> ftkCurveHandle;
59
60 ext::shared_ptr<Quote> ufrRate;
61 Period fsp;
62 Real alpha;
63
64 // utilities
65
66 CommonVars() {
67 settlementDays = 2;
68 businessConvention = Unadjusted;
69 dayCount = SimpleDayCounter();
70 calendar = NullCalendar();
71 ccy = EURCurrency();
72 fixedFrequency = Annual;
73 floatingTenor = 6 * Months;
74
75 index = ext::make_shared<IborIndex>(
76 args: "FTK_IDX", args&: floatingTenor, args&: settlementDays, args&: ccy, args&: calendar,
77 args&: businessConvention, args: false, args&: dayCount, args&: ftkCurveHandle);
78
79 /* Data source: https://fred.stlouisfed.org/
80 Note that these rates are used as a proxy.
81
82 In order to fully replicate the rates published by the Dutch Central Bank
83 (with the required accuracy) one needs to use Bloomberg CMPL BID Euribor 6m swap
84 rates as stated in the documentation: https://www.toezicht.dnb.nl */
85 Datum swapData[] = {{.n: 1, .units: Years, .rate: -0.00315}, {.n: 2, .units: Years, .rate: -0.00205}, {.n: 3, .units: Years, .rate: -0.00144},
86 {.n: 4, .units: Years, .rate: -0.00068}, {.n: 5, .units: Years, .rate: 0.00014}, {.n: 6, .units: Years, .rate: 0.00103},
87 {.n: 7, .units: Years, .rate: 0.00194}, {.n: 8, .units: Years, .rate: 0.00288}, {.n: 9, .units: Years, .rate: 0.00381},
88 {.n: 10, .units: Years, .rate: 0.00471}, {.n: 12, .units: Years, .rate: 0.0063}, {.n: 15, .units: Years, .rate: 0.00808},
89 {.n: 20, .units: Years, .rate: 0.00973}, {.n: 25, .units: Years, .rate: 0.01035}, {.n: 30, .units: Years, .rate: 0.01055},
90 {.n: 40, .units: Years, .rate: 0.0103}, {.n: 50, .units: Years, .rate: 0.0103}};
91
92 InterestRate ufr(0.023, dayCount, Compounded, Annual);
93 ufrRate = ext::shared_ptr<Quote>(
94 new SimpleQuote(ufr.equivalentRate(comp: Continuous, freq: Annual, t: 1.0)));
95 fsp = 20 * Years;
96 alpha = 0.1;
97
98 today = calendar.adjust(Date(29, March, 2019));
99 Settings::instance().evaluationDate() = today;
100 settlement = calendar.advance(today, n: settlementDays, unit: Days);
101
102 Size nInstruments = LENGTH(swapData);
103 std::vector<ext::shared_ptr<RateHelper> > instruments(nInstruments);
104 for (Size i = 0; i < nInstruments; i++) {
105 instruments[i] = ext::shared_ptr<RateHelper>(new SwapRateHelper(
106 swapData[i].rate, Period(swapData[i].n, swapData[i].units), calendar,
107 fixedFrequency, businessConvention, dayCount, index));
108 }
109
110 ext::shared_ptr<YieldTermStructure> ftkCurve(
111 new PiecewiseYieldCurve<Discount, LogLinear>(settlement, instruments, dayCount));
112 ftkCurve->enableExtrapolation();
113 ftkCurveHandle.linkTo(h: ftkCurve);
114 }
115 };
116
117 ext::shared_ptr<Quote> calculateLLFR(const Handle<YieldTermStructure>& ts, const Period& fsp) {
118 DayCounter dc = ts->dayCounter();
119 Real omega = 8.0 / 15.0;
120 Time cutOff = ts->timeFromReference(d: ts->referenceDate() + fsp);
121
122 LLFRWeight llfrWeights[] = {{.ttm: 25.0, .weight: 1.0}, {.ttm: 30.0, .weight: 0.5}, {.ttm: 40.0, .weight: 0.25}, {.ttm: 50.0, .weight: 0.125}};
123 Size nWeights = LENGTH(llfrWeights);
124 Rate llfr = 0.0;
125 for (Size j = 0; j < nWeights; j++) {
126 LLFRWeight w = llfrWeights[j];
127 llfr += w.weight * ts->forwardRate(t1: cutOff, t2: w.ttm, comp: Continuous, freq: NoFrequency, extrapolate: true);
128 }
129 return ext::shared_ptr<Quote>(new SimpleQuote(omega * llfr));
130 }
131
132 Rate calculateExtrapolatedForward(Time t, Time fsp, Rate llfr, Rate ufr, Real alpha) {
133 Time deltaT = t - fsp;
134 Real beta = (1.0 - std::exp(x: -alpha * deltaT)) / (alpha * deltaT);
135 return ufr + (llfr - ufr) * beta;
136 }
137}
138
139void UltimateForwardTermStructureTest::testDutchCentralBankRates() {
140 BOOST_TEST_MESSAGE("Testing DNB replication of UFR zero annually compounded rates...");
141
142 using namespace ultimate_forward_term_structure_test;
143
144 CommonVars vars;
145
146 ext::shared_ptr<Quote> llfr = calculateLLFR(ts: vars.ftkCurveHandle, fsp: vars.fsp);
147
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[] = {{.n: 10, .units: Years, .rate: 0.00477}, {.n: 20, .units: Years, .rate: 0.01004}, {.n: 30, .units: Years, .rate: 0.01223},
155 {.n: 40, .units: Years, .rate: 0.01433}, {.n: 50, .units: Years, .rate: 0.01589}, {.n: 60, .units: Years, .rate: 0.01702},
156 {.n: 70, .units: Years, .rate: 0.01785}, {.n: 80, .units: Years, .rate: 0.01849}, {.n: 90, .units: Years, .rate: 0.01899},
157 {.n: 100, .units: Years, .rate: 0.01939}};
158
159 Real tolerance = 1.0e-4;
160 Size nRates = LENGTH(expectedZeroes);
161
162 for (Size i = 0; i < nRates; ++i) {
163 Period p = expectedZeroes[i].n * expectedZeroes[i].units;
164 Date maturity = vars.settlement + p;
165
166 Rate actual = ufrTs->zeroRate(d: maturity, resultDayCounter: vars.dayCount, comp: Compounded, freq: Annual).rate();
167 Rate expected = expectedZeroes[i].rate;
168
169 if (std::fabs(x: actual - expected) > tolerance)
170 BOOST_ERROR("unable to reproduce zero yield rate from the UFR curve\n"
171 << std::setprecision(5)
172 << " calculated: " << actual << "\n"
173 << " expected: " << expected << "\n"
174 << " tenor: " << p << "\n");
175 }
176}
177
178void UltimateForwardTermStructureTest::testExtrapolatedForward() {
179 BOOST_TEST_MESSAGE("Testing continuous forward rates in extrapolation region...");
180
181 using namespace ultimate_forward_term_structure_test;
182
183 CommonVars vars;
184
185 ext::shared_ptr<Quote> llfr(new SimpleQuote(0.0125));
186
187 ext::shared_ptr<YieldTermStructure> ufrTs(
188 new UltimateForwardTermStructure(vars.ftkCurveHandle, Handle<Quote>(llfr),
189 Handle<Quote>(vars.ufrRate), vars.fsp, vars.alpha));
190 Time cutOff = ufrTs->timeFromReference(d: ufrTs->referenceDate() + vars.fsp);
191
192 Period tenors[] = {
193 20 * Years, 30 * Years, 40 * Years, 50 * Years, 60 * Years,
194 70 * Years, 80 * Years, 90 * Years, 100 * Years,
195 };
196
197 Size nTenors = LENGTH(tenors);
198
199 for (Size i = 0; i < nTenors; ++i) {
200 Date maturity = vars.settlement + tenors[i];
201 Time t = ufrTs->timeFromReference(d: maturity);
202
203 Rate actual = ufrTs->forwardRate(t1: cutOff, t2: t, comp: Continuous, freq: NoFrequency, extrapolate: true).rate();
204 Rate expected = calculateExtrapolatedForward(t, fsp: cutOff, llfr: llfr->value(),
205 ufr: vars.ufrRate->value(), alpha: vars.alpha);
206
207 Real tolerance = 1.0e-10;
208 if (std::fabs(x: actual - expected) > tolerance)
209 BOOST_ERROR("unable to replicate the forward rate from the UFR curve\n"
210 << std::setprecision(5)
211 << " calculated: " << actual << "\n"
212 << " expected: " << expected << "\n"
213 << " tenor: " << tenors[i] << "\n");
214 }
215}
216
217void UltimateForwardTermStructureTest::testZeroRateAtFirstSmoothingPoint() {
218 BOOST_TEST_MESSAGE("Testing zero rate on the first smoothing point...");
219
220 using namespace ultimate_forward_term_structure_test;
221
222 CommonVars vars;
223
224 ext::shared_ptr<Quote> llfr(new SimpleQuote(0.0125));
225
226 ext::shared_ptr<YieldTermStructure> ufrTs(
227 new UltimateForwardTermStructure(vars.ftkCurveHandle, Handle<Quote>(llfr),
228 Handle<Quote>(vars.ufrRate), vars.fsp, vars.alpha));
229 Time cutOff = ufrTs->timeFromReference(d: ufrTs->referenceDate() + vars.fsp);
230
231 Rate actual = ufrTs->zeroRate(t: cutOff, comp: Continuous, freq: NoFrequency, extrapolate: true).rate();
232 Rate expected = vars.ftkCurveHandle->zeroRate(t: cutOff, comp: Continuous, freq: NoFrequency, extrapolate: true).rate();
233
234 Real tolerance = 1.0e-10;
235 if (std::fabs(x: actual - expected) > tolerance)
236 BOOST_ERROR("unable to replicate the zero rate on the First Smoothing Point\n"
237 << std::setprecision(5)
238 << " calculated: " << actual << "\n"
239 << " expected: " << expected << "\n"
240 << " FSP: " << vars.fsp << "\n");
241}
242
243void UltimateForwardTermStructureTest::testThatInspectorsEqualToBaseCurve() {
244 BOOST_TEST_MESSAGE("Testing UFR curve inspectors...");
245
246 using namespace ultimate_forward_term_structure_test;
247
248 CommonVars vars;
249
250 ext::shared_ptr<Quote> llfr(new SimpleQuote(0.0125));
251
252 ext::shared_ptr<YieldTermStructure> ufrTs(
253 new UltimateForwardTermStructure(vars.ftkCurveHandle, Handle<Quote>(llfr),
254 Handle<Quote>(vars.ufrRate), vars.fsp, vars.alpha));
255
256 if (ufrTs->dayCounter() != vars.ftkCurveHandle->dayCounter())
257 BOOST_ERROR("different day counter on the UFR curve than on the base curve\n"
258 << " UFR curve: " << ufrTs->dayCounter() << "\n"
259 << " base curve: " << vars.ftkCurveHandle->dayCounter() << "\n");
260
261 if (ufrTs->referenceDate() != vars.ftkCurveHandle->referenceDate())
262 BOOST_ERROR("different reference date on the UFR curve than on the base curve\n"
263 << " UFR curve: " << ufrTs->referenceDate() << "\n"
264 << " base curve: " << vars.ftkCurveHandle->referenceDate() << "\n");
265
266 if (ufrTs->maxDate() == vars.ftkCurveHandle->maxDate())
267 BOOST_ERROR("same max date on the UFR curve as on the base curve\n"
268 << " UFR curve: " << ufrTs->maxDate() << "\n"
269 << " base curve: " << vars.ftkCurveHandle->maxDate() << "\n");
270
271 if (ufrTs->maxTime() == vars.ftkCurveHandle->maxTime())
272 BOOST_ERROR("same max time on the UFR curve as on the base curve\n"
273 << " UFR curve: " << ufrTs->maxTime() << "\n"
274 << " base curve: " << vars.ftkCurveHandle->maxTime() << "\n");
275}
276
277void UltimateForwardTermStructureTest::testExceptionWhenFspLessOrEqualZero() {
278 BOOST_TEST_MESSAGE("Testing exception when the first smoothing point is less than or equal to zero...");
279
280 using namespace ultimate_forward_term_structure_test;
281
282 CommonVars vars;
283
284 ext::shared_ptr<Quote> llfr(new SimpleQuote(0.0125));
285
286 BOOST_CHECK_THROW(
287 ext::shared_ptr<YieldTermStructure> ufrTsZeroPeriod(
288 new UltimateForwardTermStructure(vars.ftkCurveHandle, Handle<Quote>(llfr),
289 Handle<Quote>(vars.ufrRate), 0 * Years, vars.alpha)),
290 Error);
291
292 BOOST_CHECK_THROW(
293 ext::shared_ptr<YieldTermStructure> ufrTsNegativePeriod(
294 new UltimateForwardTermStructure(vars.ftkCurveHandle, Handle<Quote>(llfr),
295 Handle<Quote>(vars.ufrRate), -1 * Years, vars.alpha)),
296 Error);
297}
298
299void UltimateForwardTermStructureTest::testObservability() {
300 BOOST_TEST_MESSAGE("Testing observability of the UFR curve...");
301
302 using namespace ultimate_forward_term_structure_test;
303
304 CommonVars vars;
305
306 ext::shared_ptr<SimpleQuote> llfr(new SimpleQuote(0.0125));
307 Handle<Quote> llfr_quote(llfr);
308 ext::shared_ptr<SimpleQuote> ufr(new SimpleQuote(0.02));
309 Handle<Quote> ufr_handle(ufr);
310 ext::shared_ptr<YieldTermStructure> ufrTs(new UltimateForwardTermStructure(
311 vars.ftkCurveHandle, llfr_quote, ufr_handle, vars.fsp, vars.alpha));
312
313 Flag flag;
314 flag.registerWith(h: ufrTs);
315 llfr->setValue(0.012);
316 if (!flag.isUp())
317 BOOST_ERROR("Observer was not notified of LLFR change.");
318 flag.lower();
319 ufr->setValue(0.019);
320 if (!flag.isUp())
321 BOOST_ERROR("Observer was not notified of UFR change.");
322}
323
324test_suite* UltimateForwardTermStructureTest::suite() {
325 auto* suite = BOOST_TEST_SUITE("UFR term structure tests");
326
327 suite->add(QUANTLIB_TEST_CASE(&UltimateForwardTermStructureTest::testDutchCentralBankRates));
328 suite->add(QUANTLIB_TEST_CASE(&UltimateForwardTermStructureTest::testExtrapolatedForward));
329 suite->add(
330 QUANTLIB_TEST_CASE(&UltimateForwardTermStructureTest::testZeroRateAtFirstSmoothingPoint));
331 suite->add(
332 QUANTLIB_TEST_CASE(&UltimateForwardTermStructureTest::testThatInspectorsEqualToBaseCurve));
333 suite->add(
334 QUANTLIB_TEST_CASE(&UltimateForwardTermStructureTest::testExceptionWhenFspLessOrEqualZero));
335 suite->add(QUANTLIB_TEST_CASE(&UltimateForwardTermStructureTest::testObservability));
336 return suite;
337}
338

source code of quantlib/test-suite/ultimateforwardtermstructure.cpp