1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3/*
4 Copyright (C) 2018 StatPro Italia srl
5 Copyright (C) 2021, 2022 Ralf Konrad Eckel
6
7 This file is part of QuantLib, a free-software/open-source library
8 for financial quantitative analysts and developers - http://quantlib.org/
9
10 QuantLib is free software: you can redistribute it and/or modify it
11 under the terms of the QuantLib license. You should have received a
12 copy of the license along with this program; if not, please email
13 <quantlib-dev@lists.sf.net>. The license is also available online at
14 <http://quantlib.org/license.shtml>.
15
16 This program is distributed in the hope that it will be useful, but WITHOUT
17 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18 FOR A PARTICULAR PURPOSE. See the license for more details.
19*/
20
21#include "callablebonds.hpp"
22#include "utilities.hpp"
23#include <ql/experimental/callablebonds/callablebond.hpp>
24#include <ql/experimental/callablebonds/treecallablebondengine.hpp>
25#include <ql/experimental/callablebonds/blackcallablebondengine.hpp>
26#include <ql/instruments/bonds/zerocouponbond.hpp>
27#include <ql/instruments/bonds/fixedratebond.hpp>
28#include <ql/pricingengines/bond/discountingbondengine.hpp>
29#include <ql/time/daycounters/thirty360.hpp>
30#include <ql/time/daycounters/actual365fixed.hpp>
31#include <ql/time/calendars/nullcalendar.hpp>
32#include <ql/time/calendars/target.hpp>
33#include <ql/time/calendars/unitedstates.hpp>
34#include <ql/time/schedule.hpp>
35#include <ql/termstructures/yield/flatforward.hpp>
36#include <ql/models/shortrate/onefactormodels/hullwhite.hpp>
37#include <ql/shared_ptr.hpp>
38#include <iomanip>
39
40using namespace QuantLib;
41using namespace boost::unit_test_framework;
42
43namespace {
44
45 struct Globals {
46 // global data
47 Date today, settlement;
48 Calendar calendar;
49 DayCounter dayCounter;
50 BusinessDayConvention rollingConvention;
51
52 RelinkableHandle<YieldTermStructure> termStructure;
53 RelinkableHandle<ShortRateModel> model;
54
55 Date issueDate() const {
56 // ensure that we're in mid-coupon
57 return calendar.adjust(today - 100*Days);
58 }
59
60 Date maturityDate() const {
61 // ensure that we're in mid-coupon
62 return calendar.advance(issueDate(),n: 10,unit: Years);
63 }
64
65 std::vector<Date> evenYears() const {
66 std::vector<Date> dates;
67 for (Size i=2; i<10; i+=2)
68 dates.push_back(x: calendar.advance(issueDate(),n: i,unit: Years));
69 return dates;
70 }
71
72 std::vector<Date> oddYears() const {
73 std::vector<Date> dates;
74 for (Size i=1; i<10; i+=2)
75 dates.push_back(x: calendar.advance(issueDate(),n: i,unit: Years));
76 return dates;
77 }
78
79 template <class R>
80 ext::shared_ptr<YieldTermStructure> makeFlatCurve(const R& r) const {
81 return ext::shared_ptr<YieldTermStructure>(
82 new FlatForward(settlement, r, dayCounter));
83 }
84
85 Globals() {
86 calendar = TARGET();
87 dayCounter = Actual365Fixed();
88 rollingConvention = ModifiedFollowing;
89
90 today = Settings::instance().evaluationDate();
91 settlement = calendar.advance(today,n: 2,unit: Days);
92 }
93 };
94
95}
96
97void CallableBondTest::testInterplay() {
98
99 BOOST_TEST_MESSAGE("Testing interplay of callability and puttability for callable bonds...");
100
101 Globals vars;
102
103 vars.termStructure.linkTo(h: vars.makeFlatCurve(r: 0.03));
104 vars.model.linkTo(h: ext::make_shared<HullWhite>(args&: vars.termStructure));
105
106 Size timeSteps = 240;
107
108 ext::shared_ptr<PricingEngine> engine =
109 ext::make_shared<TreeCallableZeroCouponBondEngine>(
110 args: *(vars.model), args&: timeSteps, args&: vars.termStructure);
111
112 /* case 1: an earlier out-of-the-money callability must prevent
113 a later in-the-money puttability
114 */
115
116 CallabilitySchedule callabilities;
117
118 callabilities.push_back(x: ext::make_shared<Callability>(
119 args: Bond::Price(100.0, Bond::Price::Clean),
120 args: Callability::Call,
121 args: vars.calendar.advance(vars.issueDate(),n: 4,unit: Years)));
122
123 callabilities.push_back(x: ext::make_shared<Callability>(
124 args: Bond::Price(1000.0, Bond::Price::Clean),
125 args: Callability::Put,
126 args: vars.calendar.advance(vars.issueDate(),n: 6,unit: Years)));
127
128 CallableZeroCouponBond bond(3, 100.0, vars.calendar,
129 vars.maturityDate(), Thirty360(Thirty360::BondBasis),
130 vars.rollingConvention, 100.0,
131 vars.issueDate(), callabilities);
132 bond.setPricingEngine(engine);
133
134 Real expected = callabilities[0]->price().amount() *
135 vars.termStructure->discount(d: callabilities[0]->date()) /
136 vars.termStructure->discount(d: bond.settlementDate());
137
138 if (std::fabs(x: bond.settlementValue() - expected) > 1.0e-2)
139 BOOST_ERROR(
140 "callability not exercised correctly:\n"
141 << std::setprecision(5)
142 << " calculated NPV: " << bond.settlementValue() << "\n"
143 << " expected: " << expected << "\n"
144 << " difference: " << bond.settlementValue()-expected);
145
146 /* case 2: same as case 1, with an added callability later on */
147
148 callabilities.push_back(x: ext::make_shared<Callability>(
149 args: Bond::Price(100.0, Bond::Price::Clean),
150 args: Callability::Call,
151 args: vars.calendar.advance(vars.issueDate(),n: 8,unit: Years)));
152
153 bond = CallableZeroCouponBond(3, 100.0, vars.calendar,
154 vars.maturityDate(), Thirty360(Thirty360::BondBasis),
155 vars.rollingConvention, 100.0,
156 vars.issueDate(), callabilities);
157 bond.setPricingEngine(engine);
158
159 if (std::fabs(x: bond.settlementValue() - expected) > 1.0e-2)
160 BOOST_ERROR(
161 "callability not exercised correctly:\n"
162 << std::setprecision(5)
163 << " calculated NPV: " << bond.settlementValue() << "\n"
164 << " expected: " << expected << "\n"
165 << " difference: " << bond.settlementValue()-expected);
166
167 /* case 3: an earlier in-the-money puttability must prevent
168 a later in-the-money callability
169 */
170
171 callabilities.clear();
172
173 callabilities.push_back(x: ext::make_shared<Callability>(
174 args: Bond::Price(100.0, Bond::Price::Clean),
175 args: Callability::Put,
176 args: vars.calendar.advance(vars.issueDate(),n: 4,unit: Years)));
177
178 callabilities.push_back(x: ext::make_shared<Callability>(
179 args: Bond::Price(10.0, Bond::Price::Clean),
180 args: Callability::Call,
181 args: vars.calendar.advance(vars.issueDate(),n: 6,unit: Years)));
182
183 bond = CallableZeroCouponBond(3, 100.0, vars.calendar,
184 vars.maturityDate(), Thirty360(Thirty360::BondBasis),
185 vars.rollingConvention, 100.0,
186 vars.issueDate(), callabilities);
187 bond.setPricingEngine(engine);
188
189 expected = callabilities[0]->price().amount() *
190 vars.termStructure->discount(d: callabilities[0]->date()) /
191 vars.termStructure->discount(d: bond.settlementDate());
192
193 if (std::fabs(x: bond.settlementValue() - expected) > 1.0e-2)
194 BOOST_ERROR(
195 "puttability not exercised correctly:\n"
196 << std::setprecision(5)
197 << " calculated NPV: " << bond.settlementValue() << "\n"
198 << " expected: " << expected << "\n"
199 << " difference: " << bond.settlementValue()-expected);
200
201 /* case 4: same as case 3, with an added puttability later on */
202
203 callabilities.push_back(x: ext::make_shared<Callability>(
204 args: Bond::Price(100.0, Bond::Price::Clean),
205 args: Callability::Put,
206 args: vars.calendar.advance(vars.issueDate(),n: 8,unit: Years)));
207
208 bond = CallableZeroCouponBond(3, 100.0, vars.calendar,
209 vars.maturityDate(), Thirty360(Thirty360::BondBasis),
210 vars.rollingConvention, 100.0,
211 vars.issueDate(), callabilities);
212 bond.setPricingEngine(engine);
213
214 if (std::fabs(x: bond.settlementValue() - expected) > 1.0e-2)
215 BOOST_ERROR(
216 "puttability not exercised correctly:\n"
217 << std::setprecision(5)
218 << " calculated NPV: " << bond.settlementValue() << "\n"
219 << " expected: " << expected << "\n"
220 << " difference: " << bond.settlementValue()-expected);
221}
222
223
224void CallableBondTest::testConsistency() {
225
226 BOOST_TEST_MESSAGE("Testing consistency of callable bonds...");
227
228 Globals vars;
229
230 vars.termStructure.linkTo(h: vars.makeFlatCurve(r: 0.032));
231 vars.model.linkTo(h: ext::make_shared<HullWhite>(args&: vars.termStructure));
232
233 Schedule schedule =
234 MakeSchedule()
235 .from(effectiveDate: vars.issueDate())
236 .to(terminationDate: vars.maturityDate())
237 .withCalendar(vars.calendar)
238 .withFrequency(Semiannual)
239 .withConvention(vars.rollingConvention)
240 .withRule(DateGeneration::Backward);
241
242 std::vector<Rate> coupons(1, 0.05);
243
244 FixedRateBond bond(3, 100.0, schedule,
245 coupons, Thirty360(Thirty360::BondBasis));
246 bond.setPricingEngine(
247 ext::make_shared<DiscountingBondEngine>(args&: vars.termStructure));
248
249 CallabilitySchedule callabilities;
250 std::vector<Date> callabilityDates = vars.evenYears();
251 for (auto& callabilityDate : callabilityDates) {
252 callabilities.push_back(x: ext::make_shared<Callability>(
253 args: Bond::Price(110.0, Bond::Price::Clean), args: Callability::Call, args&: callabilityDate));
254 }
255
256 CallabilitySchedule puttabilities;
257 std::vector<Date> puttabilityDates = vars.oddYears();
258 for (auto& puttabilityDate : puttabilityDates) {
259 puttabilities.push_back(x: ext::make_shared<Callability>(args: Bond::Price(90.0, Bond::Price::Clean),
260 args: Callability::Put, args&: puttabilityDate));
261 }
262
263 Size timeSteps = 240;
264
265 ext::shared_ptr<PricingEngine> engine =
266 ext::make_shared<TreeCallableFixedRateBondEngine>(
267 args: *(vars.model), args&: timeSteps, args&: vars.termStructure);
268
269 CallableFixedRateBond callable(3, 100.0, schedule,
270 coupons, Thirty360(Thirty360::BondBasis),
271 vars.rollingConvention,
272 100.0, vars.issueDate(),
273 callabilities);
274 callable.setPricingEngine(engine);
275
276 CallableFixedRateBond puttable(3, 100.0, schedule,
277 coupons, Thirty360(Thirty360::BondBasis),
278 vars.rollingConvention,
279 100.0, vars.issueDate(),
280 puttabilities);
281 puttable.setPricingEngine(engine);
282
283 if (bond.cleanPrice() <= callable.cleanPrice())
284 BOOST_ERROR(
285 "inconsistent prices:\n"
286 << std::setprecision(8)
287 << " plain bond: " << bond.cleanPrice() << "\n"
288 << " callable: " << callable.cleanPrice() << "\n"
289 << " (should be lower)");
290
291 if (bond.cleanPrice() >= puttable.cleanPrice())
292 BOOST_ERROR(
293 "inconsistent prices:\n"
294 << std::setprecision(8)
295 << " plain bond: " << bond.cleanPrice() << "\n"
296 << " puttable: " << puttable.cleanPrice() << "\n"
297 << " (should be higher)");
298}
299
300
301void CallableBondTest::testObservability() {
302
303 BOOST_TEST_MESSAGE("Testing observability of callable bonds...");
304
305 Globals vars;
306
307 ext::shared_ptr<SimpleQuote> observable(new SimpleQuote(0.03));
308 Handle<Quote> h(observable);
309 vars.termStructure.linkTo(h: vars.makeFlatCurve(r: h));
310 vars.model.linkTo(h: ext::make_shared<HullWhite>(args&: vars.termStructure));
311
312 Schedule schedule =
313 MakeSchedule()
314 .from(effectiveDate: vars.issueDate())
315 .to(terminationDate: vars.maturityDate())
316 .withCalendar(vars.calendar)
317 .withFrequency(Semiannual)
318 .withConvention(vars.rollingConvention)
319 .withRule(DateGeneration::Backward);
320
321 std::vector<Rate> coupons(1, 0.05);
322
323 CallabilitySchedule callabilities;
324
325 std::vector<Date> callabilityDates = vars.evenYears();
326 for (auto& callabilityDate : callabilityDates) {
327 callabilities.push_back(x: ext::make_shared<Callability>(
328 args: Bond::Price(110.0, Bond::Price::Clean), args: Callability::Call, args&: callabilityDate));
329 }
330 std::vector<Date> puttabilityDates = vars.oddYears();
331 for (auto& puttabilityDate : puttabilityDates) {
332 callabilities.push_back(x: ext::make_shared<Callability>(args: Bond::Price(90.0, Bond::Price::Clean),
333 args: Callability::Put, args&: puttabilityDate));
334 }
335
336 CallableZeroCouponBond bond(3, 100.0, vars.calendar,
337 vars.maturityDate(), Thirty360(Thirty360::BondBasis),
338 vars.rollingConvention, 100.0,
339 vars.issueDate(), callabilities);
340
341 Size timeSteps = 240;
342
343 ext::shared_ptr<PricingEngine> engine =
344 ext::make_shared<TreeCallableFixedRateBondEngine>(
345 args: *(vars.model), args&: timeSteps, args&: vars.termStructure);
346
347 bond.setPricingEngine(engine);
348
349 Real originalValue = bond.NPV();
350
351 observable->setValue(0.04);
352
353 if (bond.NPV() == originalValue)
354 BOOST_ERROR(
355 "callable coupon bond was not notified of observable change");
356
357
358}
359
360void CallableBondTest::testDegenerate() {
361
362 BOOST_TEST_MESSAGE("Repricing bonds using degenerate callable bonds...");
363
364 Globals vars;
365
366 vars.termStructure.linkTo(h: vars.makeFlatCurve(r: 0.034));
367 vars.model.linkTo(h: ext::make_shared<HullWhite>(args&: vars.termStructure));
368
369 Schedule schedule =
370 MakeSchedule()
371 .from(effectiveDate: vars.issueDate())
372 .to(terminationDate: vars.maturityDate())
373 .withCalendar(vars.calendar)
374 .withFrequency(Semiannual)
375 .withConvention(vars.rollingConvention)
376 .withRule(DateGeneration::Backward);
377
378 std::vector<Rate> coupons(1, 0.05);
379
380 ZeroCouponBond zeroCouponBond(3, vars.calendar, 100.0,
381 vars.maturityDate(),
382 vars.rollingConvention);
383 FixedRateBond couponBond(3, 100.0, schedule,
384 coupons, Thirty360(Thirty360::BondBasis));
385
386 // no callability
387 CallabilitySchedule callabilities;
388
389 CallableZeroCouponBond bond1(3, 100.0, vars.calendar,
390 vars.maturityDate(), Thirty360(Thirty360::BondBasis),
391 vars.rollingConvention, 100.0,
392 vars.issueDate(), callabilities);
393
394 CallableFixedRateBond bond2(3, 100.0, schedule,
395 coupons, Thirty360(Thirty360::BondBasis),
396 vars.rollingConvention,
397 100.0, vars.issueDate(),
398 callabilities);
399
400 ext::shared_ptr<PricingEngine> discountingEngine =
401 ext::make_shared<DiscountingBondEngine>(args&: vars.termStructure);
402
403 zeroCouponBond.setPricingEngine(discountingEngine);
404 couponBond.setPricingEngine(discountingEngine);
405
406 Size timeSteps = 240;
407
408 ext::shared_ptr<PricingEngine> treeEngine =
409 ext::make_shared<TreeCallableFixedRateBondEngine>(
410 args: *(vars.model), args&: timeSteps, args&: vars.termStructure);
411
412 bond1.setPricingEngine(treeEngine);
413 bond2.setPricingEngine(treeEngine);
414
415 double tolerance = 1.0e-4;
416
417 if (std::fabs(x: bond1.cleanPrice() - zeroCouponBond.cleanPrice()) > tolerance)
418 BOOST_ERROR(
419 "failed to reproduce zero-coupon bond price:\n"
420 << std::setprecision(7)
421 << " calculated: " << bond1.cleanPrice() << "\n"
422 << " expected: " << zeroCouponBond.cleanPrice());
423
424 if (std::fabs(x: bond2.cleanPrice() - couponBond.cleanPrice()) > tolerance)
425 BOOST_ERROR(
426 "failed to reproduce fixed-rate bond price:\n"
427 << std::setprecision(7)
428 << " calculated: " << bond2.cleanPrice() << "\n"
429 << " expected: " << couponBond.cleanPrice());
430
431 // out-of-the-money callability
432
433 std::vector<Date> callabilityDates = vars.evenYears();
434 for (auto& callabilityDate : callabilityDates) {
435 callabilities.push_back(x: ext::make_shared<Callability>(
436 args: Bond::Price(10000.0, Bond::Price::Clean), args: Callability::Call, args&: callabilityDate));
437 }
438 std::vector<Date> puttabilityDates = vars.oddYears();
439 for (auto& puttabilityDate : puttabilityDates) {
440 callabilities.push_back(x: ext::make_shared<Callability>(args: Bond::Price(0.0, Bond::Price::Clean),
441 args: Callability::Put, args&: puttabilityDate));
442 }
443
444 bond1 = CallableZeroCouponBond(3, 100.0, vars.calendar,
445 vars.maturityDate(), Thirty360(Thirty360::BondBasis),
446 vars.rollingConvention, 100.0,
447 vars.issueDate(), callabilities);
448
449 bond2 = CallableFixedRateBond(3, 100.0, schedule,
450 coupons, Thirty360(Thirty360::BondBasis),
451 vars.rollingConvention,
452 100.0, vars.issueDate(),
453 callabilities);
454
455 bond1.setPricingEngine(treeEngine);
456 bond2.setPricingEngine(treeEngine);
457
458 if (std::fabs(x: bond1.cleanPrice() - zeroCouponBond.cleanPrice()) > tolerance)
459 BOOST_ERROR(
460 "failed to reproduce zero-coupon bond price:\n"
461 << std::setprecision(7)
462 << " calculated: " << bond1.cleanPrice() << "\n"
463 << " expected: " << zeroCouponBond.cleanPrice());
464
465 if (std::fabs(x: bond2.cleanPrice() - couponBond.cleanPrice()) > tolerance)
466 BOOST_ERROR(
467 "failed to reproduce fixed-rate bond price:\n"
468 << std::setprecision(7)
469 << " calculated: " << bond2.cleanPrice() << "\n"
470 << " expected: " << couponBond.cleanPrice());
471}
472
473void CallableBondTest::testCached() {
474
475 BOOST_TEST_MESSAGE("Testing callable-bond value against cached values...");
476
477 Globals vars;
478
479 vars.today = Date(3,June,2004);
480 Settings::instance().evaluationDate() = vars.today;
481 vars.settlement = vars.calendar.advance(vars.today,n: 3,unit: Days);
482
483 vars.termStructure.linkTo(h: vars.makeFlatCurve(r: 0.032));
484 vars.model.linkTo(h: ext::make_shared<HullWhite>(args&: vars.termStructure));
485
486 Schedule schedule =
487 MakeSchedule()
488 .from(effectiveDate: vars.issueDate())
489 .to(terminationDate: vars.maturityDate())
490 .withCalendar(vars.calendar)
491 .withFrequency(Semiannual)
492 .withConvention(vars.rollingConvention)
493 .withRule(DateGeneration::Backward);
494
495 std::vector<Rate> coupons(1, 0.05);
496
497 CallabilitySchedule callabilities;
498 CallabilitySchedule puttabilities;
499 CallabilitySchedule all_exercises;
500
501 std::vector<Date> callabilityDates = vars.evenYears();
502 for (auto& callabilityDate : callabilityDates) {
503 ext::shared_ptr<Callability> exercise = ext::make_shared<Callability>(
504 args: Bond::Price(110.0, Bond::Price::Clean), args: Callability::Call, args&: callabilityDate);
505 callabilities.push_back(x: exercise);
506 all_exercises.push_back(x: exercise);
507 }
508 std::vector<Date> puttabilityDates = vars.oddYears();
509 for (auto& puttabilityDate : puttabilityDates) {
510 ext::shared_ptr<Callability> exercise = ext::make_shared<Callability>(
511 args: Bond::Price(100.0, Bond::Price::Clean), args: Callability::Put, args&: puttabilityDate);
512 puttabilities.push_back(x: exercise);
513 all_exercises.push_back(x: exercise);
514 }
515
516 Size timeSteps = 240;
517
518 ext::shared_ptr<PricingEngine> engine =
519 ext::make_shared<TreeCallableFixedRateBondEngine>(
520 args: *(vars.model), args&: timeSteps, args&: vars.termStructure);
521
522 double tolerance = 1.0e-8;
523
524 double storedPrice1 = 110.60975477;
525 CallableFixedRateBond bond1(3, 10000.0, schedule,
526 coupons, Thirty360(Thirty360::BondBasis),
527 vars.rollingConvention,
528 100.0, vars.issueDate(),
529 callabilities);
530 bond1.setPricingEngine(engine);
531
532 if (std::fabs(x: bond1.cleanPrice() - storedPrice1) > tolerance)
533 BOOST_ERROR(
534 "failed to reproduce cached callable-bond price:\n"
535 << std::setprecision(12)
536 << " calculated: " << bond1.cleanPrice() << "\n"
537 << " expected: " << storedPrice1);
538
539 double storedPrice2 = 115.16559362;
540 CallableFixedRateBond bond2(3, 10000.0, schedule,
541 coupons, Thirty360(Thirty360::BondBasis),
542 vars.rollingConvention,
543 100.0, vars.issueDate(),
544 puttabilities);
545 bond2.setPricingEngine(engine);
546
547 if (std::fabs(x: bond2.cleanPrice() - storedPrice2) > tolerance)
548 BOOST_ERROR(
549 "failed to reproduce cached puttable-bond price:\n"
550 << std::setprecision(12)
551 << " calculated: " << bond2.cleanPrice() << "\n"
552 << " expected: " << storedPrice2);
553
554 double storedPrice3 = 110.97509625;
555 CallableFixedRateBond bond3(3, 10000.0, schedule,
556 coupons, Thirty360(Thirty360::BondBasis),
557 vars.rollingConvention,
558 100.0, vars.issueDate(),
559 all_exercises);
560 bond3.setPricingEngine(engine);
561
562 if (std::fabs(x: bond3.cleanPrice() - storedPrice3) > tolerance)
563 BOOST_ERROR(
564 "failed to reproduce cached callable/puttable-bond price:\n"
565 << std::setprecision(12)
566 << " calculated: " << bond3.cleanPrice() << "\n"
567 << " expected: " << storedPrice3);
568
569
570}
571
572void CallableBondTest::testSnappingExerciseDate2ClosestCouponDate() {
573
574 BOOST_TEST_MESSAGE("Testing snap of callability dates to the closest coupon date...");
575
576 /* This is a test case inspired by
577 * https://github.com/lballabio/QuantLib/issues/930#issuecomment-853886024 */
578
579 auto today = Date(18, May, 2021);
580
581 Settings::instance().evaluationDate() = today;
582
583 auto calendar = UnitedStates(UnitedStates::FederalReserve);
584 auto accrualDCC = Thirty360(Thirty360::Convention::USA);
585 auto frequency = Semiannual;
586 RelinkableHandle<YieldTermStructure> termStructure;
587 termStructure.linkTo(h: ext::make_shared<FlatForward>(args&: today, args: 0.02, args: Actual365Fixed()));
588
589 auto makeBonds = [&calendar, &accrualDCC, frequency,
590 &termStructure](Date callDate, ext::shared_ptr<FixedRateBond>& fixedRateBond,
591 ext::shared_ptr<CallableFixedRateBond>& callableBond) {
592 auto settlementDays = 2;
593 auto settlementDate = Date(20, May, 2021);
594 auto coupon = 0.05;
595 auto faceAmount = 100.00;
596 auto redemption = faceAmount;
597 auto maturityDate = Date(14, Feb, 2026);
598 auto issueDate = settlementDate - 2 * 366 * Days;
599 Schedule schedule = MakeSchedule()
600 .from(effectiveDate: issueDate)
601 .to(terminationDate: maturityDate)
602 .withFrequency(frequency)
603 .withCalendar(calendar)
604 .withConvention(Unadjusted)
605 .withTerminationDateConvention(Unadjusted)
606 .backwards()
607 .endOfMonth(flag: false);
608 auto coupons = std::vector<Rate>(schedule.size() - 1, coupon);
609
610 CallabilitySchedule callabilitySchedule;
611 callabilitySchedule.push_back(x: ext::make_shared<Callability>(
612 args: Bond::Price(faceAmount, Bond::Price::Clean), args: Callability::Type::Call, args&: callDate));
613
614 auto newCallableBond = ext::make_shared<CallableFixedRateBond>(
615 args&: settlementDays, args&: faceAmount, args&: schedule, args&: coupons, args&: accrualDCC,
616 args: BusinessDayConvention::Following, args&: redemption, args&: issueDate, args&: callabilitySchedule);
617
618 auto model = ext::make_shared<HullWhite>(args&: termStructure, args: 1e-12, args: 0.003);
619 auto treeEngine = ext::make_shared<TreeCallableFixedRateBondEngine>(args&: model, args: 40);
620 newCallableBond->setPricingEngine(treeEngine);
621
622 callableBond.swap(other&: newCallableBond);
623
624 auto fixedRateBondSchedule = schedule.until(truncationDate: callDate);
625 auto fixedRateBondCoupons = std::vector<Rate>(schedule.size() - 1, coupon);
626
627 auto newFixedRateBond = ext::make_shared<FixedRateBond>(
628 args&: settlementDays, args&: faceAmount, args&: fixedRateBondSchedule, args&: fixedRateBondCoupons, args&: accrualDCC,
629 args: BusinessDayConvention::Following, args&: redemption, args&: issueDate);
630 auto discountingEngine = ext::make_shared<DiscountingBondEngine>(args&: termStructure);
631 newFixedRateBond->setPricingEngine(discountingEngine);
632
633 fixedRateBond.swap(other&: newFixedRateBond);
634 };
635
636 auto initialCallDate = Date(14, Feb, 2022);
637 Real tolerance = 1e-10;
638 Real prevOAS = 0.0266;
639 Real expectedOasStep = 0.00005;
640
641 ext::shared_ptr<CallableFixedRateBond> callableBond;
642 ext::shared_ptr<FixedRateBond> fixedRateBond;
643
644 for (int i = -10; i < 11; i++) {
645 auto callDate = initialCallDate + i * Days;
646 if (calendar.isBusinessDay(d: callDate)) {
647 makeBonds(callDate, fixedRateBond, callableBond);
648 auto npvFixedRateBond = fixedRateBond->NPV();
649 auto npvCallable = callableBond->NPV();
650
651 if (std::fabs(x: npvCallable - npvFixedRateBond) > tolerance) {
652 BOOST_ERROR("failed to reproduce bond price at "
653 << io::iso_date(callDate) << ":\n"
654 << std::setprecision(7) << " calculated: " << npvCallable << "\n"
655 << " expected: " << npvFixedRateBond << " +/- " << std::scientific
656 << std::setprecision(1) << tolerance);
657 }
658
659 auto cleanPrice = callableBond->cleanPrice() - 2.0;
660 auto oas = callableBond->OAS(cleanPrice, engineTS: termStructure, dayCounter: accrualDCC,
661 compounding: QuantLib::Continuous, frequency);
662 if (prevOAS - oas < expectedOasStep) {
663 BOOST_ERROR("failed to get expected change in OAS at "
664 << io::iso_date(callDate) << ":\n"
665 << std::setprecision(7) << " calculated: " << oas << "\n"
666 << " previous: " << prevOAS << "\n"
667 << " should at least change by " << expectedOasStep);
668 }
669 prevOAS = oas;
670 }
671 }
672}
673
674
675void CallableBondTest::testBlackEngine() {
676
677 BOOST_TEST_MESSAGE("Testing Black engine for European callable bonds...");
678
679 Globals vars;
680
681 vars.today = Date(20, September, 2022);
682 Settings::instance().evaluationDate() = vars.today;
683 vars.settlement = vars.calendar.advance(vars.today, n: 3, unit: Days);
684
685 vars.termStructure.linkTo(h: vars.makeFlatCurve(r: 0.03));
686
687 CallabilitySchedule callabilities = {
688 ext::make_shared<Callability>(
689 args: Bond::Price(100.0, Bond::Price::Clean),
690 args: Callability::Call,
691 args: vars.calendar.advance(vars.issueDate(),n: 4,unit: Years))
692 };
693
694 CallableZeroCouponBond bond(3, 10000.0, vars.calendar,
695 vars.maturityDate(), Thirty360(Thirty360::BondBasis),
696 vars.rollingConvention, 100.0,
697 vars.issueDate(), callabilities);
698
699 bond.setPricingEngine(ext::make_shared<BlackCallableZeroCouponBondEngine>(
700 args: Handle<Quote>(ext::make_shared<SimpleQuote>(args: 0.3)), args&: vars.termStructure));
701
702 Real expected = 74.52915084;
703 Real calculated = bond.cleanPrice();
704
705 if (std::fabs(x: calculated - expected) > 1.0e-4)
706 BOOST_ERROR(
707 "failed to reproduce cached price:\n"
708 << std::setprecision(5)
709 << " calculated NPV: " << calculated << "\n"
710 << " expected: " << expected << "\n"
711 << " difference: " << calculated - expected);
712}
713
714
715void CallableBondTest::testImpliedVol() {
716
717 BOOST_TEST_MESSAGE("Testing implied-volatility calculation for callable bonds...");
718
719 Globals vars;
720
721 vars.termStructure.linkTo(h: vars.makeFlatCurve(r: 0.03));
722
723 Schedule schedule =
724 MakeSchedule()
725 .from(effectiveDate: vars.issueDate())
726 .to(terminationDate: vars.maturityDate())
727 .withCalendar(vars.calendar)
728 .withFrequency(Semiannual)
729 .withConvention(vars.rollingConvention)
730 .withRule(DateGeneration::Backward);
731
732 std::vector<Rate> coupons = { 0.01 };
733
734 CallabilitySchedule callabilities = {
735 ext::make_shared<Callability>(
736 args: Bond::Price(100.0, Bond::Price::Clean),
737 args: Callability::Call,
738 args: schedule.at(i: 8))
739 };
740
741 CallableFixedRateBond bond(3, 10000.0, schedule,
742 coupons, Thirty360(Thirty360::BondBasis),
743 vars.rollingConvention,
744 100.0, vars.issueDate(),
745 callabilities);
746
747 auto targetPrice = Bond::Price(78.50, Bond::Price::Dirty);
748 Real volatility = bond.impliedVolatility(targetPrice,
749 discountCurve: vars.termStructure,
750 accuracy: 1e-8, // accuracy
751 maxEvaluations: 200, // max evaluations
752 minVol: 1e-4, // min vol
753 maxVol: 1.0); // max vol
754
755 bond.setPricingEngine(ext::make_shared<BlackCallableZeroCouponBondEngine>(
756 args: Handle<Quote>(ext::make_shared<SimpleQuote>(args&: volatility)), args&: vars.termStructure));
757
758 if (std::fabs(x: bond.dirtyPrice() - targetPrice.amount()) > 1.0e-4)
759 BOOST_ERROR(
760 "failed to reproduce target dirty price with implied volatility:\n"
761 << std::setprecision(5)
762 << " calculated price: " << bond.dirtyPrice() << "\n"
763 << " expected: " << targetPrice.amount() << "\n"
764 << " difference: " << bond.dirtyPrice() - targetPrice.amount());
765
766 targetPrice = Bond::Price(78.50, Bond::Price::Clean);
767 volatility = bond.impliedVolatility(targetPrice,
768 discountCurve: vars.termStructure,
769 accuracy: 1e-8, // accuracy
770 maxEvaluations: 200, // max evaluations
771 minVol: 1e-4, // min vol
772 maxVol: 1.0); // max vol
773
774 bond.setPricingEngine(ext::make_shared<BlackCallableZeroCouponBondEngine>(
775 args: Handle<Quote>(ext::make_shared<SimpleQuote>(args&: volatility)), args&: vars.termStructure));
776
777 if (std::fabs(x: bond.cleanPrice() - targetPrice.amount()) > 1.0e-4)
778 BOOST_ERROR(
779 "failed to reproduce target clean price with implied volatility:\n"
780 << std::setprecision(5)
781 << " calculated price: " << bond.cleanPrice() << "\n"
782 << " expected: " << targetPrice.amount() << "\n"
783 << " difference: " << bond.cleanPrice() - targetPrice.amount());
784
785
786 QL_DEPRECATED_DISABLE_WARNING
787
788 Real targetNPV = 7850.0;
789 volatility = bond.impliedVolatility(targetValue: targetNPV,
790 discountCurve: vars.termStructure,
791 accuracy: 1e-8, // accuracy
792 maxEvaluations: 200, // max evaluations
793 minVol: 1e-4, // min vol
794 maxVol: 1.0); // max vol
795
796 QL_DEPRECATED_ENABLE_WARNING
797
798 bond.setPricingEngine(ext::make_shared<BlackCallableZeroCouponBondEngine>(
799 args: Handle<Quote>(ext::make_shared<SimpleQuote>(args&: volatility)), args&: vars.termStructure));
800
801 if (std::fabs(x: bond.NPV() - targetNPV) > 1.0e-4)
802 BOOST_ERROR(
803 "failed to reproduce target NPV with implied volatility:\n"
804 << std::setprecision(5)
805 << " calculated NPV: " << bond.NPV() << "\n"
806 << " expected: " << targetNPV << "\n"
807 << " difference: " << bond.NPV() - targetNPV);
808}
809
810void CallableBondTest::testCallableFixedRateBondWithArbitrarySchedule() {
811 BOOST_TEST_MESSAGE("Testing callable fixed-rate bond with arbitrary schedule...");
812
813 Globals vars;
814
815 Natural settlementDays = 2;
816 vars.today = Date(10, Jan, 2020);
817 Settings::instance().evaluationDate() = vars.today;
818 vars.settlement = vars.calendar.advance(vars.today, n: settlementDays, unit: Days);
819
820 vars.termStructure.linkTo(h: vars.makeFlatCurve(r: 0.03));
821 vars.model.linkTo(h: ext::make_shared<HullWhite>(args&: vars.termStructure));
822
823 Size timeSteps = 240;
824 ext::shared_ptr<PricingEngine> engine = ext::make_shared<TreeCallableFixedRateBondEngine>(
825 args: *(vars.model), args&: timeSteps, args&: vars.termStructure);
826
827 std::vector<Date> dates(4);
828 dates[0] = Date(20, February, 2020);
829 dates[1] = Date(15, Aug, 2020);
830 dates[2] = Date(25, Sep, 2021);
831 dates[3] = Date(27, Jan, 2022);
832
833 Schedule schedule(dates, vars.calendar, Unadjusted);
834
835 CallabilitySchedule callabilities = {
836 ext::make_shared<Callability>(
837 args: Bond::Price(100.0, Bond::Price::Clean),
838 args: Callability::Call,
839 args&: dates[2])
840 };
841
842 std::vector<Rate> coupons(1, 0.06);
843
844 CallableFixedRateBond callableBond(settlementDays, 100.0, schedule, coupons, vars.dayCounter,
845 vars.rollingConvention, 100.0, vars.issueDate(), callabilities);
846 callableBond.setPricingEngine(engine);
847
848 BOOST_CHECK_NO_THROW(callableBond.cleanPrice());
849}
850
851
852test_suite* CallableBondTest::suite() {
853 auto* suite = BOOST_TEST_SUITE("Callable-bond tests");
854 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testConsistency));
855 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testInterplay));
856 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testObservability));
857 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testDegenerate));
858 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testCached));
859 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testSnappingExerciseDate2ClosestCouponDate));
860 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testBlackEngine));
861 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testImpliedVol));
862 suite->add(QUANTLIB_TEST_CASE(&CallableBondTest::testCallableFixedRateBondWithArbitrarySchedule));
863 return suite;
864}
865

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