1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3/*
4 Copyright (C) 2005, 2006, 2007, 2008, 2009, 2017 StatPro Italia srl
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#include "piecewiseyieldcurve.hpp"
21#include "utilities.hpp"
22#include <ql/cashflows/iborcoupon.hpp>
23#include <ql/indexes/bmaindex.hpp>
24#include <ql/indexes/ibor/euribor.hpp>
25#include <ql/indexes/ibor/jpylibor.hpp>
26#include <ql/indexes/ibor/usdlibor.hpp>
27#include <ql/instruments/forwardrateagreement.hpp>
28#include <ql/instruments/makevanillaswap.hpp>
29#include <ql/math/comparison.hpp>
30#include <ql/math/interpolations/backwardflatinterpolation.hpp>
31#include <ql/math/interpolations/convexmonotoneinterpolation.hpp>
32#include <ql/math/interpolations/cubicinterpolation.hpp>
33#include <ql/math/interpolations/forwardflatinterpolation.hpp>
34#include <ql/math/interpolations/linearinterpolation.hpp>
35#include <ql/math/interpolations/loginterpolation.hpp>
36#include <ql/pricingengines/bond/discountingbondengine.hpp>
37#include <ql/pricingengines/swap/discountingswapengine.hpp>
38#include <ql/quotes/simplequote.hpp>
39#include <ql/termstructures/globalbootstrap.hpp>
40#include <ql/termstructures/yield/bondhelpers.hpp>
41#include <ql/termstructures/yield/flatforward.hpp>
42#include <ql/termstructures/yield/piecewiseyieldcurve.hpp>
43#include <ql/termstructures/yield/ratehelpers.hpp>
44#include <ql/time/asx.hpp>
45#include <ql/time/calendars/japan.hpp>
46#include <ql/time/calendars/jointcalendar.hpp>
47#include <ql/time/calendars/target.hpp>
48#include <ql/time/calendars/weekendsonly.hpp>
49#include <ql/time/daycounters/actual360.hpp>
50#include <ql/time/daycounters/actualactual.hpp>
51#include <ql/time/daycounters/thirty360.hpp>
52#include <ql/time/imm.hpp>
53#include <ql/utilities/dataformatters.hpp>
54#include <iomanip>
55#include <map>
56#include <string>
57#include <utility>
58#include <vector>
59
60using namespace QuantLib;
61using namespace boost::unit_test_framework;
62using std::map;
63using std::vector;
64using std::string;
65
66namespace piecewise_yield_curve_test {
67
68 struct Datum {
69 Integer n;
70 TimeUnit units;
71 Rate rate;
72 };
73
74 struct BondDatum {
75 Integer n;
76 TimeUnit units;
77 Integer length;
78 Frequency frequency;
79 Rate coupon;
80 Real price;
81 };
82
83 Datum depositData[] = {
84 { .n: 1, .units: Weeks, .rate: 4.559 },
85 { .n: 1, .units: Months, .rate: 4.581 },
86 { .n: 2, .units: Months, .rate: 4.573 },
87 { .n: 3, .units: Months, .rate: 4.557 },
88 { .n: 6, .units: Months, .rate: 4.496 },
89 { .n: 9, .units: Months, .rate: 4.490 }
90 };
91
92 Datum fraData[] = {
93 { .n: 1, .units: Months, .rate: 4.581 },
94 { .n: 2, .units: Months, .rate: 4.573 },
95 { .n: 3, .units: Months, .rate: 4.557 },
96 { .n: 6, .units: Months, .rate: 4.496 },
97 { .n: 9, .units: Months, .rate: 4.490 }
98 };
99
100 Datum immFutData[] = {
101 { .n: 1, .units: Months, .rate: 4.581 },
102 { .n: 2, .units: Months, .rate: 4.573 },
103 { .n: 3, .units: Months, .rate: 4.557 }
104 };
105
106 Datum asxFutData[] = {
107 { .n: 1, .units: Months, .rate: 4.581 },
108 { .n: 2, .units: Months, .rate: 4.573 },
109 { .n: 3, .units: Months, .rate: 4.557 }
110 };
111
112 Datum swapData[] = {
113 { .n: 1, .units: Years, .rate: 4.54 },
114 { .n: 2, .units: Years, .rate: 4.63 },
115 { .n: 3, .units: Years, .rate: 4.75 },
116 { .n: 4, .units: Years, .rate: 4.86 },
117 { .n: 5, .units: Years, .rate: 4.99 },
118 { .n: 6, .units: Years, .rate: 5.11 },
119 { .n: 7, .units: Years, .rate: 5.23 },
120 { .n: 8, .units: Years, .rate: 5.33 },
121 { .n: 9, .units: Years, .rate: 5.41 },
122 { .n: 10, .units: Years, .rate: 5.47 },
123 { .n: 12, .units: Years, .rate: 5.60 },
124 { .n: 15, .units: Years, .rate: 5.75 },
125 { .n: 20, .units: Years, .rate: 5.89 },
126 { .n: 25, .units: Years, .rate: 5.95 },
127 { .n: 30, .units: Years, .rate: 5.96 }
128 };
129
130 BondDatum bondData[] = {
131 { .n: 6, .units: Months, .length: 5, .frequency: Semiannual, .coupon: 4.75, .price: 101.320 },
132 { .n: 1, .units: Years, .length: 3, .frequency: Semiannual, .coupon: 2.75, .price: 100.590 },
133 { .n: 2, .units: Years, .length: 5, .frequency: Semiannual, .coupon: 5.00, .price: 105.650 },
134 { .n: 5, .units: Years, .length: 11, .frequency: Semiannual, .coupon: 5.50, .price: 113.610 },
135 { .n: 10, .units: Years, .length: 11, .frequency: Semiannual, .coupon: 3.75, .price: 104.070 }
136 };
137
138 Datum bmaData[] = {
139 { .n: 1, .units: Years, .rate: 67.56 },
140 { .n: 2, .units: Years, .rate: 68.00 },
141 { .n: 3, .units: Years, .rate: 68.25 },
142 { .n: 4, .units: Years, .rate: 68.50 },
143 { .n: 5, .units: Years, .rate: 68.81 },
144 { .n: 7, .units: Years, .rate: 69.50 },
145 { .n: 10, .units: Years, .rate: 70.44 },
146 { .n: 15, .units: Years, .rate: 71.69 },
147 { .n: 20, .units: Years, .rate: 72.69 },
148 { .n: 30, .units: Years, .rate: 73.81 }
149 };
150
151 struct CommonVars {
152 // global variables
153 Calendar calendar;
154 Natural settlementDays;
155 Date today, settlement;
156 BusinessDayConvention fixedLegConvention;
157 Frequency fixedLegFrequency;
158 DayCounter fixedLegDayCounter;
159 Natural bondSettlementDays;
160 DayCounter bondDayCounter;
161 BusinessDayConvention bondConvention;
162 Real bondRedemption;
163 Frequency bmaFrequency;
164 BusinessDayConvention bmaConvention;
165 DayCounter bmaDayCounter;
166
167 Size deposits, fras, immFuts, asxFuts, swaps, bonds, bmas;
168 std::vector<ext::shared_ptr<SimpleQuote> > rates, fraRates,
169 immFutPrices, asxFutPrices,
170 prices, fractions;
171 std::vector<ext::shared_ptr<RateHelper> > instruments,
172 immFutHelpers, asxFutHelpers,
173 bondHelpers, bmaHelpers;
174
175 std::vector<ext::shared_ptr<RateHelper> > fraHelpers(bool useIndexedFra) const {
176 auto helpers = std::vector<ext::shared_ptr<RateHelper> >(fras);
177 auto euribor3m = ext::make_shared<Euribor3M>();
178 for (Size i=0; i<fras; i++) {
179 Handle<Quote> r(fraRates[i]);
180 helpers[i] = ext::make_shared<FraRateHelper>(
181 args&: r, args&: fraData[i].n, args: fraData[i].n + 3,
182 args: euribor3m->fixingDays(),
183 args: euribor3m->fixingCalendar(),
184 args: euribor3m->businessDayConvention(),
185 args: euribor3m->endOfMonth(),
186 args: euribor3m->dayCounter(),
187 args: Pillar::LastRelevantDate,
188 args: Date(),
189 args&: useIndexedFra);
190 }
191
192 return helpers;
193 }
194
195 std::vector<Schedule> schedules;
196 ext::shared_ptr<YieldTermStructure> termStructure;
197
198 // setup
199 CommonVars(Date evaluationDate = Date()) {
200 // data
201 calendar = TARGET();
202 settlementDays = 2;
203 today = calendar.adjust(evaluationDate != Date() ? evaluationDate : Date::todaysDate());
204 Settings::instance().evaluationDate() = today;
205 settlement = calendar.advance(today,n: settlementDays,unit: Days);
206 fixedLegConvention = Unadjusted;
207 fixedLegFrequency = Annual;
208 fixedLegDayCounter = Thirty360(Thirty360::BondBasis);
209 bondSettlementDays = 3;
210 bondDayCounter = ActualActual(ActualActual::ISDA);
211 bondConvention = Following;
212 bondRedemption = 100.0;
213 bmaFrequency = Quarterly;
214 bmaConvention = Following;
215 bmaDayCounter = ActualActual(ActualActual::ISDA);
216
217 deposits = LENGTH(depositData);
218 fras = LENGTH(fraData);
219 immFuts = LENGTH(immFutData);
220 asxFuts = LENGTH(asxFutData);
221 swaps = LENGTH(swapData);
222 bonds = LENGTH(bondData);
223 bmas = LENGTH(bmaData);
224
225 // market elements
226 rates = std::vector<ext::shared_ptr<SimpleQuote> >(deposits+swaps);
227 fraRates = std::vector<ext::shared_ptr<SimpleQuote> >(fras);
228 immFutPrices = std::vector<ext::shared_ptr<SimpleQuote> >(immFuts);
229 asxFutPrices = std::vector<ext::shared_ptr<SimpleQuote> >(asxFuts);
230 prices = std::vector<ext::shared_ptr<SimpleQuote> >(bonds);
231 fractions = std::vector<ext::shared_ptr<SimpleQuote> >(bmas);
232 for (Size i=0; i<deposits; i++) {
233 rates[i] = ext::make_shared<SimpleQuote>(
234 args: depositData[i].rate/100);
235 }
236 for (Size i=0; i<swaps; i++) {
237 rates[i+deposits] = ext::make_shared<SimpleQuote>(
238 args: swapData[i].rate/100);
239 }
240 for (Size i=0; i<fras; i++) {
241 fraRates[i] = ext::make_shared<SimpleQuote>(
242 args: fraData[i].rate/100);
243 }
244 for (Size i = 0; i<bonds; i++) {
245 prices[i] = ext::make_shared<SimpleQuote>(
246 args&: bondData[i].price);
247 }
248 for (Size i = 0; i<immFuts; i++) {
249 immFutPrices[i] = ext::make_shared<SimpleQuote>(
250 args: 100.0 - immFutData[i].rate);
251 }
252 for (Size i = 0; i<asxFuts; i++) {
253 asxFutPrices[i] = ext::make_shared<SimpleQuote>(
254 args: 100.0 - asxFutData[i].rate);
255 }
256 for (Size i = 0; i<bmas; i++) {
257 fractions[i] = ext::make_shared<SimpleQuote>(
258 args: bmaData[i].rate/100);
259 }
260
261 // rate helpers
262 instruments = std::vector<ext::shared_ptr<RateHelper> >(deposits+swaps);
263 immFutHelpers = std::vector<ext::shared_ptr<RateHelper> >(immFuts);
264 asxFutHelpers = std::vector<ext::shared_ptr<RateHelper> >();
265 bondHelpers = std::vector<ext::shared_ptr<RateHelper> >(bonds);
266 schedules = std::vector<Schedule>(bonds);
267 bmaHelpers = std::vector<ext::shared_ptr<RateHelper> >(bmas);
268
269 ext::shared_ptr<IborIndex> euribor6m(new Euribor6M);
270 for (Size i=0; i<deposits; i++) {
271 Handle<Quote> r(rates[i]);
272 instruments[i] = ext::shared_ptr<RateHelper>(new
273 DepositRateHelper(r,
274 ext::make_shared<Euribor>(
275 args: depositData[i].n*depositData[i].units)));
276 }
277 for (Size i=0; i<swaps; i++) {
278 Handle<Quote> r(rates[i+deposits]);
279 instruments[i+deposits] = ext::shared_ptr<RateHelper>(new
280 SwapRateHelper(r, swapData[i].n*swapData[i].units,
281 calendar,
282 fixedLegFrequency, fixedLegConvention,
283 fixedLegDayCounter, euribor6m));
284 }
285
286 Date immDate = Date();
287 auto euribor3m = ext::make_shared<Euribor3M>();
288 for (Size i = 0; i<immFuts; i++) {
289 Handle<Quote> r(immFutPrices[i]);
290 immDate = IMM::nextDate(d: immDate, mainCycle: false);
291 // if the fixing is before the evaluation date, we
292 // just jump forward by one future maturity
293 if (euribor3m->fixingDate(valueDate: immDate) <
294 Settings::instance().evaluationDate())
295 immDate = IMM::nextDate(d: immDate, mainCycle: false);
296 immFutHelpers[i] = ext::shared_ptr<RateHelper>(new
297 FuturesRateHelper(r, immDate, euribor3m, Handle<Quote>(),
298 Futures::IMM));
299 }
300 Date asxDate = Date();
301 for (Size i = 0; i<asxFuts; i++) {
302 Handle<Quote> r(asxFutPrices[i]);
303 asxDate = ASX::nextDate(d: asxDate, mainCycle: false);
304 // if the fixing is before the evaluation date, we
305 // just jump forward by one future maturity
306 if (euribor3m->fixingDate(valueDate: asxDate) <
307 Settings::instance().evaluationDate())
308 asxDate = ASX::nextDate(d: asxDate, mainCycle: false);
309 if (euribor3m->fixingCalendar().isBusinessDay(d: asxDate))
310 asxFutHelpers.push_back(x: ext::shared_ptr<RateHelper>(new
311 FuturesRateHelper(r, asxDate, euribor3m,
312 Handle<Quote>(), Futures::ASX)));
313 }
314
315 for (Size i=0; i<bonds; i++) {
316 Handle<Quote> p(prices[i]);
317 Date maturity =
318 calendar.advance(today, n: bondData[i].n, unit: bondData[i].units);
319 Date issue =
320 calendar.advance(maturity, n: -bondData[i].length, unit: Years);
321 std::vector<Rate> coupons(1, bondData[i].coupon/100.0);
322 schedules[i] = Schedule(issue, maturity,
323 Period(bondData[i].frequency),
324 calendar,
325 bondConvention, bondConvention,
326 DateGeneration::Backward, false);
327 bondHelpers[i] = ext::shared_ptr<RateHelper>(new
328 FixedRateBondHelper(p,
329 bondSettlementDays,
330 bondRedemption, schedules[i],
331 coupons, bondDayCounter,
332 bondConvention,
333 bondRedemption, issue));
334 }
335 }
336 };
337
338
339 template <class T, class I, template<class C> class B>
340 void testCurveConsistency(CommonVars& vars,
341 const I& interpolator = I(),
342 Real tolerance = 1.0e-9) {
343
344 vars.termStructure = ext::shared_ptr<YieldTermStructure>(new
345 PiecewiseYieldCurve<T,I,B>(vars.settlement, vars.instruments,
346 Actual360(),
347 interpolator));
348
349 RelinkableHandle<YieldTermStructure> curveHandle;
350 curveHandle.linkTo(h: vars.termStructure);
351
352 // check deposits
353 for (Size i=0; i<vars.deposits; i++) {
354 Euribor index(depositData[i].n*depositData[i].units,curveHandle);
355 Rate expectedRate = depositData[i].rate/100,
356 estimatedRate = index.fixing(fixingDate: vars.today);
357 if (std::fabs(x: expectedRate-estimatedRate) > tolerance) {
358 BOOST_ERROR(
359 depositData[i].n << " "
360 << (depositData[i].units == Weeks ? "week(s)" : "month(s)")
361 << " deposit:"
362 << std::setprecision(8)
363 << "\n estimated rate: " << io::rate(estimatedRate)
364 << "\n expected rate: " << io::rate(expectedRate));
365 }
366 }
367
368 // check swaps
369 ext::shared_ptr<IborIndex> euribor6m(new Euribor6M(curveHandle));
370 for (Size i=0; i<vars.swaps; i++) {
371 Period tenor = swapData[i].n*swapData[i].units;
372
373 VanillaSwap swap = MakeVanillaSwap(tenor, euribor6m, 0.0)
374 .withEffectiveDate(vars.settlement)
375 .withFixedLegDayCount(dc: vars.fixedLegDayCounter)
376 .withFixedLegTenor(t: Period(vars.fixedLegFrequency))
377 .withFixedLegConvention(bdc: vars.fixedLegConvention)
378 .withFixedLegTerminationDateConvention(bdc: vars.fixedLegConvention);
379
380 Rate expectedRate = swapData[i].rate/100,
381 estimatedRate = swap.fairRate();
382 Spread error = std::fabs(x: expectedRate-estimatedRate);
383 if (error > tolerance) {
384 BOOST_ERROR(
385 swapData[i].n << " year(s) swap:\n"
386 << std::setprecision(8)
387 << "\n estimated rate: " << io::rate(estimatedRate)
388 << "\n expected rate: " << io::rate(expectedRate)
389 << "\n error: " << io::rate(error)
390 << "\n tolerance: " << io::rate(tolerance));
391 }
392 }
393
394 // check bonds
395 vars.termStructure = ext::shared_ptr<YieldTermStructure>(new
396 PiecewiseYieldCurve<T,I,B>(vars.settlement, vars.bondHelpers,
397 Actual360(),
398 interpolator));
399 curveHandle.linkTo(h: vars.termStructure);
400
401 for (Size i=0; i<vars.bonds; i++) {
402 Date maturity = vars.calendar.advance(vars.today,
403 n: bondData[i].n,
404 unit: bondData[i].units);
405 Date issue = vars.calendar.advance(maturity,
406 n: -bondData[i].length,
407 unit: Years);
408 std::vector<Rate> coupons(1, bondData[i].coupon/100.0);
409
410 FixedRateBond bond(vars.bondSettlementDays, 100.0,
411 vars.schedules[i], coupons,
412 vars.bondDayCounter, vars.bondConvention,
413 vars.bondRedemption, issue);
414
415 ext::shared_ptr<PricingEngine> bondEngine(
416 new DiscountingBondEngine(curveHandle));
417 bond.setPricingEngine(bondEngine);
418
419 Real expectedPrice = bondData[i].price,
420 estimatedPrice = bond.cleanPrice();
421 Real error = std::fabs(x: expectedPrice-estimatedPrice);
422 if (error > tolerance) {
423 BOOST_ERROR(io::ordinal(i+1) << " bond failure:" <<
424 std::setprecision(8) <<
425 "\n estimated price: " << estimatedPrice <<
426 "\n expected price: " << expectedPrice <<
427 "\n error: " << error);
428 }
429 }
430
431 // check FRA, use indexed
432
433 bool useIndexedFra = true;
434 ext::shared_ptr<IborIndex> euribor3m(new Euribor3M(curveHandle));
435
436 vars.termStructure = ext::shared_ptr<YieldTermStructure>(new
437 PiecewiseYieldCurve<T,I>(vars.settlement, vars.fraHelpers(useIndexedFra),
438 Actual360(),
439 interpolator));
440 curveHandle.linkTo(h: vars.termStructure);
441
442 for (Size i=0; i<vars.fras; i++) {
443 Date start =
444 vars.calendar.advance(vars.settlement,
445 n: fraData[i].n,
446 unit: fraData[i].units,
447 convention: euribor3m->businessDayConvention(),
448 endOfMonth: euribor3m->endOfMonth());
449 BOOST_REQUIRE(fraData[i].units == Months);
450
451 ForwardRateAgreement fra(euribor3m, start, Position::Long,
452 fraData[i].rate/100, 100.0, curveHandle);
453 Rate expectedRate = fraData[i].rate/100,
454 estimatedRate = fra.forwardRate();
455 if (std::fabs(x: expectedRate-estimatedRate) > tolerance) {
456 BOOST_ERROR(io::ordinal(i+1) << " FRA (indexed) failure:" <<
457 std::setprecision(8) <<
458 "\n estimated rate: " << io::rate(estimatedRate) <<
459 "\n expected rate: " << io::rate(expectedRate));
460 }
461 }
462
463 // check FRA, don't use indexed
464
465 useIndexedFra = false;
466
467 vars.termStructure = ext::shared_ptr<YieldTermStructure>(new
468 PiecewiseYieldCurve<T,I>(vars.settlement, vars.fraHelpers(useIndexedFra),
469 Actual360(),
470 interpolator));
471 curveHandle.linkTo(h: vars.termStructure);
472
473 for (Size i=0; i<vars.fras; i++) {
474 Date start =
475 vars.calendar.advance(vars.settlement,
476 n: fraData[i].n,
477 unit: fraData[i].units,
478 convention: euribor3m->businessDayConvention(),
479 endOfMonth: euribor3m->endOfMonth());
480 BOOST_REQUIRE(fraData[i].units == Months);
481
482 Date end = vars.calendar.advance(vars.settlement, n: 3 + fraData[i].n, unit: Months,
483 convention: euribor3m->businessDayConvention(),
484 endOfMonth: euribor3m->endOfMonth());
485 ForwardRateAgreement fra(euribor3m, start, end, Position::Long,
486 fraData[i].rate/100, 100.0, curveHandle);
487 Rate expectedRate = fraData[i].rate/100,
488 estimatedRate = fra.forwardRate();
489 if (std::fabs(x: expectedRate-estimatedRate) > tolerance) {
490 BOOST_ERROR(io::ordinal(i+1) << " FRA (at par) failure:" <<
491 std::setprecision(8) <<
492 "\n estimated rate: " << io::rate(estimatedRate) <<
493 "\n expected rate: " << io::rate(expectedRate));
494 }
495 }
496
497 // check immFuts
498 vars.termStructure = ext::shared_ptr<YieldTermStructure>(new
499 PiecewiseYieldCurve<T, I>(vars.settlement, vars.immFutHelpers,
500 Actual360(),
501 interpolator));
502 curveHandle.linkTo(h: vars.termStructure);
503
504 Date immStart = Date();
505 for (Size i = 0; i<vars.immFuts; i++) {
506 immStart = IMM::nextDate(d: immStart, mainCycle: false);
507 // if the fixing is before the evaluation date, we
508 // just jump forward by one future maturity
509 if (euribor3m->fixingDate(valueDate: immStart) <
510 Settings::instance().evaluationDate())
511 immStart = IMM::nextDate(d: immStart, mainCycle: false);
512
513 ForwardRateAgreement immFut(euribor3m, immStart, Position::Long,
514 immFutData[i].rate / 100, 100.0, curveHandle);
515 Rate expectedRate = immFutData[i].rate / 100,
516 estimatedRate = immFut.forwardRate();
517 if (std::fabs(x: expectedRate - estimatedRate) > tolerance) {
518 BOOST_ERROR(io::ordinal(i + 1) << " IMM futures failure:" <<
519 std::setprecision(8) <<
520 "\n estimated rate: " << io::rate(estimatedRate) <<
521 "\n expected rate: " << io::rate(expectedRate));
522 }
523 }
524
525 // check asxFuts
526 vars.termStructure = ext::shared_ptr<YieldTermStructure>(new
527 PiecewiseYieldCurve<T, I>(vars.settlement, vars.asxFutHelpers,
528 Actual360(),
529 interpolator));
530 curveHandle.linkTo(h: vars.termStructure);
531
532 Date asxStart = Date();
533 for (Size i = 0; i<vars.asxFuts; i++) {
534 asxStart = ASX::nextDate(d: asxStart, mainCycle: false);
535 // if the fixing is before the evaluation date, we
536 // just jump forward by one future maturity
537 if (euribor3m->fixingDate(valueDate: asxStart) <
538 Settings::instance().evaluationDate())
539 asxStart = ASX::nextDate(d: asxStart, mainCycle: false);
540 if (euribor3m->fixingCalendar().isHoliday(d: asxStart))
541 continue;
542
543 ForwardRateAgreement asxFut(euribor3m, asxStart, Position::Long,
544 asxFutData[i].rate / 100, 100.0, curveHandle);
545 Rate expectedRate = asxFutData[i].rate / 100,
546 estimatedRate = asxFut.forwardRate();
547 if (std::fabs(x: expectedRate - estimatedRate) > tolerance) {
548 BOOST_ERROR(io::ordinal(i + 1) << " ASX futures failure:" <<
549 std::setprecision(8) <<
550 "\n estimated rate: " << io::rate(estimatedRate) <<
551 "\n expected rate: " << io::rate(expectedRate));
552 }
553 }
554
555 // end checks
556 }
557
558 template <class T, class I, template<class C> class B>
559 void testBMACurveConsistency(CommonVars& vars,
560 const I& interpolator = I(),
561 Real tolerance = 1.0e-9) {
562
563 // re-adjust settlement
564 vars.calendar = JointCalendar(BMAIndex().fixingCalendar(),
565 USDLibor(3*Months).fixingCalendar(),
566 JoinHolidays);
567 vars.today = vars.calendar.adjust(Date::todaysDate());
568 Settings::instance().evaluationDate() = vars.today;
569 vars.settlement =
570 vars.calendar.advance(vars.today,n: vars.settlementDays,unit: Days);
571
572
573 Handle<YieldTermStructure> riskFreeCurve(
574 ext::shared_ptr<YieldTermStructure>(
575 new FlatForward(vars.settlement, 0.04, Actual360())));
576
577 ext::shared_ptr<BMAIndex> bmaIndex(new BMAIndex);
578 ext::shared_ptr<IborIndex> liborIndex(
579 new USDLibor(3*Months,riskFreeCurve));
580 for (Size i=0; i<vars.bmas; ++i) {
581 Handle<Quote> f(vars.fractions[i]);
582 vars.bmaHelpers[i] = ext::shared_ptr<RateHelper>(
583 new BMASwapRateHelper(f, bmaData[i].n*bmaData[i].units,
584 vars.settlementDays,
585 vars.calendar,
586 Period(vars.bmaFrequency),
587 vars.bmaConvention,
588 vars.bmaDayCounter,
589 bmaIndex,
590 liborIndex));
591 }
592
593 Weekday w = vars.today.weekday();
594 Date lastWednesday =
595 (w >= 4) ? vars.today - (w - 4) : vars.today + (4 - w - 7);
596 Date lastFixing = bmaIndex->fixingCalendar().adjust(lastWednesday);
597 bmaIndex->addFixing(fixingDate: lastFixing, fixing: 0.03);
598
599 vars.termStructure = ext::shared_ptr<YieldTermStructure>(new
600 PiecewiseYieldCurve<T,I,B>(vars.today, vars.bmaHelpers,
601 Actual360(),
602 interpolator));
603
604 RelinkableHandle<YieldTermStructure> curveHandle;
605 curveHandle.linkTo(h: vars.termStructure);
606
607 // check BMA swaps
608 ext::shared_ptr<BMAIndex> bma(new BMAIndex(curveHandle));
609 ext::shared_ptr<IborIndex> libor3m(new USDLibor(3*Months,
610 riskFreeCurve));
611 for (Size i=0; i<vars.bmas; i++) {
612 Period tenor = bmaData[i].n*bmaData[i].units;
613
614 Schedule bmaSchedule =
615 MakeSchedule().from(effectiveDate: vars.settlement)
616 .to(terminationDate: vars.settlement+tenor)
617 .withFrequency(vars.bmaFrequency)
618 .withCalendar(bma->fixingCalendar())
619 .withConvention(vars.bmaConvention)
620 .backwards();
621 Schedule liborSchedule =
622 MakeSchedule().from(effectiveDate: vars.settlement)
623 .to(terminationDate: vars.settlement+tenor)
624 .withTenor(libor3m->tenor())
625 .withCalendar(libor3m->fixingCalendar())
626 .withConvention(libor3m->businessDayConvention())
627 .endOfMonth(flag: libor3m->endOfMonth())
628 .backwards();
629
630
631 BMASwap swap(Swap::Payer, 100.0,
632 liborSchedule, 0.75, 0.0,
633 libor3m, libor3m->dayCounter(),
634 bmaSchedule, bma, vars.bmaDayCounter);
635 swap.setPricingEngine(ext::shared_ptr<PricingEngine>(
636 new DiscountingSwapEngine(libor3m->forwardingTermStructure())));
637
638 Real expectedFraction = bmaData[i].rate/100,
639 estimatedFraction = swap.fairLiborFraction();
640 Real error = std::fabs(x: expectedFraction-estimatedFraction);
641 if (error > tolerance) {
642 BOOST_ERROR(bmaData[i].n << " year(s) BMA swap:\n"
643 << std::setprecision(8)
644 << "\n estimated libor fraction: " << estimatedFraction
645 << "\n expected libor fraction: " << expectedFraction
646 << "\n error: " << error
647 << "\n tolerance: " << tolerance);
648 }
649 }
650 }
651
652}
653
654
655void PiecewiseYieldCurveTest::testLogCubicDiscountConsistency() {
656
657 BOOST_TEST_MESSAGE(
658 "Testing consistency of piecewise-log-cubic discount curve...");
659
660 using namespace piecewise_yield_curve_test;
661
662 CommonVars vars;
663
664 testCurveConsistency<Discount,LogCubic,IterativeBootstrap>(
665 vars,
666 interpolator: MonotonicLogCubic());
667 testBMACurveConsistency<Discount,LogCubic,IterativeBootstrap>(
668 vars,
669 interpolator: MonotonicLogCubic());
670}
671
672void PiecewiseYieldCurveTest::testLogLinearDiscountConsistency() {
673
674 BOOST_TEST_MESSAGE(
675 "Testing consistency of piecewise-log-linear discount curve...");
676
677 using namespace piecewise_yield_curve_test;
678
679 CommonVars vars;
680
681 testCurveConsistency<Discount,LogLinear,IterativeBootstrap>(vars);
682 testBMACurveConsistency<Discount,LogLinear,IterativeBootstrap>(vars);
683}
684
685void PiecewiseYieldCurveTest::testLinearDiscountConsistency() {
686
687 BOOST_TEST_MESSAGE(
688 "Testing consistency of piecewise-linear discount curve...");
689
690 using namespace piecewise_yield_curve_test;
691
692 CommonVars vars;
693
694 testCurveConsistency<Discount,Linear,IterativeBootstrap>(vars);
695 testBMACurveConsistency<Discount,Linear,IterativeBootstrap>(vars);
696}
697
698void PiecewiseYieldCurveTest::testLinearZeroConsistency() {
699
700 BOOST_TEST_MESSAGE(
701 "Testing consistency of piecewise-linear zero-yield curve...");
702
703 using namespace piecewise_yield_curve_test;
704
705 CommonVars vars;
706
707 testCurveConsistency<ZeroYield,Linear,IterativeBootstrap>(vars);
708 testBMACurveConsistency<ZeroYield,Linear,IterativeBootstrap>(vars);
709}
710
711void PiecewiseYieldCurveTest::testSplineZeroConsistency() {
712
713 BOOST_TEST_MESSAGE(
714 "Testing consistency of piecewise-cubic zero-yield curve...");
715
716 using namespace piecewise_yield_curve_test;
717
718 CommonVars vars;
719
720 testCurveConsistency<ZeroYield,Cubic,IterativeBootstrap>(
721 vars,
722 interpolator: Cubic(CubicInterpolation::Spline, true,
723 CubicInterpolation::SecondDerivative, 0.0,
724 CubicInterpolation::SecondDerivative, 0.0));
725 testBMACurveConsistency<ZeroYield,Cubic,IterativeBootstrap>(
726 vars,
727 interpolator: Cubic(CubicInterpolation::Spline, true,
728 CubicInterpolation::SecondDerivative, 0.0,
729 CubicInterpolation::SecondDerivative, 0.0));
730}
731
732void PiecewiseYieldCurveTest::testLinearForwardConsistency() {
733
734 BOOST_TEST_MESSAGE(
735 "Testing consistency of piecewise-linear forward-rate curve...");
736
737 using namespace piecewise_yield_curve_test;
738
739 CommonVars vars;
740
741 testCurveConsistency<ForwardRate,Linear,IterativeBootstrap>(vars);
742 testBMACurveConsistency<ForwardRate,Linear,IterativeBootstrap>(vars);
743}
744
745void PiecewiseYieldCurveTest::testFlatForwardConsistency() {
746
747 BOOST_TEST_MESSAGE(
748 "Testing consistency of piecewise-flat forward-rate curve...");
749
750 using namespace piecewise_yield_curve_test;
751
752 CommonVars vars;
753
754 testCurveConsistency<ForwardRate,BackwardFlat,IterativeBootstrap>(vars);
755 testBMACurveConsistency<ForwardRate,BackwardFlat,IterativeBootstrap>(vars);
756}
757
758void PiecewiseYieldCurveTest::testSplineForwardConsistency() {
759
760 BOOST_TEST_MESSAGE(
761 "Testing consistency of piecewise-cubic forward-rate curve...");
762
763 using namespace piecewise_yield_curve_test;
764
765 CommonVars vars;
766
767 testCurveConsistency<ForwardRate,Cubic,IterativeBootstrap>(
768 vars,
769 interpolator: Cubic(CubicInterpolation::Spline, true,
770 CubicInterpolation::SecondDerivative, 0.0,
771 CubicInterpolation::SecondDerivative, 0.0));
772 testBMACurveConsistency<ForwardRate,Cubic,IterativeBootstrap>(
773 vars,
774 interpolator: Cubic(CubicInterpolation::Spline, true,
775 CubicInterpolation::SecondDerivative, 0.0,
776 CubicInterpolation::SecondDerivative, 0.0));
777}
778
779void PiecewiseYieldCurveTest::testConvexMonotoneForwardConsistency() {
780 BOOST_TEST_MESSAGE(
781 "Testing consistency of convex monotone forward-rate curve...");
782
783 using namespace piecewise_yield_curve_test;
784
785 CommonVars vars;
786 testCurveConsistency<ForwardRate,ConvexMonotone,IterativeBootstrap>(vars);
787
788 testBMACurveConsistency<ForwardRate,ConvexMonotone,
789 IterativeBootstrap>(vars);
790}
791
792
793void PiecewiseYieldCurveTest::testLocalBootstrapConsistency() {
794 BOOST_TEST_MESSAGE(
795 "Testing consistency of local-bootstrap algorithm...");
796
797 using namespace piecewise_yield_curve_test;
798
799 CommonVars vars;
800 testCurveConsistency<ForwardRate,ConvexMonotone,LocalBootstrap>(
801 vars, interpolator: ConvexMonotone(), tolerance: 1.0e-6);
802 testBMACurveConsistency<ForwardRate,ConvexMonotone,LocalBootstrap>(
803 vars, interpolator: ConvexMonotone(), tolerance: 1.0e-7);
804}
805
806void PiecewiseYieldCurveTest::testParFraRegression() {
807 BOOST_TEST_MESSAGE("Testing regression for at-par FRA...");
808
809 using namespace piecewise_yield_curve_test;
810
811 CommonVars vars(Date(23, February, 2023));
812
813 bool useIndexedFra = false;
814 RelinkableHandle<YieldTermStructure> curveHandle;
815 auto euribor3m = ext::make_shared<Euribor3M>(args&: curveHandle);
816
817 vars.termStructure = ext::make_shared<PiecewiseYieldCurve<ZeroYield, Linear>>(
818 args&: vars.settlement, args: vars.fraHelpers(useIndexedFra), args: Actual360());
819 curveHandle.linkTo(h: vars.termStructure);
820
821 for (Size i=0; i<vars.fras; i++) {
822 Date start = vars.calendar.advance(vars.settlement,
823 n: fraData[i].n,
824 unit: fraData[i].units,
825 convention: euribor3m->businessDayConvention(),
826 endOfMonth: euribor3m->endOfMonth());
827 BOOST_REQUIRE(fraData[i].units == Months);
828
829 Date end = vars.calendar.advance(vars.settlement, n: 3 + fraData[i].n, unit: Months,
830 convention: euribor3m->businessDayConvention(),
831 endOfMonth: euribor3m->endOfMonth());
832 ForwardRateAgreement fra(euribor3m, start, end, Position::Long,
833 fraData[i].rate/100, 100.0, curveHandle);
834 Rate expectedRate = fraData[i].rate/100;
835 Rate estimatedRate = fra.forwardRate();
836 Real tolerance = 1.0e-6;
837 if (std::fabs(x: expectedRate-estimatedRate) > tolerance) {
838 BOOST_ERROR(io::ordinal(i+1) << " FRA (at par) failure:" <<
839 std::setprecision(8) <<
840 "\n estimated rate: " << io::rate(estimatedRate) <<
841 "\n expected rate: " << io::rate(expectedRate));
842 }
843 }
844}
845
846void PiecewiseYieldCurveTest::testObservability() {
847
848 BOOST_TEST_MESSAGE("Testing observability of piecewise yield curve...");
849
850 using namespace piecewise_yield_curve_test;
851
852 CommonVars vars;
853
854 vars.termStructure = ext::shared_ptr<YieldTermStructure>(
855 new PiecewiseYieldCurve<Discount,LogLinear>(vars.settlementDays,
856 vars.calendar,
857 vars.instruments,
858 Actual360()));
859
860 ext::dynamic_pointer_cast<LazyObject>(r: vars.termStructure)->forwardFirstNotificationOnly();
861
862 Flag f;
863 f.registerWith(h: vars.termStructure);
864
865 for (Size i=0; i<vars.deposits+vars.swaps; i++) {
866 Time testTime =
867 Actual360().yearFraction(d1: vars.settlement,
868 d2: vars.instruments[i]->pillarDate());
869 DiscountFactor discount = vars.termStructure->discount(t: testTime);
870 f.lower();
871 vars.rates[i]->setValue(vars.rates[i]->value()*1.01);
872 if (!f.isUp())
873 BOOST_FAIL("Observer was not notified of underlying rate change");
874 if (vars.termStructure->discount(t: testTime,extrapolate: true) == discount)
875 BOOST_FAIL("rate change did not trigger recalculation");
876 vars.rates[i]->setValue(vars.rates[i]->value()/1.01);
877 }
878
879 vars.termStructure->maxDate();
880 f.lower();
881 Settings::instance().evaluationDate() =
882 vars.calendar.advance(vars.today,n: 15,unit: Days);
883 if (!f.isUp())
884 BOOST_FAIL("Observer was not notified of date change");
885
886 f.lower();
887 Settings::instance().evaluationDate() = vars.today;
888 if (f.isUp())
889 BOOST_FAIL("Observer was notified of date change"
890 " without an intervening recalculation");
891}
892
893
894void PiecewiseYieldCurveTest::testLiborFixing() {
895
896 BOOST_TEST_MESSAGE(
897 "Testing use of today's LIBOR fixings in swap curve...");
898
899 using namespace piecewise_yield_curve_test;
900
901 CommonVars vars;
902
903 std::vector<ext::shared_ptr<RateHelper> > swapHelpers(vars.swaps);
904 ext::shared_ptr<IborIndex> euribor6m(new Euribor6M);
905
906 for (Size i=0; i<vars.swaps; i++) {
907 Handle<Quote> r(vars.rates[i+vars.deposits]);
908 swapHelpers[i] = ext::shared_ptr<RateHelper>(new
909 SwapRateHelper(r, Period(swapData[i].n, swapData[i].units),
910 vars.calendar,
911 vars.fixedLegFrequency, vars.fixedLegConvention,
912 vars.fixedLegDayCounter, euribor6m));
913 }
914
915 vars.termStructure = ext::shared_ptr<YieldTermStructure>(new
916 PiecewiseYieldCurve<Discount,LogLinear>(vars.settlement,
917 swapHelpers,
918 Actual360()));
919
920 Handle<YieldTermStructure> curveHandle =
921 Handle<YieldTermStructure>(vars.termStructure);
922
923 ext::shared_ptr<IborIndex> index(new Euribor6M(curveHandle));
924 for (Size i=0; i<vars.swaps; i++) {
925 Period tenor = swapData[i].n*swapData[i].units;
926
927 VanillaSwap swap = MakeVanillaSwap(tenor, index, 0.0)
928 .withEffectiveDate(vars.settlement)
929 .withFixedLegDayCount(dc: vars.fixedLegDayCounter)
930 .withFixedLegTenor(t: Period(vars.fixedLegFrequency))
931 .withFixedLegConvention(bdc: vars.fixedLegConvention)
932 .withFixedLegTerminationDateConvention(bdc: vars.fixedLegConvention);
933
934 Rate expectedRate = swapData[i].rate/100,
935 estimatedRate = swap.fairRate();
936 Real tolerance = 1.0e-9;
937 if (std::fabs(x: expectedRate-estimatedRate) > tolerance) {
938 BOOST_ERROR("before LIBOR fixing:\n"
939 << swapData[i].n << " year(s) swap:\n"
940 << std::setprecision(8)
941 << " estimated rate: "
942 << io::rate(estimatedRate) << "\n"
943 << " expected rate: "
944 << io::rate(expectedRate));
945 }
946 }
947
948 Flag f;
949 f.registerWith(h: vars.termStructure);
950 f.lower();
951
952 index->addFixing(fixingDate: vars.today, fixing: 0.0425);
953
954 if (!f.isUp())
955 BOOST_ERROR("Observer was not notified of rate fixing");
956
957 for (Size i=0; i<vars.swaps; i++) {
958 Period tenor = swapData[i].n*swapData[i].units;
959
960 VanillaSwap swap = MakeVanillaSwap(tenor, index, 0.0)
961 .withEffectiveDate(vars.settlement)
962 .withFixedLegDayCount(dc: vars.fixedLegDayCounter)
963 .withFixedLegTenor(t: Period(vars.fixedLegFrequency))
964 .withFixedLegConvention(bdc: vars.fixedLegConvention)
965 .withFixedLegTerminationDateConvention(bdc: vars.fixedLegConvention);
966
967 Rate expectedRate = swapData[i].rate/100,
968 estimatedRate = swap.fairRate();
969 Real tolerance = 1.0e-9;
970 if (std::fabs(x: expectedRate-estimatedRate) > tolerance) {
971 BOOST_ERROR("after LIBOR fixing:\n"
972 << swapData[i].n << " year(s) swap:\n"
973 << std::setprecision(8)
974 << " estimated rate: "
975 << io::rate(estimatedRate) << "\n"
976 << " expected rate: "
977 << io::rate(expectedRate));
978 }
979 }
980}
981
982void PiecewiseYieldCurveTest::testJpyLibor() {
983 BOOST_TEST_MESSAGE(
984 "Testing bootstrap over JPY LIBOR swaps...");
985
986 using namespace piecewise_yield_curve_test;
987
988 CommonVars vars;
989
990 vars.today = Date(4, October, 2007);
991 Settings::instance().evaluationDate() = vars.today;
992
993 vars.calendar = Japan();
994 vars.settlement =
995 vars.calendar.advance(vars.today,n: vars.settlementDays,unit: Days);
996
997 // market elements
998 vars.rates = std::vector<ext::shared_ptr<SimpleQuote> >(vars.swaps);
999 for (Size i=0; i<vars.swaps; i++) {
1000 vars.rates[i] = ext::make_shared<SimpleQuote>(
1001 args: swapData[i].rate/100);
1002 }
1003
1004 // rate helpers
1005 vars.instruments = std::vector<ext::shared_ptr<RateHelper> >(vars.swaps);
1006
1007 ext::shared_ptr<IborIndex> index(new JPYLibor(6*Months));
1008 for (Size i=0; i<vars.swaps; i++) {
1009 Handle<Quote> r(vars.rates[i]);
1010 vars.instruments[i] = ext::shared_ptr<RateHelper>(
1011 new SwapRateHelper(r, swapData[i].n*swapData[i].units,
1012 vars.calendar,
1013 vars.fixedLegFrequency, vars.fixedLegConvention,
1014 vars.fixedLegDayCounter, index));
1015 }
1016
1017 vars.termStructure = ext::shared_ptr<YieldTermStructure>(
1018 new PiecewiseYieldCurve<Discount,LogLinear>(
1019 vars.settlement, vars.instruments,
1020 Actual360()));
1021
1022 RelinkableHandle<YieldTermStructure> curveHandle;
1023 curveHandle.linkTo(h: vars.termStructure);
1024
1025 // check swaps
1026 ext::shared_ptr<IborIndex> jpylibor6m(new JPYLibor(6*Months,curveHandle));
1027 for (Size i=0; i<vars.swaps; i++) {
1028 Period tenor = swapData[i].n*swapData[i].units;
1029
1030 VanillaSwap swap = MakeVanillaSwap(tenor, jpylibor6m, 0.0)
1031 .withEffectiveDate(vars.settlement)
1032 .withFixedLegDayCount(dc: vars.fixedLegDayCounter)
1033 .withFixedLegTenor(t: Period(vars.fixedLegFrequency))
1034 .withFixedLegConvention(bdc: vars.fixedLegConvention)
1035 .withFixedLegTerminationDateConvention(bdc: vars.fixedLegConvention)
1036 .withFixedLegCalendar(cal: vars.calendar)
1037 .withFloatingLegCalendar(cal: vars.calendar);
1038
1039 Rate expectedRate = swapData[i].rate/100,
1040 estimatedRate = swap.fairRate();
1041 Spread error = std::fabs(x: expectedRate-estimatedRate);
1042 Real tolerance = 1.0e-9;
1043
1044 if (error > tolerance) {
1045 BOOST_ERROR(swapData[i].n << " year(s) swap:\n"
1046 << std::setprecision(8)
1047 << "\n estimated rate: " << io::rate(estimatedRate)
1048 << "\n expected rate: " << io::rate(expectedRate)
1049 << "\n error: " << io::rate(error)
1050 << "\n tolerance: " << io::rate(tolerance));
1051 }
1052 }
1053}
1054
1055void PiecewiseYieldCurveTest::testDefaultInstantiation() {
1056
1057 BOOST_TEST_MESSAGE("Testing instantiation of curves without passing an interpolator...");
1058
1059 using namespace piecewise_yield_curve_test;
1060
1061 CommonVars vars;
1062
1063 // no actual tests at runtime; this tests that all these instantiations compile
1064 PiecewiseYieldCurve<Discount, Linear> linear(vars.settlement, vars.instruments, Actual360());
1065 PiecewiseYieldCurve<Discount, LogLinear> log_linear(vars.settlement, vars.instruments, Actual360());
1066 PiecewiseYieldCurve<Discount, Cubic> cubic(vars.settlement, vars.instruments, Actual360());
1067 PiecewiseYieldCurve<Discount, DefaultLogCubic> log_cubic(vars.settlement, vars.instruments, Actual360());
1068 PiecewiseYieldCurve<Discount, MonotonicLogCubic> monotonic_log_cubic(vars.settlement, vars.instruments, Actual360());
1069 PiecewiseYieldCurve<Discount, KrugerLog> kruger_log_cubic(vars.settlement, vars.instruments, Actual360());
1070 PiecewiseYieldCurve<ForwardRate, BackwardFlat> backward(vars.settlement, vars.instruments, Actual360());
1071 PiecewiseYieldCurve<ForwardRate, ForwardFlat> forward(vars.settlement, vars.instruments, Actual360());
1072 PiecewiseYieldCurve<ForwardRate, ConvexMonotone> convex(vars.settlement, vars.instruments, Actual360());
1073}
1074
1075void PiecewiseYieldCurveTest::testSwapRateHelperLastRelevantDate() {
1076 BOOST_TEST_MESSAGE("Testing SwapRateHelper last relevant date...");
1077
1078 Settings::instance().evaluationDate() = Date(22, Dec, 2016);
1079 Date today = Settings::instance().evaluationDate();
1080
1081 Handle<YieldTermStructure> flat3m(
1082 ext::make_shared<FlatForward>(args&: today, args: Handle<Quote>(ext::make_shared<SimpleQuote>(args: 0.02)), args: Actual365Fixed()));
1083 ext::shared_ptr<IborIndex> usdLibor3m = ext::make_shared<USDLibor>(args: 3 * Months, args&: flat3m);
1084
1085 // note that the calendar should be US+UK here actually, but technically it should also work with
1086 // the US calendar only
1087 ext::shared_ptr<RateHelper> helper = ext::make_shared<SwapRateHelper>(
1088 args: 0.02, args: 50 * Years, args: UnitedStates(UnitedStates::GovernmentBond), args: Semiannual, args: ModifiedFollowing,
1089 args: Thirty360(Thirty360::BondBasis), args&: usdLibor3m);
1090
1091 PiecewiseYieldCurve<Discount, LogLinear> curve(today, std::vector<ext::shared_ptr<RateHelper> >(1, helper),
1092 Actual365Fixed());
1093 BOOST_CHECK_NO_THROW(curve.discount(1.0));
1094}
1095
1096void PiecewiseYieldCurveTest::testSwapRateHelperSpotDate() {
1097 BOOST_TEST_MESSAGE("Testing SwapRateHelper spot date...");
1098
1099 ext::shared_ptr<IborIndex> usdLibor3m = ext::make_shared<USDLibor>(args: 3 * Months);
1100
1101 ext::shared_ptr<SwapRateHelper> helper = ext::make_shared<SwapRateHelper>(
1102 args: 0.02, args: 5 * Years, args: UnitedStates(UnitedStates::GovernmentBond), args: Semiannual, args: ModifiedFollowing,
1103 args: Thirty360(Thirty360::BondBasis), args&: usdLibor3m);
1104
1105 Settings::instance().evaluationDate() = Date(11, October, 2019);
1106
1107 // Advancing 2 days on the US calendar would yield October 16th (because October 14th
1108 // is Columbus day), but the LIBOR spot is calculated advancing on the UK calendar,
1109 // resulting in October 15th which is also a business day for the US calendar.
1110 Date expected = Date(15, October, 2019);
1111 Date calculated = helper->swap()->startDate();
1112 if (calculated != expected)
1113 BOOST_ERROR("expected spot date: " << expected << "\n"
1114 "calculated: " << calculated);
1115
1116 // Settings::instance().evaluationDate() = Date(1, July, 2020);
1117
1118 // TODO: July 3rd is holiday in the US, but not for LIBOR purposes. This should probably
1119 // be considered when building the schedule.
1120 // expected = Date(3, July, 2020);
1121 // calculated = helper->swap()->startDate();
1122 // if (calculated != expected)
1123 // BOOST_ERROR("expected spot date: " << expected << "\n"
1124 // "calculated: " << calculated);
1125}
1126
1127void PiecewiseYieldCurveTest::testBadPreviousCurve() {
1128 BOOST_TEST_MESSAGE("Testing bootstrap starting from bad guess...");
1129
1130 using namespace piecewise_yield_curve_test;
1131
1132 Datum data[] = {
1133 { .n: 1, .units: Weeks, .rate: -0.003488 },
1134 { .n: 2, .units: Weeks, .rate: -0.0033 },
1135 { .n: 6, .units: Months, .rate: -0.00339 },
1136 { .n: 2, .units: Years, .rate: -0.00336 },
1137 { .n: 8, .units: Years, .rate: 0.00302 },
1138 { .n: 50, .units: Years, .rate: 0.01185 }
1139 };
1140
1141 std::vector<ext::shared_ptr<RateHelper> > helpers;
1142 ext::shared_ptr<Euribor> euribor1m(new Euribor1M);
1143 for (auto& i : data) {
1144 helpers.push_back(x: ext::make_shared<SwapRateHelper>(
1145 args&: i.rate, args: Period(i.n, i.units), args: TARGET(), args: Monthly, args: Unadjusted,
1146 args: Thirty360(Thirty360::BondBasis), args&: euribor1m));
1147 }
1148
1149 Date today = Date(12, October, 2017);
1150 Date test_date = Date(16, December, 2016);
1151
1152 Settings::instance().evaluationDate() = today;
1153
1154 ext::shared_ptr<YieldTermStructure> curve =
1155 ext::make_shared<PiecewiseYieldCurve<ForwardRate, BackwardFlat> >(
1156 args&: test_date, args&: helpers, args: Actual360());
1157
1158 // force bootstrap on today's date, so we have a previous curve...
1159 curve->discount(t: 1.0);
1160
1161 // ...then move to a date where the previous curve is a bad guess.
1162 Settings::instance().evaluationDate() = test_date;
1163
1164 RelinkableHandle<YieldTermStructure> h;
1165 h.linkTo(h: curve);
1166
1167 ext::shared_ptr<Euribor1M> index = ext::make_shared<Euribor1M>(args&: h);
1168 for (auto& i : data) {
1169 Period tenor = i.n * i.units;
1170
1171 VanillaSwap swap = MakeVanillaSwap(tenor, index, 0.0)
1172 .withFixedLegDayCount(dc: Thirty360(Thirty360::BondBasis))
1173 .withFixedLegTenor(t: Period(1, Months))
1174 .withFixedLegConvention(bdc: Unadjusted);
1175 swap.setPricingEngine(ext::make_shared<DiscountingSwapEngine>(args&: h));
1176
1177 Rate expectedRate = i.rate, estimatedRate = swap.fairRate();
1178 Spread error = std::fabs(x: expectedRate-estimatedRate);
1179 Real tolerance = 1.0e-9;
1180 if (error > tolerance) {
1181 BOOST_ERROR(tenor << " swap:\n"
1182 << std::setprecision(8)
1183 << "\n estimated rate: " << io::rate(estimatedRate)
1184 << "\n expected rate: " << io::rate(expectedRate)
1185 << "\n error: " << io::rate(error)
1186 << "\n tolerance: " << io::rate(tolerance));
1187 }
1188 }
1189}
1190
1191void PiecewiseYieldCurveTest::testConstructionWithExplicitBootstrap() {
1192
1193 BOOST_TEST_MESSAGE("Testing that construction with an explicit bootstrap succeeds...");
1194
1195 using namespace piecewise_yield_curve_test;
1196
1197 CommonVars vars;
1198
1199 // With an explicit IterativeBootstrap object
1200 typedef PiecewiseYieldCurve<ForwardRate, Linear, IterativeBootstrap> PwLinearForward;
1201 ext::shared_ptr<YieldTermStructure> yts =
1202 ext::make_shared<PwLinearForward>(
1203 args&: vars.settlement, args&: vars.instruments, args: Actual360(), args: Linear(),
1204 args: PwLinearForward::bootstrap_type());
1205
1206 // Check anything to show that the construction succeeded
1207 BOOST_CHECK_NO_THROW(yts->discount(1.0, true));
1208
1209 // With an explicit LocalBootstrap object
1210 typedef PiecewiseYieldCurve<ForwardRate, ConvexMonotone, LocalBootstrap> PwCmForward;
1211 yts = ext::make_shared<PwCmForward>(
1212 args&: vars.settlement, args&: vars.instruments, args: Actual360(), args: ConvexMonotone(),
1213 args: PwCmForward::bootstrap_type());
1214
1215 BOOST_CHECK_NO_THROW(yts->discount(1.0, true));
1216}
1217
1218void PiecewiseYieldCurveTest::testLargeRates() {
1219 BOOST_TEST_MESSAGE("Testing bootstrap with large input rates...");
1220
1221 using namespace piecewise_yield_curve_test;
1222
1223 Datum data[] = {
1224 { .n: 1, .units: Weeks, .rate: 2.418633 },
1225 { .n: 2, .units: Weeks, .rate: 1.361540 },
1226 { .n: 3, .units: Weeks, .rate: 1.195362 },
1227 { .n: 1, .units: Months, .rate: 0.829009 }
1228 };
1229
1230 std::vector<ext::shared_ptr<RateHelper> > helpers;
1231 for (auto& i : data) {
1232 helpers.push_back(x: ext::make_shared<DepositRateHelper>(
1233 args&: i.rate, args: Period(i.n, i.units), args: 0, args: WeekendsOnly(), args: Following, args: false, args: Actual360()));
1234 }
1235
1236 Date today = Date(12, October, 2017);
1237
1238 Settings::instance().evaluationDate() = today;
1239
1240 Real accuracy = Null<Real>(); // use the default
1241 Real minValue = Null<Real>(); // use the default
1242 Real maxValue = 3.0; // override
1243
1244 typedef PiecewiseYieldCurve<ForwardRate, BackwardFlat> PiecewiseCurve;
1245 ext::shared_ptr<YieldTermStructure> curve =
1246 ext::make_shared<PiecewiseCurve>(
1247 args&: today, args&: helpers, args: Actual360(), args: BackwardFlat(),
1248 args: PiecewiseCurve::bootstrap_type(accuracy, minValue, maxValue));
1249
1250 // force bootstrap and check it worked
1251 curve->discount(t: 0.01);
1252 BOOST_CHECK_NO_THROW(curve->discount(0.01));
1253}
1254
1255namespace piecewise_yield_curve_test {
1256 // helper classes for testGlobalBootstrap() below:
1257
1258 // functor returning the additional error terms for the cost function
1259 struct additionalErrors {
1260 explicit additionalErrors(
1261 std::vector<ext::shared_ptr<BootstrapHelper<YieldTermStructure> > > additionalHelpers)
1262 : additionalHelpers(std::move(additionalHelpers)) {}
1263 std::vector<ext::shared_ptr<BootstrapHelper<YieldTermStructure> > > additionalHelpers;
1264 Array operator()() {
1265 Array errors(5);
1266 Real a = additionalHelpers[0]->impliedQuote();
1267 Real b = additionalHelpers[6]->impliedQuote();
1268 for (Size k = 0; k < 5; ++k) {
1269 errors[k] = (5.0 - k) / 6.0 * a + (1.0 + k) / 6.0 * b -
1270 additionalHelpers[1 + k]->impliedQuote();
1271 }
1272 return errors;
1273 }
1274 };
1275
1276 // functor returning additional dates used in the bootstrap
1277 struct additionalDates {
1278 std::vector<Date> operator()() {
1279 Date settl = TARGET().advance(date: Settings::instance().evaluationDate(), period: 2 * Days);
1280 std::vector<Date> dates;
1281 for (Size i = 0; i < 5; ++i)
1282 dates.push_back(x: TARGET().advance(date: settl, period: (1 + i) * Months));
1283 return dates;
1284 }
1285 };
1286}
1287
1288void PiecewiseYieldCurveTest::testGlobalBootstrap() {
1289
1290 BOOST_TEST_MESSAGE("Testing global bootstrap...");
1291
1292 using namespace piecewise_yield_curve_test;
1293
1294 Date today(26, Sep, 2019);
1295 Settings::instance().evaluationDate() = today;
1296
1297 // market rates
1298 Real refMktRate[] = {-0.373, -0.388, -0.402, -0.418, -0.431, -0.441, -0.45,
1299 -0.457, -0.463, -0.469, -0.461, -0.463, -0.479, -0.4511,
1300 -0.45418, -0.439, -0.4124, -0.37703, -0.3335, -0.28168, -0.22725,
1301 -0.1745, -0.12425, -0.07746, 0.0385, 0.1435, 0.17525, 0.17275,
1302 0.1515, 0.1225, 0.095, 0.0644};
1303
1304 // expected outputs
1305 Date refDate[] = {
1306 Date(31, Mar, 2020), Date(30, Apr, 2020), Date(29, May, 2020), Date(30, Jun, 2020),
1307 Date(31, Jul, 2020), Date(31, Aug, 2020), Date(30, Sep, 2020), Date(30, Oct, 2020),
1308 Date(30, Nov, 2020), Date(31, Dec, 2020), Date(29, Jan, 2021), Date(26, Feb, 2021),
1309 Date(31, Mar, 2021), Date(30, Sep, 2021), Date(30, Sep, 2022), Date(29, Sep, 2023),
1310 Date(30, Sep, 2024), Date(30, Sep, 2025), Date(30, Sep, 2026), Date(30, Sep, 2027),
1311 Date(29, Sep, 2028), Date(28, Sep, 2029), Date(30, Sep, 2030), Date(30, Sep, 2031),
1312 Date(29, Sep, 2034), Date(30, Sep, 2039), Date(30, Sep, 2044), Date(30, Sep, 2049),
1313 Date(30, Sep, 2054), Date(30, Sep, 2059), Date(30, Sep, 2064), Date(30, Sep, 2069)};
1314
1315 Real refZeroRate[] = {-0.00373354, -0.00381005, -0.00387689, -0.00394124, -0.00407706, -0.00413633, -0.00411935,
1316 -0.00416370, -0.00420557, -0.00424431, -0.00427824, -0.00430977, -0.00434401, -0.00445243,
1317 -0.00448506, -0.00433690, -0.00407401, -0.00372752, -0.00330050, -0.00279139, -0.00225477,
1318 -0.00173422, -0.00123688, -0.00077237, 0.00038554, 0.00144248, 0.00175995, 0.00172873,
1319 0.00150782, 0.00121145, 0.000933912, 0.000628946};
1320
1321 // build ql helpers
1322 std::vector<ext::shared_ptr<RateHelper> > helpers;
1323 ext::shared_ptr<IborIndex> index = ext::make_shared<Euribor>(args: 6 * Months);
1324
1325 helpers.push_back(x: ext::make_shared<DepositRateHelper>(
1326 args: refMktRate[0] / 100.0, args: 6 * Months, args: 2, args: TARGET(), args: ModifiedFollowing, args: true, args: Actual360()));
1327
1328 for (Size i = 0; i < 12; ++i) {
1329 helpers.push_back(
1330 x: ext::make_shared<FraRateHelper>(args: refMktRate[1 + i] / 100.0, args: (i + 1) * Months, args&: index));
1331 }
1332
1333 Size swapTenors[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 20, 25, 30, 35, 40, 45, 50};
1334 for (Size i = 0; i < 19; ++i) {
1335 helpers.push_back(x: ext::make_shared<SwapRateHelper>(
1336 args: refMktRate[13 + i] / 100.0, args: swapTenors[i] * Years, args: TARGET(), args: Annual, args: ModifiedFollowing,
1337 args: Thirty360(Thirty360::BondBasis), args&: index));
1338 }
1339
1340 // global bootstrap constraints
1341 std::vector<ext::shared_ptr<BootstrapHelper<YieldTermStructure> > > additionalHelpers;
1342
1343 // set up the additional rate helpers we need in the cost function
1344 for (Size i = 0; i < 7; ++i) {
1345 additionalHelpers.push_back(
1346 x: ext::make_shared<FraRateHelper>(args: -0.004, args: (12 + i) * Months, args&: index));
1347 }
1348
1349 // build curve with additional dates and constraints using a global bootstrapper
1350 typedef PiecewiseYieldCurve<SimpleZeroYield, Linear, GlobalBootstrap> Curve;
1351 ext::shared_ptr<Curve> curve = ext::make_shared<Curve>(
1352 args: 2, args: TARGET(), args&: helpers, args: Actual365Fixed(), args: std::vector<Handle<Quote> >(), args: std::vector<Date>(),
1353 args: Linear(),
1354 args: Curve::bootstrap_type(additionalHelpers, additionalDates(),
1355 additionalErrors(additionalHelpers), 1.0e-12));
1356 curve->enableExtrapolation();
1357
1358 // check expected pillar dates
1359 for (Size i = 0; i < LENGTH(refDate); ++i) {
1360 BOOST_CHECK_EQUAL(refDate[i], helpers[i]->pillarDate());
1361 }
1362
1363 // check expected zero rates
1364 for (Size i = 0; i < LENGTH(refZeroRate); ++i) {
1365 // 0.01 basis points tolerance
1366 QL_CHECK_SMALL(std::fabs(refZeroRate[i] - curve->zeroRate(refDate[i], Actual360(), Continuous).rate()),
1367 1E-6);
1368 }
1369}
1370
1371/* This test attempts to build an ARS collateralised in USD curve as of 25 Sep 2019. Using the default
1372 IterativeBootstrap with no retries, the yield curve building fails. Allowing retries, it expands the min and max
1373 bounds and passes.
1374*/
1375void PiecewiseYieldCurveTest::testIterativeBootstrapRetries() {
1376
1377 BOOST_TEST_MESSAGE("Testing iterative bootstrap with retries...");
1378
1379 Date asof(25, Sep, 2019);
1380 Settings::instance().evaluationDate() = asof;
1381 Actual365Fixed tsDayCounter;
1382
1383 // USD discount curve built out of FedFunds OIS swaps.
1384 vector<Date> usdCurveDates = {
1385 Date(25, Sep, 2019),
1386 Date(26, Sep, 2019),
1387 Date(8, Oct, 2019),
1388 Date(16, Oct, 2019),
1389 Date(22, Oct, 2019),
1390 Date(30, Oct, 2019),
1391 Date(2, Dec, 2019),
1392 Date(31, Dec, 2019),
1393 Date(29, Jan, 2020),
1394 Date(2, Mar, 2020),
1395 Date(31, Mar, 2020),
1396 Date(29, Apr, 2020),
1397 Date(29, May, 2020),
1398 Date(1, Jul, 2020),
1399 Date(29, Jul, 2020),
1400 Date(31, Aug, 2020),
1401 Date(30, Sep, 2020)
1402 };
1403
1404 vector<DiscountFactor> usdCurveDfs = {
1405 1.000000000,
1406 0.999940837,
1407 0.999309357,
1408 0.998894646,
1409 0.998574816,
1410 0.998162528,
1411 0.996552511,
1412 0.995197584,
1413 0.993915264,
1414 0.992530008,
1415 0.991329696,
1416 0.990179606,
1417 0.989005698,
1418 0.987751691,
1419 0.986703371,
1420 0.985495036,
1421 0.984413446
1422 };
1423
1424 Handle<YieldTermStructure> usdYts(ext::make_shared<InterpolatedDiscountCurve<LogLinear> >(
1425 args&: usdCurveDates, args&: usdCurveDfs, args&: tsDayCounter));
1426
1427 // USD/ARS forward points
1428 Handle<Quote> arsSpot(ext::make_shared<SimpleQuote>(args: 56.881));
1429 map<Period, Real> arsFwdPoints = {
1430 {1 * Months, 8.5157},
1431 {2 * Months, 12.7180},
1432 {3 * Months, 17.8310},
1433 {6 * Months, 30.3680},
1434 {9 * Months, 45.5520},
1435 {1 * Years, 60.7370}
1436 };
1437
1438 // Create the FX swap rate helpers for the ARS in USD curve.
1439 vector<ext::shared_ptr<RateHelper> > instruments;
1440 for (map<Period, Real>::const_iterator it = arsFwdPoints.begin(); it != arsFwdPoints.end(); ++it) {
1441 Handle<Quote> arsFwd(ext::make_shared<SimpleQuote>(args: it->second));
1442 instruments.push_back(x: ext::make_shared<FxSwapRateHelper>(args&: arsFwd, args&: arsSpot, args: it->first, args: 2,
1443 args: UnitedStates(UnitedStates::GovernmentBond), args: Following, args: false, args: true, args&: usdYts));
1444 }
1445
1446 // Create the ARS in USD curve with the default IterativeBootstrap.
1447 typedef PiecewiseYieldCurve<Discount, LogLinear, IterativeBootstrap> LLDFCurve;
1448 ext::shared_ptr<YieldTermStructure> arsYts = ext::make_shared<LLDFCurve>(args&: asof, args&: instruments, args&: tsDayCounter);
1449
1450 // USD/ARS spot date. The date on which we check the ARS discount curve.
1451 Date spotDate(27, Sep, 2019);
1452
1453 // Check that the ARS in USD curve throws by requesting a discount factor.
1454 BOOST_CHECK_EXCEPTION(arsYts->discount(spotDate), Error,
1455 ExpectedErrorMessage("1st iteration: failed at 1st alive instrument"));
1456
1457 // Create the ARS in USD curve with an IterativeBootstrap allowing for 4 retries.
1458 IterativeBootstrap<LLDFCurve> ib(Null<Real>(), Null<Real>(), Null<Real>(), 5);
1459 arsYts = ext::make_shared<LLDFCurve>(args&: asof, args&: instruments, args&: tsDayCounter, args&: ib);
1460
1461 // Check that the ARS in USD curve builds and populate the spot ARS discount factor.
1462 DiscountFactor spotDfArs = 1.0;
1463 BOOST_REQUIRE_NO_THROW(spotDfArs = arsYts->discount(spotDate));
1464
1465 // Additional dates and discount factors used in the final check i.e. that calculated 1Y FX forward equals input.
1466 Date oneYearFwdDate(28, Sep, 2020);
1467 DiscountFactor spotDfUsd = usdYts->discount(d: spotDate);
1468 DiscountFactor oneYearDfUsd = usdYts->discount(d: oneYearFwdDate);
1469
1470 // Given that the ARS in USD curve builds, check that the 1Y USD/ARS forward rate is as expected.
1471 DiscountFactor oneYearDfArs = arsYts->discount(d: oneYearFwdDate);
1472 Real calcFwd = (spotDfArs * arsSpot->value() / oneYearDfArs) / (spotDfUsd / oneYearDfUsd);
1473 Real expFwd = arsSpot->value() + arsFwdPoints.at(k: 1 * Years);
1474 QL_CHECK_SMALL(calcFwd - expFwd, 1e-10);
1475}
1476
1477test_suite* PiecewiseYieldCurveTest::suite() {
1478
1479 auto* suite = BOOST_TEST_SUITE("Piecewise yield curve tests");
1480
1481 // unstable
1482 // suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testLogCubicDiscountConsistency));
1483 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testLogLinearDiscountConsistency));
1484 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testLinearDiscountConsistency));
1485
1486 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testLinearZeroConsistency));
1487 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testSplineZeroConsistency));
1488
1489 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testLinearForwardConsistency));
1490 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testFlatForwardConsistency));
1491 // unstable
1492 // suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testSplineForwardConsistency));
1493
1494 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testConvexMonotoneForwardConsistency));
1495 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testLocalBootstrapConsistency));
1496
1497 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testParFraRegression));
1498
1499 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testObservability));
1500 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testLiborFixing));
1501
1502 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testDefaultInstantiation));
1503
1504 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testJpyLibor));
1505
1506 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testSwapRateHelperLastRelevantDate));
1507 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testSwapRateHelperSpotDate));
1508
1509 if (IborCoupon::Settings::instance().usingAtParCoupons()) {
1510 // This regression test didn't work with indexed coupons anyway.
1511 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testBadPreviousCurve));
1512 }
1513
1514 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testConstructionWithExplicitBootstrap));
1515 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testLargeRates));
1516
1517 if (IborCoupon::Settings::instance().usingAtParCoupons()) {
1518 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testGlobalBootstrap));
1519 }
1520
1521 suite->add(QUANTLIB_TEST_CASE(&PiecewiseYieldCurveTest::testIterativeBootstrapRetries));
1522
1523 return suite;
1524}
1525

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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