1/*
2 Copyright (C) 2014 Cheng Li
3
4 This file is part of QuantLib, a free-software/open-source library
5 for financial quantitative analysts and developers - http://quantlib.org/
6
7 QuantLib is free software: you can redistribute it and/or modify it
8 under the terms of the QuantLib license. You should have received a
9 copy of the license along with this program; if not, please email
10 <quantlib-dev@lists.sf.net>. The license is also available online at
11 <http://quantlib.org/license.shtml>.
12
13 This program is distributed in the hope that it will be useful, but WITHOUT
14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15 FOR A PARTICULAR PURPOSE. See the license for more details.
16*/
17
18#include "amortizingbond.hpp"
19#include "utilities.hpp"
20#include <ql/instruments/bonds/amortizingfixedratebond.hpp>
21#include <ql/cashflows/fixedratecoupon.hpp>
22#include <ql/time/daycounters/actualactual.hpp>
23#include <ql/time/calendars/nullcalendar.hpp>
24#include <ql/settings.hpp>
25#include <ql/time/calendars/brazil.hpp>
26#include <ql/time/calendars/unitedstates.hpp>
27#include <ql/time/daycounters/actual360.hpp>
28#include <ql/time/daycounters/business252.hpp>
29#include <iostream>
30
31using namespace QuantLib;
32using namespace boost::unit_test_framework;
33
34void AmortizingBondTest::testAmortizingFixedRateBond() {
35 BOOST_TEST_MESSAGE("Testing amortizing fixed rate bond...");
36
37 /*
38 * Following data is generated from Excel using function pmt with Nper = 360, PV = 100.0
39 */
40
41 Real rates[] = {0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12};
42 Real amounts[] = {0.277777778, 0.321639520, 0.369619473, 0.421604034,
43 0.477415295, 0.536821623, 0.599550525,
44 0.665302495, 0.733764574, 0.804622617,
45 0.877571570, 0.952323396, 1.028612597};
46
47 Frequency freq = Monthly;
48
49 Date refDate = Settings::instance().evaluationDate();
50
51 const Real tolerance = 1.0e-6;
52
53 for (Size i=0; i<LENGTH(rates); ++i) {
54
55 auto schedule = sinkingSchedule(startDate: refDate, bondLength: Period(30, Years), frequency: freq, paymentCalendar: NullCalendar());
56 auto notionals = sinkingNotionals(bondLength: Period(30, Years), frequency: freq, couponRate: rates[i], initialNotional: 100.0);
57
58 AmortizingFixedRateBond myBond(0, notionals, schedule, {rates[i]},
59 ActualActual(ActualActual::ISMA));
60
61 Leg cashflows = myBond.cashflows();
62
63 for (Size k=0; k < cashflows.size() / 2; ++k) {
64 Real coupon = cashflows[2*k]->amount();
65 Real principal = cashflows[2*k+1]->amount();
66 Real totalAmount = coupon + principal;
67
68 // Check the amount is same as pmt returned
69
70 Real error = std::fabs(x: totalAmount-amounts[i]);
71 if (error > tolerance) {
72 BOOST_ERROR("\n" <<
73 " Rate: " << rates[i] <<
74 " " << k << "th cash flow "
75 " Failed!" <<
76 " Expected Amount: " << amounts[i] <<
77 " Calculated Amount: " << totalAmount);
78 }
79
80 // Check the coupon result
81 Real expectedCoupon = notionals[k] * rates[i] / Integer(freq);
82 error = std::fabs(x: coupon - expectedCoupon);
83
84 if (error > tolerance) {
85 BOOST_ERROR("\n" <<
86 " Rate: " << rates[i] <<
87 " " << k << "th cash flow "
88 " Failed!" <<
89 " Expected Coupon: " << expectedCoupon <<
90 " Calculated Coupon: " << coupon);
91 }
92 }
93 }
94}
95
96void AmortizingBondTest::testBrazilianAmortizingFixedRateBond() {
97 BOOST_TEST_MESSAGE("Testing Brazilian amortizing fixed rate bond...");
98
99 /*
100 * Following data is based on the following Brazilian onshore corporate bond code:
101 * SND Code - RISF11
102 * ISIN Code - BRRISFDBS005
103 * Fiduciary Agent URL - https://www.pentagonotrustee.com.br/Site/DetalhesEmissor?ativo=RISF11&aba=tab-5&tipo=undefined
104 */
105
106 static const Real arr[] = {
107 1000 , 983.33300000, 966.66648898, 950.00019204,
108 933.33338867, 916.66685434, 900.00001759, 883.33291726,
109 866.66619177, 849.99933423, 833.33254728, 816.66589633,
110 799.99937871, 783.33299165, 766.66601558, 749.99946306,
111 733.33297499, 716.66651646, 699.99971995, 683.33272661,
112 666.66624140, 649.99958536, 633.33294599, 616.66615618,
113 599.99951997, 583.33273330, 566.66633377, 549.99954356,
114 533.33290739, 516.66625403, 499.99963400, 483.33314619,
115 466.66636930, 449.99984658, 433.33320226, 416.66634063,
116 399.99968700, 383.33290004, 366.66635221, 349.99953317,
117 333.33290539, 316.66626012, 299.99948151, 283.33271031,
118 266.66594695, 249.99932526, 233.33262024, 216.66590450,
119 199.99931312, 183.33277035, 166.66617153, 149.99955437,
120 133.33295388, 116.66633464, 99.99973207, 83.33307672,
121 66.66646137, 49.99984602, 33.33324734, 16.66662367
122 };
123 std::vector<Real> notionals (arr, arr + sizeof(arr) / sizeof(arr[0]) );
124
125 Real expected_amortizations[] = {
126 16.66700000, 16.66651102, 16.66629694, 16.66680337,
127 16.66653432, 16.66683675, 16.66710033, 16.66672548,
128 16.66685753, 16.66678695, 16.66665095, 16.66651761,
129 16.66638706, 16.66697606, 16.66655251, 16.66648807,
130 16.66645852, 16.66679651, 16.66699333, 16.66648520,
131 16.66665604, 16.66663937, 16.66678981, 16.66663620,
132 16.66678667, 16.66639952, 16.66679021, 16.66663617,
133 16.66665336, 16.66662002, 16.66648780, 16.66677688,
134 16.66652271, 16.66664432, 16.66686163, 16.66665363,
135 16.66678696, 16.66654783, 16.66681904, 16.66662777,
136 16.66664527, 16.66677860, 16.66677119, 16.66676335,
137 16.66662168, 16.66670502, 16.66671573, 16.66659137,
138 16.66654276, 16.66659882, 16.66661715, 16.66660049,
139 16.66661924, 16.66660257, 16.66665534, 16.66661534,
140 16.66661534, 16.66659867, 16.66662367, 16.66662367
141 };
142
143 Real expected_coupons[] = {
144 5.97950399, 4.85474255, 5.27619136, 5.18522454,
145 5.33753111, 5.24221882, 4.91231709, 4.59116258,
146 4.73037674, 4.63940686, 4.54843737, 3.81920094,
147 4.78359948, 3.86733691, 4.38439657, 4.09359456,
148 4.00262671, 4.28531030, 3.82068947, 3.55165259,
149 3.46502778, 3.71720657, 3.62189368, 2.88388676,
150 3.58769952, 2.72800044, 3.38838360, 3.00196900,
151 2.91100034, 3.08940793, 2.59877059, 2.63809514,
152 2.42551945, 2.45615766, 2.59111761, 1.94857222,
153 2.28751141, 1.79268582, 2.19248291, 1.81913832,
154 1.90625855, 1.89350716, 1.48110584, 1.62031828,
155 1.38600825, 1.23425366, 1.39521333, 1.06968563,
156 1.03950542, 1.00065409, 0.90968563, 0.81871706,
157 0.79726493, 0.63678002, 0.57187676, 0.49829046,
158 0.32913418, 0.27290565, 0.19062560, 0.08662552
159 };
160
161 Natural settlementDays = 0;
162 Date issueDate(2, March, 2020);
163 Date maturityDate(2, March, 2025);
164
165 Schedule schedule(issueDate,
166 maturityDate,
167 Period(Monthly),
168 Brazil(Brazil::Settlement),
169 Unadjusted,
170 Unadjusted,
171 DateGeneration::Backward,
172 false);
173
174 std::vector<InterestRate> couponRates = {
175 InterestRate(0.0675,
176 Business252(Brazil()),
177 Compounded, Annual)
178 };
179
180 Leg coupons = FixedRateLeg(schedule)
181 .withNotionals(notionals)
182 .withCouponRates(couponRates)
183 .withPaymentAdjustment(Following);
184
185 Bond risf11(settlementDays,
186 schedule.calendar(),
187 issueDate,
188 coupons);
189
190 const Real tolerance = 1.0e-6;
191 Real error;
192 Leg cashflows = risf11.cashflows();
193 for (Size k=0; k < cashflows.size() / 2; ++k) {
194 error = std::fabs(x: expected_coupons[k] - cashflows[2*k]->amount());
195 if(error > tolerance) {
196 BOOST_ERROR("\n" <<
197 " " << k << "th cash flow "
198 " Failed!" <<
199 " Expected Coupon: " << expected_coupons[k] <<
200 " Calculated Coupon: " << cashflows[2*k]->amount());
201 }
202
203 error = std::fabs(x: expected_amortizations[k]- cashflows[2*k+1]->amount());
204 if(error > tolerance) {
205 BOOST_ERROR("\n" <<
206 " " << k << "th cash flow "
207 " Failed!" <<
208 " Expected Amortization: " << expected_amortizations[k] <<
209 " Calculated Amortization: " << cashflows[2*k+1]->amount());
210 }
211
212 }
213
214}
215
216void AmortizingBondTest::testAmortizingFixedRateBondWithDrawDown() {
217 BOOST_TEST_MESSAGE("Testing amortizing fixed rate bond with draw-down...");
218
219 Date issueDate = Date(19, May, 2012);
220 Date maturityDate = Date(25, May, 2017);
221 Calendar calendar = UnitedStates(UnitedStates::GovernmentBond);
222 Natural settlementDays = 3;
223
224 Schedule schedule(issueDate, maturityDate, Period(Semiannual), calendar,
225 Unadjusted, Unadjusted, DateGeneration::Backward, false);
226
227 std::vector<Real> nominals = { 100.0, 100.0, 100.5, 100.5, 101.5, 101.5, 90.0, 80.0, 70.0, 60.0 };
228 std::vector<Real> rates = { 0.042 };
229
230 Leg leg = FixedRateLeg(schedule)
231 .withNotionals(nominals)
232 .withCouponRates(rates, paymentDayCounter: Actual360())
233 .withPaymentAdjustment(Unadjusted)
234 .withPaymentCalendar(calendar);
235
236 Bond bond(settlementDays, calendar, issueDate, leg);
237
238 const auto& cfs = bond.cashflows();
239
240 // first draw-down
241 Real calculated = cfs.at(n: 2)->amount();
242 Real expected = nominals[1] - nominals[2];
243 Real error = std::fabs(x: calculated - expected);
244 Real tolerance = 1e-8;
245
246 if(error > tolerance) {
247 BOOST_ERROR("Failed to calculate first draw down: "
248 << "\n expected: " << expected
249 << "\n calculated: " << calculated);
250 }
251
252 // second draw-down
253 calculated = cfs.at(n: 5)->amount();
254 expected = nominals[3] - nominals[4];
255 error = std::fabs(x: calculated - expected);
256
257 if(error > tolerance) {
258 BOOST_ERROR("Failed to calculate second draw down: "
259 << "\n expected: " << expected
260 << "\n calculated: " << calculated);
261 }
262
263 // first amortization
264 calculated = cfs.at(n: 8)->amount();
265 expected = nominals[5] - nominals[6];
266 error = std::fabs(x: calculated - expected);
267
268 if(error > tolerance) {
269 BOOST_ERROR("Failed to calculate fist amortization: "
270 << "\n expected: " << expected
271 << "\n calculated: " << calculated);
272 }
273
274}
275
276test_suite* AmortizingBondTest::suite() {
277 auto* suite = BOOST_TEST_SUITE("Amortizing Bond tests");
278 suite->add(QUANTLIB_TEST_CASE(&AmortizingBondTest::testAmortizingFixedRateBond));
279 suite->add(QUANTLIB_TEST_CASE(&AmortizingBondTest::testBrazilianAmortizingFixedRateBond));
280 suite->add(QUANTLIB_TEST_CASE(&AmortizingBondTest::testAmortizingFixedRateBondWithDrawDown));
281 return suite;
282}
283

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