| 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 | |
| 31 | using namespace QuantLib; |
| 32 | using namespace boost::unit_test_framework; |
| 33 | |
| 34 | void 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 | |
| 96 | void 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 | |
| 216 | void 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 | |
| 276 | test_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 | |