1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3/*
4 Copyright (C) 2018 Peter Caspers
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 "cmsspread.hpp"
21#include "utilities.hpp"
22
23#include <ql/cashflows/cmscoupon.hpp>
24#include <ql/cashflows/lineartsrpricer.hpp>
25#include <ql/experimental/coupons/cmsspreadcoupon.hpp>
26#include <ql/experimental/coupons/lognormalcmsspreadpricer.hpp>
27#include <ql/indexes/swap/euriborswap.hpp>
28#include <ql/math/array.hpp>
29#include <ql/math/comparison.hpp>
30#include <ql/math/distributions/normaldistribution.hpp>
31#include <ql/math/matrixutilities/pseudosqrt.hpp>
32#include <ql/math/randomnumbers/sobolrsg.hpp>
33#include <ql/quotes/simplequote.hpp>
34#include <ql/termstructures/volatility/swaption/swaptionconstantvol.hpp>
35#include <ql/termstructures/yield/flatforward.hpp>
36#include <ql/time/calendars/target.hpp>
37#include <ql/time/daycounters/actual360.hpp>
38
39#include <boost/accumulators/accumulators.hpp>
40#include <boost/accumulators/statistics/mean.hpp>
41#include <boost/accumulators/statistics/stats.hpp>
42
43using namespace QuantLib;
44using namespace boost::unit_test_framework;
45using namespace boost::accumulators;
46
47namespace {
48struct TestData {
49 TestData() {
50 refDate = Date(23, February, 2018);
51 Settings::instance().evaluationDate() = refDate;
52
53 yts2 = Handle<YieldTermStructure>(
54 ext::make_shared<FlatForward>(args&: refDate, args: 0.02, args: Actual365Fixed()));
55
56 swLn = Handle<SwaptionVolatilityStructure>(
57 ext::make_shared<ConstantSwaptionVolatility>(
58 args&: refDate, args: TARGET(), args: Following, args: 0.20, args: Actual365Fixed(),
59 args: ShiftedLognormal, args: 0.0));
60 swSln = Handle<SwaptionVolatilityStructure>(
61 ext::make_shared<ConstantSwaptionVolatility>(
62 args&: refDate, args: TARGET(), args: Following, args: 0.10, args: Actual365Fixed(),
63 args: ShiftedLognormal, args: 0.01));
64 swN = Handle<SwaptionVolatilityStructure>(
65 ext::make_shared<ConstantSwaptionVolatility>(
66 args&: refDate, args: TARGET(), args: Following, args: 0.0075, args: Actual365Fixed(), args: Normal,
67 args: 0.01));
68
69 reversion = Handle<Quote>(ext::make_shared<SimpleQuote>(args: 0.01));
70 cmsPricerLn =
71 ext::make_shared<LinearTsrPricer>(args&: swLn, args&: reversion, args&: yts2);
72 cmsPricerSln =
73 ext::make_shared<LinearTsrPricer>(args&: swSln, args&: reversion, args&: yts2);
74 cmsPricerN = ext::make_shared<LinearTsrPricer>(args&: swN, args&: reversion, args&: yts2);
75
76 correlation = Handle<Quote>(ext::make_shared<SimpleQuote>(args: 0.6));
77 cmsspPricerLn = ext::make_shared<LognormalCmsSpreadPricer>(
78 args&: cmsPricerLn, args&: correlation, args&: yts2, args: 32);
79 cmsspPricerSln = ext::make_shared<LognormalCmsSpreadPricer>(
80 args&: cmsPricerSln, args&: correlation, args&: yts2, args: 32);
81 cmsspPricerN = ext::make_shared<LognormalCmsSpreadPricer>(
82 args&: cmsPricerN, args&: correlation, args&: yts2, args: 32);
83 }
84
85 Date refDate;
86 Handle<YieldTermStructure> yts2;
87 Handle<SwaptionVolatilityStructure> swLn, swSln, swN;
88 Handle<Quote> reversion, correlation;
89 ext::shared_ptr<CmsCouponPricer> cmsPricerLn, cmsPricerSln, cmsPricerN;
90 ext::shared_ptr<CmsSpreadCouponPricer> cmsspPricerLn, cmsspPricerSln,
91 cmsspPricerN;
92};
93} // namespace
94
95void CmsSpreadTest::testFixings() {
96 BOOST_TEST_MESSAGE("Testing fixings of cms spread indices...");
97
98 TestData d;
99
100 ext::shared_ptr<SwapIndex> cms10y =
101 ext::make_shared<EuriborSwapIsdaFixA>(args: 10 * Years, args&: d.yts2, args&: d.yts2);
102 ext::shared_ptr<SwapIndex> cms2y =
103 ext::make_shared<EuriborSwapIsdaFixA>(args: 2 * Years, args&: d.yts2, args&: d.yts2);
104 ext::shared_ptr<SwapSpreadIndex> cms10y2y =
105 ext::make_shared<SwapSpreadIndex>(args: "cms10y2y", args&: cms10y, args&: cms2y);
106
107 Settings::instance().enforcesTodaysHistoricFixings() = false;
108
109 BOOST_CHECK_THROW(cms10y2y->fixing(d.refDate - 1), QuantLib::Error);
110 BOOST_REQUIRE_NO_THROW(cms10y2y->fixing(d.refDate));
111 BOOST_CHECK_EQUAL(cms10y2y->fixing(d.refDate),
112 cms10y->fixing(d.refDate) - cms2y->fixing(d.refDate));
113 cms10y->addFixing(fixingDate: d.refDate, fixing: 0.05);
114 BOOST_CHECK_EQUAL(cms10y2y->fixing(d.refDate),
115 cms10y->fixing(d.refDate) - cms2y->fixing(d.refDate));
116 cms2y->addFixing(fixingDate: d.refDate, fixing: 0.04);
117 BOOST_CHECK_EQUAL(cms10y2y->fixing(d.refDate),
118 cms10y->fixing(d.refDate) - cms2y->fixing(d.refDate));
119 Date futureFixingDate = TARGET().adjust(d.refDate + 1 * Years);
120 BOOST_CHECK_EQUAL(cms10y2y->fixing(futureFixingDate),
121 cms10y->fixing(futureFixingDate) -
122 cms2y->fixing(futureFixingDate));
123 IndexManager::instance().clearHistories();
124
125 Settings::instance().enforcesTodaysHistoricFixings() = true;
126 BOOST_CHECK_THROW(cms10y2y->fixing(d.refDate), QuantLib::Error);
127 cms10y->addFixing(fixingDate: d.refDate, fixing: 0.05);
128 BOOST_CHECK_THROW(cms10y2y->fixing(d.refDate), QuantLib::Error);
129 cms2y->addFixing(fixingDate: d.refDate, fixing: 0.04);
130 BOOST_CHECK_EQUAL(cms10y2y->fixing(d.refDate),
131 cms10y->fixing(d.refDate) - cms2y->fixing(d.refDate));
132}
133
134namespace {
135Real mcReferenceValue(const ext::shared_ptr<CmsCoupon>& cpn1,
136 const ext::shared_ptr<CmsCoupon>& cpn2, const Real cap,
137 const Real floor,
138 const Handle<SwaptionVolatilityStructure>& vol,
139 const Real correlation) {
140 Size samples = 1000000;
141 accumulator_set<Real, stats<tag::mean> > acc;
142 Matrix Cov(2, 2);
143 Cov(0, 0) = vol->blackVariance(optionDate: cpn1->fixingDate(), swapTenor: cpn1->index()->tenor(),
144 strike: cpn1->indexFixing());
145 Cov(1, 1) = vol->blackVariance(optionDate: cpn2->fixingDate(), swapTenor: cpn2->index()->tenor(),
146 strike: cpn2->indexFixing());
147 Cov(0, 1) = Cov(1, 0) = std::sqrt(x: Cov(0, 0) * Cov(1, 1)) * correlation;
148 Matrix C = pseudoSqrt(Cov);
149
150 Array atmRate(2), adjRate(2), avg(2), volShift(2);
151 atmRate[0] = cpn1->indexFixing();
152 atmRate[1] = cpn2->indexFixing();
153 adjRate[0] = cpn1->adjustedFixing();
154 adjRate[1] = cpn2->adjustedFixing();
155 if (vol->volatilityType() == ShiftedLognormal) {
156 volShift[0] = vol->shift(optionDate: cpn1->fixingDate(), swapTenor: cpn1->index()->tenor());
157 volShift[1] = vol->shift(optionDate: cpn2->fixingDate(), swapTenor: cpn2->index()->tenor());
158 avg[0] =
159 std::log(x: (adjRate[0] + volShift[0]) / (atmRate[0] + volShift[0])) -
160 0.5 * Cov(0, 0);
161 avg[1] =
162 std::log(x: (adjRate[1] + volShift[1]) / (atmRate[1] + volShift[1])) -
163 0.5 * Cov(1, 1);
164 } else {
165 avg[0] = adjRate[0];
166 avg[1] = adjRate[1];
167 }
168
169 InverseCumulativeNormal icn;
170 SobolRsg sb_(2, 42);
171 Array w(2), z(2);
172 for (Size i = 0; i < samples; ++i) {
173 std::vector<Real> seq = sb_.nextSequence().value;
174 std::transform(first: seq.begin(), last: seq.end(), result: w.begin(), unary_op: icn);
175 z = C * w + avg;
176 for (Size i = 0; i < 2; ++i) {
177 if (vol->volatilityType() == ShiftedLognormal) {
178 z[i] =
179 (atmRate[i] + volShift[i]) * std::exp(x: z[i]) - volShift[i];
180 }
181 }
182 acc(std::min(a: std::max(a: z[0] - z[1], b: floor), b: cap));
183 }
184 return mean(acc);
185} // mcReferenceValue
186} // namespace
187
188void CmsSpreadTest::testCouponPricing() {
189 BOOST_TEST_MESSAGE("Testing pricing of cms spread coupons...");
190
191 TestData d;
192 Real tol = 1E-6; // abs tolerance coupon rate
193
194 ext::shared_ptr<SwapIndex> cms10y =
195 ext::make_shared<EuriborSwapIsdaFixA>(args: 10 * Years, args&: d.yts2, args&: d.yts2);
196 ext::shared_ptr<SwapIndex> cms2y =
197 ext::make_shared<EuriborSwapIsdaFixA>(args: 2 * Years, args&: d.yts2, args&: d.yts2);
198 ext::shared_ptr<SwapSpreadIndex> cms10y2y =
199 ext::make_shared<SwapSpreadIndex>(args: "cms10y2y", args&: cms10y, args&: cms2y);
200
201 Date valueDate = cms10y2y->valueDate(fixingDate: d.refDate);
202 Date payDate = valueDate + 1 * Years;
203 ext::shared_ptr<CmsCoupon> cpn1a =
204 ext::make_shared<CmsCoupon>(
205 args&: payDate, args: 10000.0, args&: valueDate, args&: payDate, args: cms10y->fixingDays(), args&: cms10y,
206 args: 1.0, args: 0.0, args: Date(), args: Date(), args: Actual360(), args: false);
207 ext::shared_ptr<CmsCoupon> cpn1b = ext::make_shared<CmsCoupon>(
208 args&: payDate, args: 10000.0, args&: valueDate, args&: payDate, args: cms2y->fixingDays(),
209 args&: cms2y, args: 1.0, args: 0.0, args: Date(), args: Date(), args: Actual360(), args: false);
210 ext::shared_ptr<CmsSpreadCoupon> cpn1 =
211 ext::make_shared<CmsSpreadCoupon>(
212 args&: payDate, args: 10000.0, args&: valueDate, args&: payDate, args: cms10y2y->fixingDays(),
213 args&: cms10y2y, args: 1.0, args: 0.0, args: Date(), args: Date(), args: Actual360(), args: false);
214 BOOST_CHECK(cpn1->fixingDate() == d.refDate);
215 cpn1a->setPricer(d.cmsPricerLn);
216 cpn1b->setPricer(d.cmsPricerLn);
217 cpn1->setPricer(d.cmsspPricerLn);
218
219#ifndef __FAST_MATH__
220 constexpr double eqTol = 100*QL_EPSILON;
221#else
222 constexpr double eqTol = 1e-13;
223#endif
224 QL_CHECK_CLOSE(cpn1->rate(), cpn1a->rate() - cpn1b->rate(), eqTol);
225 cms10y->addFixing(fixingDate: d.refDate, fixing: 0.05);
226 QL_CHECK_CLOSE(cpn1->rate(), cpn1a->rate() - cpn1b->rate(), eqTol);
227 cms2y->addFixing(fixingDate: d.refDate, fixing: 0.03);
228 QL_CHECK_CLOSE(cpn1->rate(), cpn1a->rate() - cpn1b->rate(), eqTol);
229 IndexManager::instance().clearHistories();
230
231 ext::shared_ptr<CmsCoupon> cpn2a = ext::make_shared<CmsCoupon>(
232 args: Date(23, February, 2029), args: 10000.0,
233 args: Date(23, February, 2028), args: Date(23, February, 2029), args: 2,
234 args&: cms10y, args: 1.0, args: 0.0, args: Date(), args: Date(), args: Actual360(), args: false);
235 ext::shared_ptr<CmsCoupon> cpn2b = ext::make_shared<CmsCoupon>(
236 args: Date(23, February, 2029), args: 10000.0,
237 args: Date(23, February, 2028), args: Date(23, February, 2029), args: 2,
238 args&: cms2y, args: 1.0, args: 0.0, args: Date(), args: Date(), args: Actual360(), args: false);
239
240 ext::shared_ptr<CappedFlooredCmsSpreadCoupon> plainCpn =
241 ext::make_shared<CappedFlooredCmsSpreadCoupon>(
242 args: Date(23, February, 2029), args: 10000.0, args: Date(23, February, 2028),
243 args: Date(23, February, 2029), args: 2, args&: cms10y2y, args: 1.0, args: 0.0, args: Null<Rate>(),
244 args: Null<Rate>(), args: Date(), args: Date(), args: Actual360(), args: false);
245 ext::shared_ptr<CappedFlooredCmsSpreadCoupon> cappedCpn =
246 ext::make_shared<CappedFlooredCmsSpreadCoupon>(
247 args: Date(23, February, 2029), args: 10000.0, args: Date(23, February, 2028),
248 args: Date(23, February, 2029), args: 2, args&: cms10y2y, args: 1.0, args: 0.0, args: 0.03,
249 args: Null<Rate>(), args: Date(), args: Date(), args: Actual360(), args: false);
250 ext::shared_ptr<CappedFlooredCmsSpreadCoupon> flooredCpn =
251 ext::make_shared<CappedFlooredCmsSpreadCoupon>(
252 args: Date(23, February, 2029), args: 10000.0, args: Date(23, February, 2028),
253 args: Date(23, February, 2029), args: 2, args&: cms10y2y, args: 1.0, args: 0.0, args: Null<Rate>(),
254 args: 0.01, args: Date(), args: Date(), args: Actual360(), args: false);
255 ext::shared_ptr<CappedFlooredCmsSpreadCoupon> collaredCpn =
256 ext::make_shared<CappedFlooredCmsSpreadCoupon>(
257 args: Date(23, February, 2029), args: 10000.0, args: Date(23, February, 2028),
258 args: Date(23, February, 2029), args: 2, args&: cms10y2y, args: 1.0, args: 0.0, args: 0.03, args: 0.01,
259 args: Date(), args: Date(), args: Actual360(), args: false);
260
261 cpn2a->setPricer(d.cmsPricerLn);
262 cpn2b->setPricer(d.cmsPricerLn);
263 plainCpn->setPricer(d.cmsspPricerLn);
264 cappedCpn->setPricer(d.cmsspPricerLn);
265 flooredCpn->setPricer(d.cmsspPricerLn);
266 collaredCpn->setPricer(d.cmsspPricerLn);
267
268 QL_CHECK_SMALL(
269 std::abs(plainCpn->rate() - mcReferenceValue(cpn2a, cpn2b, QL_MAX_REAL,
270 -QL_MAX_REAL, d.swLn,
271 d.correlation->value())),
272 tol);
273 QL_CHECK_SMALL(
274 std::abs(cappedCpn->rate() - mcReferenceValue(cpn2a, cpn2b, 0.03,
275 -QL_MAX_REAL, d.swLn,
276 d.correlation->value())),
277 tol);
278 QL_CHECK_SMALL(
279 std::abs(flooredCpn->rate() -
280 mcReferenceValue(cpn2a, cpn2b, QL_MAX_REAL, 0.01, d.swLn,
281 d.correlation->value())),
282
283 tol);
284 QL_CHECK_SMALL(
285 std::abs(collaredCpn->rate() -
286 mcReferenceValue(cpn2a, cpn2b, 0.03, 0.01, d.swLn,
287 d.correlation->value())),
288 tol);
289
290 cpn2a->setPricer(d.cmsPricerSln);
291 cpn2b->setPricer(d.cmsPricerSln);
292 plainCpn->setPricer(d.cmsspPricerSln);
293 cappedCpn->setPricer(d.cmsspPricerSln);
294 flooredCpn->setPricer(d.cmsspPricerSln);
295 collaredCpn->setPricer(d.cmsspPricerSln);
296
297 QL_CHECK_SMALL(
298 std::abs(plainCpn->rate() - mcReferenceValue(cpn2a, cpn2b, QL_MAX_REAL,
299 -QL_MAX_REAL, d.swSln,
300 d.correlation->value())),
301 tol);
302 QL_CHECK_SMALL(
303 std::abs(cappedCpn->rate() - mcReferenceValue(cpn2a, cpn2b, 0.03,
304 -QL_MAX_REAL, d.swSln,
305 d.correlation->value())),
306 tol);
307 QL_CHECK_SMALL(
308 std::abs(flooredCpn->rate() -
309 mcReferenceValue(cpn2a, cpn2b, QL_MAX_REAL, 0.01, d.swSln,
310 d.correlation->value())),
311
312 tol);
313 QL_CHECK_SMALL(
314 std::abs(collaredCpn->rate() -
315 mcReferenceValue(cpn2a, cpn2b, 0.03, 0.01, d.swSln,
316 d.correlation->value())),
317 tol);
318
319 cpn2a->setPricer(d.cmsPricerN);
320 cpn2b->setPricer(d.cmsPricerN);
321 plainCpn->setPricer(d.cmsspPricerN);
322 cappedCpn->setPricer(d.cmsspPricerN);
323 flooredCpn->setPricer(d.cmsspPricerN);
324 collaredCpn->setPricer(d.cmsspPricerN);
325
326 QL_CHECK_SMALL(
327 std::abs(plainCpn->rate() - mcReferenceValue(cpn2a, cpn2b, QL_MAX_REAL,
328 -QL_MAX_REAL, d.swN,
329 d.correlation->value())),
330 tol);
331 QL_CHECK_SMALL(
332 std::abs(cappedCpn->rate() - mcReferenceValue(cpn2a, cpn2b, 0.03,
333 -QL_MAX_REAL, d.swN,
334 d.correlation->value())),
335 tol);
336 QL_CHECK_SMALL(std::abs(flooredCpn->rate() -
337 mcReferenceValue(cpn2a, cpn2b, QL_MAX_REAL, 0.01,
338 d.swN, d.correlation->value())),
339
340 tol);
341 QL_CHECK_SMALL(std::abs(collaredCpn->rate() -
342 mcReferenceValue(cpn2a, cpn2b, 0.03, 0.01, d.swN,
343 d.correlation->value())),
344 tol);
345}
346
347test_suite* CmsSpreadTest::suite() {
348 auto* suite = BOOST_TEST_SUITE("CmsSpreadTest");
349 suite->add(QUANTLIB_TEST_CASE(&CmsSpreadTest::testFixings));
350 suite->add(QUANTLIB_TEST_CASE(&CmsSpreadTest::testCouponPricing));
351 return suite;
352}
353

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