| 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 | |
| 60 | using namespace QuantLib; |
| 61 | using namespace boost::unit_test_framework; |
| 62 | using std::map; |
| 63 | using std::vector; |
| 64 | using std::string; |
| 65 | |
| 66 | namespace 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 | |
| 655 | void 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 | |
| 672 | void 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 | |
| 685 | void 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 | |
| 698 | void 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 | |
| 711 | void 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 | |
| 732 | void 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 | |
| 745 | void 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 | |
| 758 | void 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 | |
| 779 | void 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 | |
| 793 | void 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 | |
| 806 | void 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 | |
| 846 | void 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 | |
| 894 | void 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 | |
| 982 | void 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 | |
| 1055 | void 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 | |
| 1075 | void 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 | |
| 1096 | void 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 | |
| 1127 | void 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 | |
| 1191 | void 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 | |
| 1218 | void 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 | |
| 1255 | namespace 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 | |
| 1288 | void 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 | */ |
| 1375 | void 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> (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 | |
| 1477 | test_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 | |