1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2/*
3 Copyright (C) 2021 Marcin Rybacki
4
5 This file is part of QuantLib, a free-software/open-source library
6 for financial quantitative analysts and developers - http://quantlib.org/
7
8 QuantLib is free software: you can redistribute it and/or modify it
9 under the terms of the QuantLib license. You should have received a
10 copy of the license along with this program; if not, please email
11 <quantlib-dev@lists.sf.net>. The license is also available online at
12 <http://quantlib.org/license.shtml>.
13
14 This program is distributed in the hope that it will be useful, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
19#include "crosscurrencyratehelpers.hpp"
20#include "utilities.hpp"
21#include <ql/experimental/termstructures/crosscurrencyratehelpers.hpp>
22#include <ql/indexes/ibor/euribor.hpp>
23#include <ql/indexes/ibor/usdlibor.hpp>
24#include <ql/cashflows/iborcoupon.hpp>
25#include <ql/cashflows/simplecashflow.hpp>
26#include <ql/math/interpolations/loginterpolation.hpp>
27#include <ql/pricingengines/swap/discountingswapengine.hpp>
28#include <ql/termstructures/yield/flatforward.hpp>
29#include <ql/termstructures/yield/piecewiseyieldcurve.hpp>
30#include <ql/time/calendars/target.hpp>
31#include <ql/time/calendars/unitedstates.hpp>
32
33using namespace QuantLib;
34using namespace boost::unit_test_framework;
35
36namespace crosscurrencyratehelpers_test {
37
38 struct XccyTestDatum {
39 Integer n;
40 TimeUnit units;
41 Spread basis;
42
43 XccyTestDatum(Integer n, TimeUnit units, Spread basis) : n(n), units(units), basis(basis) {}
44 };
45
46 struct CommonVars {
47 Real basisPoint;
48 Real fxSpot;
49
50 Date today, settlement;
51 Calendar calendar;
52 Natural settlementDays;
53 Currency ccy;
54 BusinessDayConvention businessConvention;
55 DayCounter dayCount;
56 bool endOfMonth;
57
58 ext::shared_ptr<IborIndex> baseCcyIdx;
59 ext::shared_ptr<IborIndex> quoteCcyIdx;
60
61 RelinkableHandle<YieldTermStructure> baseCcyIdxHandle;
62 RelinkableHandle<YieldTermStructure> quoteCcyIdxHandle;
63
64 std::vector<XccyTestDatum> basisData;
65
66 // utilities
67
68 ext::shared_ptr<RateHelper>
69 constantNotionalXccyRateHelper(const XccyTestDatum& q,
70 const Handle<YieldTermStructure>& collateralHandle,
71 bool isFxBaseCurrencyCollateralCurrency,
72 bool isBasisOnFxBaseCurrencyLeg) const {
73 Handle<Quote> quoteHandle(ext::make_shared<SimpleQuote>(args: q.basis * basisPoint));
74 Period tenor(q.n, q.units);
75 return ext::shared_ptr<RateHelper>(new ConstNotionalCrossCurrencyBasisSwapRateHelper(
76 quoteHandle, tenor, settlementDays, calendar, businessConvention, endOfMonth,
77 baseCcyIdx, quoteCcyIdx, collateralHandle, isFxBaseCurrencyCollateralCurrency,
78 isBasisOnFxBaseCurrencyLeg));
79 }
80
81 std::vector<ext::shared_ptr<RateHelper> >
82 buildConstantNotionalXccyRateHelpers(const std::vector<XccyTestDatum>& xccyData,
83 const Handle<YieldTermStructure>& collateralHandle,
84 bool isFxBaseCurrencyCollateralCurrency,
85 bool isBasisOnFxBaseCurrencyLeg) const {
86 std::vector<ext::shared_ptr<RateHelper> > instruments;
87 instruments.reserve(n: xccyData.size());
88 for (const auto& i : xccyData) {
89 instruments.push_back(x: constantNotionalXccyRateHelper(
90 q: i, collateralHandle, isFxBaseCurrencyCollateralCurrency,
91 isBasisOnFxBaseCurrencyLeg));
92 }
93
94 return instruments;
95 }
96
97 ext::shared_ptr<RateHelper>
98 resettingXccyRateHelper(const XccyTestDatum& q,
99 const Handle<YieldTermStructure>& collateralHandle,
100 bool isFxBaseCurrencyCollateralCurrency,
101 bool isBasisOnFxBaseCurrencyLeg,
102 bool isFxBaseCurrencyLegResettable) const {
103 Handle<Quote> quoteHandle(ext::make_shared<SimpleQuote>(args: q.basis * basisPoint));
104 Period tenor(q.n, q.units);
105 return ext::shared_ptr<RateHelper>(new MtMCrossCurrencyBasisSwapRateHelper(
106 quoteHandle, tenor, settlementDays, calendar, businessConvention, endOfMonth,
107 baseCcyIdx, quoteCcyIdx, collateralHandle, isFxBaseCurrencyCollateralCurrency,
108 isBasisOnFxBaseCurrencyLeg, isFxBaseCurrencyLegResettable));
109 }
110
111 std::vector<ext::shared_ptr<RateHelper> >
112 buildResettingXccyRateHelpers(const std::vector<XccyTestDatum>& xccyData,
113 const Handle<YieldTermStructure>& collateralHandle,
114 bool isFxBaseCurrencyCollateralCurrency,
115 bool isBasisOnFxBaseCurrencyLeg,
116 bool isFxBaseCurrencyLegResettable) const {
117 std::vector<ext::shared_ptr<RateHelper> > instruments;
118 instruments.reserve(n: xccyData.size());
119 for (const auto& i : xccyData) {
120 instruments.push_back(x: resettingXccyRateHelper(
121 q: i, collateralHandle, isFxBaseCurrencyCollateralCurrency,
122 isBasisOnFxBaseCurrencyLeg, isFxBaseCurrencyLegResettable));
123 }
124
125 return instruments;
126 }
127
128 Schedule legSchedule(const Period& tenor,
129 const ext::shared_ptr<IborIndex>& idx) const {
130 return MakeSchedule()
131 .from(effectiveDate: settlement)
132 .to(terminationDate: settlement + tenor)
133 .withTenor(idx->tenor())
134 .withCalendar(calendar)
135 .withConvention(businessConvention)
136 .endOfMonth(flag: endOfMonth)
137 .backwards();
138 }
139
140 Leg constantNotionalLeg(const Schedule& schedule,
141 const ext::shared_ptr<IborIndex>& idx,
142 Real notional,
143 Spread basis) const {
144 Leg leg = IborLeg(schedule, idx).withNotionals(notional).withSpreads(spread: basis);
145 Date lastPaymentDate = leg.back()->date();
146 leg.push_back(x: ext::make_shared<SimpleCashFlow>(args&: notional, args&: lastPaymentDate));
147 return leg;
148 }
149
150 std::vector<ext::shared_ptr<Swap> >
151 buildXccyBasisSwap(const XccyTestDatum& q,
152 Real fxSpot,
153 bool isFxBaseCurrencyCollateralCurrency,
154 bool isBasisOnFxBaseCurrencyLeg) const {
155 const Real baseCcyLegNotional = 1.0;
156 Real quoteCcyLegNotional = baseCcyLegNotional * fxSpot;
157
158 Spread baseCcyLegBasis = isBasisOnFxBaseCurrencyLeg ? Real(q.basis * basisPoint) : 0.0;
159 Spread quoteCcyLegBasis = isBasisOnFxBaseCurrencyLeg ? 0.0 : Real(q.basis * basisPoint);
160
161 std::vector<ext::shared_ptr<Swap> > legs;
162 bool payer = true;
163
164 Leg baseCcyLeg = constantNotionalLeg(schedule: legSchedule(tenor: Period(q.n, q.units), idx: baseCcyIdx),
165 idx: baseCcyIdx, notional: baseCcyLegNotional, basis: baseCcyLegBasis);
166 legs.push_back(x: ext::make_shared<Swap>(args: std::vector<Leg>(1, baseCcyLeg),
167 args: std::vector<bool>(1, !payer)));
168
169 Leg quoteCcyLeg =
170 constantNotionalLeg(schedule: legSchedule(tenor: Period(q.n, q.units), idx: quoteCcyIdx), idx: quoteCcyIdx,
171 notional: quoteCcyLegNotional, basis: quoteCcyLegBasis);
172 legs.push_back(x: ext::make_shared<Swap>(args: std::vector<Leg>(1, quoteCcyLeg),
173 args: std::vector<bool>(1, payer)));
174 return legs;
175 }
176
177 CommonVars() {
178 settlementDays = 2;
179 businessConvention = Following;
180 calendar = TARGET();
181 dayCount = Actual365Fixed();
182 endOfMonth = false;
183
184 basisPoint = 1.0e-4;
185 fxSpot = 1.25;
186
187 baseCcyIdx = ext::shared_ptr<IborIndex>(new Euribor3M(baseCcyIdxHandle));
188 quoteCcyIdx = ext::shared_ptr<IborIndex>(new USDLibor(3 * Months, quoteCcyIdxHandle));
189
190 /* Data source:
191 N. Moreni, A. Pallavicini (2015)
192 FX Modelling in Collateralized Markets: foreign measures, basis curves
193 and pricing formulae.
194
195 section 4.2.1, Table 2.
196 */
197 basisData.emplace_back(args: 1, args: Years, args: -14.5);
198 basisData.emplace_back(args: 18, args: Months, args: -18.5);
199 basisData.emplace_back(args: 2, args: Years, args: -20.5);
200 basisData.emplace_back(args: 3, args: Years, args: -23.75);
201 basisData.emplace_back(args: 4, args: Years, args: -25.5);
202 basisData.emplace_back(args: 5, args: Years, args: -26.5);
203 basisData.emplace_back(args: 7, args: Years, args: -26.75);
204 basisData.emplace_back(args: 10, args: Years, args: -26.25);
205 basisData.emplace_back(args: 15, args: Years, args: -24.75);
206 basisData.emplace_back(args: 20, args: Years, args: -23.25);
207 basisData.emplace_back(args: 30, args: Years, args: -20.50);
208
209 today = calendar.adjust(Date(6, September, 2013));
210 Settings::instance().evaluationDate() = today;
211 settlement = calendar.advance(today, n: settlementDays, unit: Days);
212
213 baseCcyIdxHandle.linkTo(h: flatRate(today: settlement, forward: 0.007, dc: dayCount));
214 quoteCcyIdxHandle.linkTo(h: flatRate(today: settlement, forward: 0.015, dc: dayCount));
215 }
216 };
217}
218
219void testConstantNotionalCrossCurrencySwapsNPV(bool isFxBaseCurrencyCollateralCurrency,
220 bool isBasisOnFxBaseCurrencyLeg) {
221
222 using namespace crosscurrencyratehelpers_test;
223
224 CommonVars vars;
225
226 Handle<YieldTermStructure> collateralHandle =
227 isFxBaseCurrencyCollateralCurrency ? vars.baseCcyIdxHandle : vars.quoteCcyIdxHandle;
228
229 ext::shared_ptr<DiscountingSwapEngine> collateralCcyLegEngine(
230 new DiscountingSwapEngine(collateralHandle));
231
232 std::vector<ext::shared_ptr<RateHelper> > instruments =
233 vars.buildConstantNotionalXccyRateHelpers(xccyData: vars.basisData, collateralHandle,
234 isFxBaseCurrencyCollateralCurrency,
235 isBasisOnFxBaseCurrencyLeg);
236 ext::shared_ptr<YieldTermStructure> foreignCcyCurve(
237 new PiecewiseYieldCurve<Discount, LogLinear>(vars.settlement, instruments, vars.dayCount));
238 foreignCcyCurve->enableExtrapolation();
239 Handle<YieldTermStructure> foreignCcyHandle(foreignCcyCurve);
240 ext::shared_ptr<DiscountingSwapEngine> foreignCcyLegEngine(
241 new DiscountingSwapEngine(foreignCcyHandle));
242
243 Real tolerance = 1.0e-12;
244
245 for (Size i = 0; i < vars.basisData.size(); ++i) {
246
247 XccyTestDatum quote = vars.basisData[i];
248 std::vector<ext::shared_ptr<Swap> > xccySwapProxy = vars.buildXccyBasisSwap(
249 q: quote, fxSpot: vars.fxSpot, isFxBaseCurrencyCollateralCurrency, isBasisOnFxBaseCurrencyLeg);
250
251 if (isFxBaseCurrencyCollateralCurrency) {
252 xccySwapProxy[0]->setPricingEngine(collateralCcyLegEngine);
253 xccySwapProxy[1]->setPricingEngine(foreignCcyLegEngine);
254 } else {
255 xccySwapProxy[0]->setPricingEngine(foreignCcyLegEngine);
256 xccySwapProxy[1]->setPricingEngine(collateralCcyLegEngine);
257 }
258
259 Period p = quote.n * quote.units;
260
261 Real baseCcyLegNpv = vars.fxSpot * xccySwapProxy[0]->NPV();
262 Real quoteCcyLegNpv = xccySwapProxy[1]->NPV();
263 Real npv = baseCcyLegNpv + quoteCcyLegNpv;
264
265 if (std::fabs(x: npv) > tolerance)
266 BOOST_ERROR("unable to price the cross currency basis swap to par\n"
267 << std::setprecision(5) << " calculated NPV: " << npv << "\n"
268 << " expected: " << 0.0 << "\n"
269 << " implied basis: " << quote.basis << "\n"
270 << " tenor: " << p << "\n");
271 }
272}
273
274void testResettingCrossCurrencySwaps(bool isFxBaseCurrencyCollateralCurrency,
275 bool isBasisOnFxBaseCurrencyLeg,
276 bool isFxBaseCurrencyLegResettable) {
277
278 using namespace crosscurrencyratehelpers_test;
279
280 CommonVars vars;
281
282 Handle<YieldTermStructure> collateralHandle =
283 isFxBaseCurrencyCollateralCurrency ? vars.baseCcyIdxHandle : vars.quoteCcyIdxHandle;
284
285 std::vector<ext::shared_ptr<RateHelper> > resettingInstruments =
286 vars.buildResettingXccyRateHelpers(
287 xccyData: vars.basisData, collateralHandle, isFxBaseCurrencyCollateralCurrency,
288 isBasisOnFxBaseCurrencyLeg, isFxBaseCurrencyLegResettable);
289
290 std::vector<ext::shared_ptr<RateHelper> > constNotionalInstruments =
291 vars.buildConstantNotionalXccyRateHelpers(xccyData: vars.basisData, collateralHandle,
292 isFxBaseCurrencyCollateralCurrency,
293 isBasisOnFxBaseCurrencyLeg);
294
295 ext::shared_ptr<YieldTermStructure> resettingCurve(
296 new PiecewiseYieldCurve<Discount, LogLinear>(vars.settlement, resettingInstruments, vars.dayCount));
297 resettingCurve->enableExtrapolation();
298
299 ext::shared_ptr<YieldTermStructure> constNotionalCurve(
300 new PiecewiseYieldCurve<Discount, LogLinear>(vars.settlement, constNotionalInstruments,
301 vars.dayCount));
302 constNotionalCurve->enableExtrapolation();
303
304 Real tolerance = 1.0e-4 * 5;
305 Size numberOfInstruments = vars.basisData.size();
306
307 for (Size i = 0; i < numberOfInstruments; ++i) {
308
309 Date maturity = resettingInstruments[i]->maturityDate();
310 Rate resettingZero = resettingCurve->zeroRate(d: maturity, resultDayCounter: vars.dayCount, comp: Continuous);
311 Rate constNotionalZero = constNotionalCurve->zeroRate(d: maturity, resultDayCounter: vars.dayCount, comp: Continuous);
312
313 // The difference between resetting and constant notional curves
314 // is not expected to be substantial. With the current setup it should
315 // amount to only a few basis points - hence the tolerance level was
316 // set at 5 bps.
317 if (std::fabs(x: resettingZero - constNotionalZero) > tolerance)
318 BOOST_ERROR("too large difference between resetting and constant notional curve \n"
319 << std::setprecision(5)
320 << " zero from resetting curve: " << resettingZero << "\n"
321 << " zero from const notional curve: " << constNotionalZero << "\n"
322 << " maturity: " << maturity << "\n");
323 }
324}
325
326void CrossCurrencyRateHelpersTest::
327 testConstNotionalBasisSwapsWithCollateralInQuoteAndBasisInBaseCcy() {
328 BOOST_TEST_MESSAGE("Testing constant notional basis swaps with collateral in quote ccy and "
329 "basis in base ccy...");
330
331 bool isFxBaseCurrencyCollateralCurrency = false;
332 bool isBasisOnFxBaseCurrencyLeg = true;
333
334 testConstantNotionalCrossCurrencySwapsNPV(isFxBaseCurrencyCollateralCurrency,
335 isBasisOnFxBaseCurrencyLeg);
336}
337
338void CrossCurrencyRateHelpersTest::testConstNotionalBasisSwapsWithCollateralInBaseAndBasisInQuoteCcy() {
339 BOOST_TEST_MESSAGE(
340 "Testing constant notional basis swaps with collateral in base ccy and basis in quote ccy...");
341
342 bool isFxBaseCurrencyCollateralCurrency = true;
343 bool isBasisOnFxBaseCurrencyLeg = false;
344
345 testConstantNotionalCrossCurrencySwapsNPV(isFxBaseCurrencyCollateralCurrency,
346 isBasisOnFxBaseCurrencyLeg);
347}
348
349void CrossCurrencyRateHelpersTest::testConstNotionalBasisSwapsWithCollateralAndBasisInBaseCcy() {
350 BOOST_TEST_MESSAGE(
351 "Testing constant notional basis swaps with collateral and basis in base ccy...");
352
353 bool isFxBaseCurrencyCollateralCurrency = true;
354 bool isBasisOnFxBaseCurrencyLeg = true;
355
356 testConstantNotionalCrossCurrencySwapsNPV(isFxBaseCurrencyCollateralCurrency,
357 isBasisOnFxBaseCurrencyLeg);
358}
359
360void CrossCurrencyRateHelpersTest::testConstNotionalBasisSwapsWithCollateralAndBasisInQuoteCcy() {
361 BOOST_TEST_MESSAGE("Testing constant notional basis swaps with collateral and basis in quote ccy...");
362
363 bool isFxBaseCurrencyCollateralCurrency = false;
364 bool isBasisOnFxBaseCurrencyLeg = false;
365
366 testConstantNotionalCrossCurrencySwapsNPV(isFxBaseCurrencyCollateralCurrency,
367 isBasisOnFxBaseCurrencyLeg);
368}
369
370void CrossCurrencyRateHelpersTest::
371 testResettingBasisSwapsWithCollateralInQuoteAndBasisInBaseCcy() {
372 BOOST_TEST_MESSAGE(
373 "Testing resetting basis swaps with collateral in quote ccy and basis in base ccy...");
374
375 bool isFxBaseCurrencyCollateralCurrency = false;
376 bool isFxBaseCurrencyLegResettable = false;
377 bool isBasisOnFxBaseCurrencyLeg = true;
378
379 testResettingCrossCurrencySwaps(isFxBaseCurrencyCollateralCurrency, isBasisOnFxBaseCurrencyLeg,
380 isFxBaseCurrencyLegResettable);
381}
382
383void CrossCurrencyRateHelpersTest::
384 testResettingBasisSwapsWithCollateralInBaseAndBasisInQuoteCcy() {
385 BOOST_TEST_MESSAGE(
386 "Testing resetting basis swaps with collateral in base ccy and basis in quote ccy...");
387
388 bool isFxBaseCurrencyCollateralCurrency = true;
389 bool isFxBaseCurrencyLegResettable = true;
390 bool isBasisOnFxBaseCurrencyLeg = false;
391
392 testResettingCrossCurrencySwaps(isFxBaseCurrencyCollateralCurrency, isBasisOnFxBaseCurrencyLeg,
393 isFxBaseCurrencyLegResettable);
394}
395
396void CrossCurrencyRateHelpersTest::testResettingBasisSwapsWithCollateralAndBasisInBaseCcy() {
397 BOOST_TEST_MESSAGE("Testing resetting basis swaps with collateral and basis in base ccy...");
398
399 bool isFxBaseCurrencyCollateralCurrency = true;
400 bool isFxBaseCurrencyLegResettable = true;
401 bool isBasisOnFxBaseCurrencyLeg = true;
402
403 testResettingCrossCurrencySwaps(isFxBaseCurrencyCollateralCurrency, isBasisOnFxBaseCurrencyLeg,
404 isFxBaseCurrencyLegResettable);
405}
406
407void CrossCurrencyRateHelpersTest::testResettingBasisSwapsWithCollateralAndBasisInQuoteCcy() {
408 BOOST_TEST_MESSAGE("Testing resetting basis swaps with collateral and basis in quote ccy...");
409
410 bool isFxBaseCurrencyCollateralCurrency = false;
411 bool isFxBaseCurrencyLegResettable = false;
412 bool isBasisOnFxBaseCurrencyLeg = false;
413
414 testResettingCrossCurrencySwaps(isFxBaseCurrencyCollateralCurrency, isBasisOnFxBaseCurrencyLeg,
415 isFxBaseCurrencyLegResettable);
416}
417
418void CrossCurrencyRateHelpersTest::testExceptionWhenInstrumentTenorShorterThanIndexFrequency() {
419 BOOST_TEST_MESSAGE(
420 "Testing exception when instrument tenor is shorter than index frequency...");
421
422 using namespace crosscurrencyratehelpers_test;
423
424 CommonVars vars;
425
426 std::vector<XccyTestDatum> data{{1, Months, 10.0}};
427 Handle<YieldTermStructure> collateralHandle;
428
429 BOOST_CHECK_THROW(
430 std::vector<ext::shared_ptr<RateHelper> > resettingInstruments =
431 vars.buildConstantNotionalXccyRateHelpers(data, collateralHandle, true, true),
432 Error);
433}
434
435test_suite* CrossCurrencyRateHelpersTest::suite() {
436 auto* suite = BOOST_TEST_SUITE("Cross currency rate helpers tests");
437
438 suite->add(
439 QUANTLIB_TEST_CASE(&CrossCurrencyRateHelpersTest::
440 testConstNotionalBasisSwapsWithCollateralInQuoteAndBasisInBaseCcy));
441 suite->add(
442 QUANTLIB_TEST_CASE(&CrossCurrencyRateHelpersTest::
443 testConstNotionalBasisSwapsWithCollateralInBaseAndBasisInQuoteCcy));
444 suite->add(QUANTLIB_TEST_CASE(
445 &CrossCurrencyRateHelpersTest::testConstNotionalBasisSwapsWithCollateralAndBasisInBaseCcy));
446 suite->add(QUANTLIB_TEST_CASE(&CrossCurrencyRateHelpersTest::
447 testConstNotionalBasisSwapsWithCollateralAndBasisInQuoteCcy));
448
449 suite->add(
450 QUANTLIB_TEST_CASE(&CrossCurrencyRateHelpersTest::
451 testResettingBasisSwapsWithCollateralInQuoteAndBasisInBaseCcy));
452 suite->add(
453 QUANTLIB_TEST_CASE(&CrossCurrencyRateHelpersTest::
454 testResettingBasisSwapsWithCollateralInBaseAndBasisInQuoteCcy));
455 suite->add(QUANTLIB_TEST_CASE(
456 &CrossCurrencyRateHelpersTest::testResettingBasisSwapsWithCollateralAndBasisInBaseCcy));
457 suite->add(QUANTLIB_TEST_CASE(
458 &CrossCurrencyRateHelpersTest::testResettingBasisSwapsWithCollateralAndBasisInQuoteCcy));
459
460 suite->add(QUANTLIB_TEST_CASE(
461 &CrossCurrencyRateHelpersTest::testExceptionWhenInstrumentTenorShorterThanIndexFrequency));
462 return suite;
463}
464

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