Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
piecewiseoptionletcurve.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2019 Quaternion Risk Management Ltd
3 All rights reserved.
4
5 This file is part of ORE, a free-software/open-source library
6 for transparent pricing and risk analysis - http://opensourcerisk.org
7
8 ORE is free software: you can redistribute it and/or modify it
9 under the terms of the Modified BSD License. You should have received a
10 copy of the license along with this program.
11 The license is also available online at <http://opensourcerisk.org>
12
13 This program is distributed on the basis that it will form a useful
14 contribution to risk analytics and model standardisation, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
19#include <boost/assign.hpp>
20// clang-format off
21#include <boost/test/unit_test.hpp>
22#include <boost/test/data/test_case.hpp>
23// clang-format on
24#include <boost/variant.hpp>
25
29
30#include <ql/instruments/makecapfloor.hpp>
31#include <ql/pricingengines/capfloor/bacheliercapfloorengine.hpp>
32#include <ql/pricingengines/capfloor/blackcapfloorengine.hpp>
33#include <ql/time/date.hpp>
37
38using namespace QuantExt;
39using namespace QuantLib;
40using namespace boost::unit_test_framework;
41
43typedef QuantLib::BootstrapHelper<QuantLib::OptionletVolatilityStructure> helper;
44using boost::assign::list_of;
45using boost::assign::map_list_of;
46using std::boolalpha;
47using std::fixed;
48using std::map;
49using std::ostream;
50using std::pair;
51using std::setprecision;
52using std::string;
53using std::vector;
54
55namespace {
56
57// Variables to be used in the test
58struct CommonVars : public qle::test::TopLevelFixture {
59
60 // Constructor
61 CommonVars()
62 : referenceDate(5, Feb, 2016), settlementDays(0), calendar(TARGET()), bdc(Following),
63 dayCounter(Actual365Fixed()), accuracy(1.0e-12), globalAccuracy(1.0e-10), tolerance(1.0e-10) {
64
65 // Reference date
66 Settings::instance().evaluationDate() = referenceDate;
67
68 // Set cap floor ibor index to EUR-EURIBOR-6M and attach a forwarding curve
69 iborIndex = QuantLib::ext::make_shared<Euribor6M>(testYieldCurves.forward6M);
70 }
71
72 // Valuation date for the test
73 Date referenceDate;
74
75 // Variables used in the optionlet volatility structure creation
76 Natural settlementDays;
77 Calendar calendar;
78 BusinessDayConvention bdc;
79 DayCounter dayCounter;
80
81 // Accuracy for optionlet stripping
82 Real accuracy;
83
84 // Global accuracy for optionlet stripping
85 Real globalAccuracy;
86
87 // Test tolerance for comparing the NPVs
88 Real tolerance;
89
90 // Cap floor ibor index
91 QuantLib::ext::shared_ptr<IborIndex> iborIndex;
92
93 // EUR discount curve test data from file test/yieldcurvemarketdata.hpp
94 YieldCurveEUR testYieldCurves;
95};
96
97// Struct to hold a cap floor volatility column and some associated meta data
98struct VolatilityColumn {
99 Real strike;
100 vector<Period> tenors;
101 vector<Real> volatilities;
102 VolatilityType type;
103 Real displacement;
104};
105
106// Needed for data driven test case below as it writes out the VolatilityColumn
107ostream& operator<<(ostream& os, const VolatilityColumn& vc) {
108 return os << "Column with strike: " << vc.strike << ", volatility type: " << vc.type
109 << ", shift: " << vc.displacement;
110}
111
112// From the EUR cap floor test volatility data in file test/capfloormarketdata.hpp, create a vector of
113// VolatilityColumns which will be the data in the data driven test below
114vector<VolatilityColumn> generateVolatilityColumns() {
115
116 // Will hold the generated volatility columns
117 vector<VolatilityColumn> volatilityColumns;
118
119 // EUR cap floor test volatility data from file test/capfloormarketdata.hpp
120 CapFloorVolatilityEUR testVols;
121
122 VolatilityColumn volatilityColumn;
123 vector<Real> volatilities(testVols.tenors.size());
124 volatilityColumn.tenors = testVols.tenors;
125
126 // The normal volatilities
127 volatilityColumn.type = Normal;
128 volatilityColumn.displacement = 0.0;
129 for (Size j = 0; j < testVols.strikes.size(); j++) {
130 for (Size i = 0; i < testVols.tenors.size(); i++) {
131 volatilities[i] = testVols.nVols[i][j];
132 }
133 volatilityColumn.strike = testVols.strikes[j];
134 volatilityColumn.volatilities = volatilities;
135 volatilityColumns.push_back(volatilityColumn);
136 }
137
138 // The shifted lognormal volatilities with shift 1
139 volatilityColumn.type = ShiftedLognormal;
140 volatilityColumn.displacement = testVols.shift_1;
141 for (Size j = 0; j < testVols.strikes.size(); j++) {
142 for (Size i = 0; i < testVols.tenors.size(); i++) {
143 volatilities[i] = testVols.slnVols_1[i][j];
144 }
145 volatilityColumn.strike = testVols.strikes[j];
146 volatilityColumn.volatilities = volatilities;
147 volatilityColumns.push_back(volatilityColumn);
148 }
149
150 // The shifted lognormal volatilities with shift 2
151 volatilityColumn.type = ShiftedLognormal;
152 volatilityColumn.displacement = testVols.shift_2;
153 for (Size j = 0; j < testVols.strikes.size(); j++) {
154 for (Size i = 0; i < testVols.tenors.size(); i++) {
155 volatilities[i] = testVols.slnVols_2[i][j];
156 }
157 volatilityColumn.strike = testVols.strikes[j];
158 volatilityColumn.volatilities = volatilities;
159 volatilityColumns.push_back(volatilityColumn);
160 }
161
162 return volatilityColumns;
163}
164
165// Cap floor helper types for the data driven test case
166vector<CapFloorHelper::Type> helperTypes =
168
169// Quote types for the data driven test case
170vector<CapFloorHelper::QuoteType> quoteTypes = list_of(CapFloorHelper::Volatility)(CapFloorHelper::Premium);
171
172// Interpolation types for the data driven test case
173typedef boost::variant<Linear, BackwardFlat, QuantExt::LinearFlat, Cubic, QuantExt::CubicFlat> InterpolationType;
174
175vector<InterpolationType> interpolationTypes = list_of(InterpolationType(Linear()))(InterpolationType(BackwardFlat()))(
176 InterpolationType(QuantExt::LinearFlat()))(InterpolationType(Cubic()))(InterpolationType(QuantExt::CubicFlat()));
177
178vector<InterpolationType> interpolationTypesCached =
179 list_of(InterpolationType(Linear()))(InterpolationType(BackwardFlat()))(InterpolationType(QuantExt::LinearFlat()));
180
181// If the built optionlet structure in the test has a floating or fixed reference date
182vector<bool> isMovingValues = list_of(true)(false);
183
184// If the optionlet structure has a flat first period or not
185vector<bool> flatFirstPeriodValues = list_of(true)(false);
186
187// So that I can reuse below
188string to_string(const InterpolationType& interpolationType) {
189 string result;
190 switch (interpolationType.which()) {
191 case 0:
192 result = "Linear";
193 break;
194 case 1:
195 result = "BackwardFlat";
196 break;
197 case 2:
198 result = "LinearFlat";
199 break;
200 case 3:
201 result = "Cubic";
202 break;
203 case 4:
204 result = "CubicFlat";
205 break;
206 default:
207 BOOST_FAIL("Unexpected interpolation type");
208 }
209 return result;
210}
211
212// Cached values for comparison below. We compare against cached values for a given interpolation type and setting of
213// the bool flatFirstPeriod. The cached values are keyed on the Size: 2 * interpolationType + flatFirstPeriod.
214// clang-format off
215vector<Date> cachedDates = list_of(Date(5, Feb, 2016))(Date(7, Feb, 2017))(Date(6, Aug, 2020))(Date(5, Aug, 2022))(Date(7, Aug, 2025))(Date(7, Aug, 2035));
216map<Size, vector<Real> > cachedValues = map_list_of
217 // Linear, flat first = false
218 (0, list_of(0.000000000000)(0.009939243164)(0.008398540935)(0.008216105988)(0.006859464219)(0.006598726907).convert_to_container<vector<Real> >())
219 // Linear, flat first = true
220 (1, list_of(0.009938000000)(0.009938000000)(0.008399019469)(0.008215852284)(0.006859635836)(0.006598586367).convert_to_container<vector<Real> >())
221 // BackwardFlat, flat first = false
222 (2, list_of(0.000000000000)(0.009938000000)(0.008799306892)(0.008279139515)(0.007401656494)(0.006715983817).convert_to_container<vector<Real> >())
223 // BackwardFlat, flat first = true
224 (3, list_of(0.009938000000)(0.009938000000)(0.008799306892)(0.008279139515)(0.007401656494)(0.006715983817).convert_to_container<vector<Real> >())
225 // LinearFlat, flat first = false
226 (4, list_of(0.000000000000)(0.009939243164)(0.008398540935)(0.008216105988)(0.006859464219)(0.006598726907).convert_to_container<vector<Real> >())
227 // LinearFlat, flat first = true
228 (5, list_of(0.009938000000)(0.009938000000)(0.008399019469)(0.008215852284)(0.006859635836)(0.006598586367).convert_to_container<vector<Real> >());
229// clang-format on
230
231// Cached values, on dates that are not curve nodes, for comparison below.
232// We pick a value in the first curve period to check the flatFirstPeriod setting, in the middle of the curve to check
233// the interpolation and after the last curve date to check the extrapolation.
234// clang-format off
235vector<Date> cachedNonNodeDates = list_of(Date(5, Aug, 2016))(Date(6, Aug, 2021))(Date(7, Aug, 2036));
236map<Size, vector<Real> > cachedNonNodeValues = map_list_of
237 // Linear, flat first = false
238 (0, list_of(0.004915603956)(0.008307198335)(0.006572596059).convert_to_container<vector<Real> >())
239 // Linear, flat first = true
240 (1, list_of(0.009938000000)(0.008307310247)(0.006572424235).convert_to_container<vector<Real> >())
241 // BackwardFlat, flat first = false
242 (2, list_of(0.009938000000)(0.008279139515)(0.006715983817).convert_to_container<vector<Real> >())
243 // BackwardFlat, flat first = true
244 (3, list_of(0.009938000000)(0.008279139515)(0.006715983817).convert_to_container<vector<Real> >())
245 // LinearFlat, flat first = false
246 (4, list_of(0.004915603956)(0.008307198335)(0.006598726907).convert_to_container<vector<Real> >())
247 // LinearFlat, flat first = true
248 (5, list_of(0.009938000000)(0.008307310247)(0.006598586367).convert_to_container<vector<Real> >());
249// clang-format on
250
251} // namespace
252
253// Needed for BOOST_DATA_TEST_CASE below
254// https://stackoverflow.com/a/33965517/1771882
255namespace boost {
256namespace test_tools {
257namespace tt_detail {
258template <> struct print_log_value<InterpolationType> {
259 void operator()(ostream& os, const InterpolationType& interpolationType) { os << to_string(interpolationType); }
260};
261} // namespace tt_detail
262} // namespace test_tools
263} // namespace boost
264
265BOOST_FIXTURE_TEST_SUITE(QuantExtTestSuite, qle::test::TopLevelFixture)
266
267BOOST_AUTO_TEST_SUITE(PiecewiseOptionletCurveTests)
268
269BOOST_DATA_TEST_CASE_F(CommonVars, testPiecewiseOptionletStripping,
270 bdata::make(generateVolatilityColumns()) * bdata::make(helperTypes) * bdata::make(quoteTypes) *
271 bdata::make(interpolationTypes) * bdata::make(isMovingValues) *
272 bdata::make(flatFirstPeriodValues),
273 volatilityColumn, helperType, quoteType, interpolationType, isMoving, flatFirstPeriod) {
274
275 BOOST_TEST_MESSAGE("Testing piecewise optionlet stripping of cap floor quotes along a strike column");
276
277 BOOST_TEST_MESSAGE("Test inputs are:");
278 BOOST_TEST_MESSAGE(" Cap floor helper type: " << helperType);
279 BOOST_TEST_MESSAGE(" Cap floor strike: " << volatilityColumn.strike);
280 BOOST_TEST_MESSAGE(" Quote type: " << quoteType);
281 if (quoteType == CapFloorHelper::Volatility) {
282 BOOST_TEST_MESSAGE(" Quote volatility type: " << volatilityColumn.type);
283 BOOST_TEST_MESSAGE(" Quote displacement: " << volatilityColumn.displacement);
284 }
285 BOOST_TEST_MESSAGE(" Interpolation type: " << to_string(interpolationType));
286 BOOST_TEST_MESSAGE(" Floating reference date: " << boolalpha << isMoving);
287 BOOST_TEST_MESSAGE(" Flat first period: " << boolalpha << flatFirstPeriod);
288
289 if (quoteType == CapFloorHelper::Premium && helperType == CapFloorHelper::Automatic) {
290
291 // This is a combination that should throw an error when creating the helper
292 // Don't care about the value of the premium quote
293 Handle<Quote> quote(QuantLib::ext::make_shared<SimpleQuote>(0.01));
294 BOOST_REQUIRE_THROW(
295 QuantLib::ext::make_shared<CapFloorHelper>(helperType, volatilityColumn.tenors.front(), volatilityColumn.strike,
296 quote, iborIndex, testYieldCurves.discountEonia, isMoving, Date(),
297 quoteType, volatilityColumn.type, volatilityColumn.displacement),
298 Error);
299
300 } else {
301
302 // Form the cap floor helper instrument for each tenor in the strike column
303 vector<QuantLib::ext::shared_ptr<helper> > helpers(volatilityColumn.tenors.size());
304
305 // Store each cap floor instrument in the strike column and its NPV using the flat cap floor volatilities
306 vector<QuantLib::ext::shared_ptr<CapFloor> > instruments(volatilityColumn.tenors.size());
307 vector<Real> flatNpvs(volatilityColumn.tenors.size());
308
309 BOOST_TEST_MESSAGE("The input values at each tenor are:");
310 for (Size i = 0; i < volatilityColumn.tenors.size(); i++) {
311
312 // Get the relevant quote value
313 Real volatility = volatilityColumn.volatilities[i];
314
315 // Create the cap floor instrument and store its price using the quoted flat volatility
316 CapFloor::Type capFloorType = CapFloor::Cap;
317 if (helperType == CapFloorHelper::Floor) {
318 capFloorType = CapFloor::Floor;
319 }
320 instruments[i] = MakeCapFloor(capFloorType, volatilityColumn.tenors[i], iborIndex, volatilityColumn.strike);
321 if (volatilityColumn.type == ShiftedLognormal) {
322 instruments[i]->setPricingEngine(QuantLib::ext::make_shared<BlackCapFloorEngine>(
323 testYieldCurves.discountEonia, volatility, dayCounter, volatilityColumn.displacement));
324 } else {
325 instruments[i]->setPricingEngine(
326 QuantLib::ext::make_shared<BachelierCapFloorEngine>(testYieldCurves.discountEonia, volatility, dayCounter));
327 }
328 flatNpvs[i] = instruments[i]->NPV();
329
330 BOOST_TEST_MESSAGE(" (Cap/Floor, Tenor, Volatility, Flat NPV) = ("
331 << capFloorType << ", " << volatilityColumn.tenors[i] << ", " << fixed
332 << setprecision(13) << volatility << ", " << flatNpvs[i] << ")");
333
334 // Create a volatility or premium quote
335 RelinkableHandle<Quote> quote;
336 if (quoteType == CapFloorHelper::Volatility) {
337 quote.linkTo(QuantLib::ext::make_shared<SimpleQuote>(volatility));
338 } else {
339 quote.linkTo(QuantLib::ext::make_shared<SimpleQuote>(flatNpvs[i]));
340 }
341
342 // Create the helper instrument
343 helpers[i] =
344 QuantLib::ext::make_shared<CapFloorHelper>(helperType, volatilityColumn.tenors[i], volatilityColumn.strike,
345 quote, iborIndex, testYieldCurves.discountEonia, isMoving, Date(),
346 quoteType, volatilityColumn.type, volatilityColumn.displacement);
347 }
348
349 // Create the piecewise optionlet curve, with the given interpolation type, and fail if it is not created
350 VolatilityType curveVolatilityType = Normal;
351 Real curveDisplacement = 0.0;
352 QuantLib::ext::shared_ptr<OptionletVolatilityStructure> ovCurve;
353 switch (interpolationType.which()) {
354 case 0:
355 if (isMoving) {
356 BOOST_TEST_MESSAGE("Using Linear interpolation with a moving reference date");
357 BOOST_REQUIRE_NO_THROW(ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<Linear> >(
358 settlementDays, helpers, calendar, bdc, dayCounter, curveVolatilityType,
359 curveDisplacement, flatFirstPeriod));
360 } else {
361 BOOST_TEST_MESSAGE("Using Linear interpolation with a fixed reference date");
362 BOOST_REQUIRE_NO_THROW(ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<Linear> >(
363 referenceDate, helpers, calendar, bdc, dayCounter, curveVolatilityType,
364 curveDisplacement, flatFirstPeriod));
365 }
366 break;
367 case 1:
368 if (isMoving) {
369 BOOST_TEST_MESSAGE("Using BackwardFlat interpolation with a moving reference date");
370 BOOST_REQUIRE_NO_THROW(ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<BackwardFlat> >(
371 settlementDays, helpers, calendar, bdc, dayCounter, curveVolatilityType,
372 curveDisplacement, flatFirstPeriod));
373 } else {
374 BOOST_TEST_MESSAGE("Using BackwardFlat interpolation with a fixed reference date");
375 BOOST_REQUIRE_NO_THROW(ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<BackwardFlat> >(
376 referenceDate, helpers, calendar, bdc, dayCounter, curveVolatilityType,
377 curveDisplacement, flatFirstPeriod));
378 }
379 break;
380 case 2:
381 if (isMoving) {
382 BOOST_TEST_MESSAGE("Using LinearFlat interpolation with a moving reference date");
383 BOOST_REQUIRE_NO_THROW(ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<LinearFlat> >(
384 settlementDays, helpers, calendar, bdc, dayCounter, curveVolatilityType,
385 curveDisplacement, flatFirstPeriod));
386 } else {
387 BOOST_TEST_MESSAGE("Using LinearFlat interpolation with a fixed reference date");
388 BOOST_REQUIRE_NO_THROW(ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<LinearFlat> >(
389 referenceDate, helpers, calendar, bdc, dayCounter, curveVolatilityType,
390 curveDisplacement, flatFirstPeriod));
391 }
392 break;
393 case 3:
394 if (isMoving) {
395 BOOST_TEST_MESSAGE("Using Cubic interpolation with a moving reference date");
396 BOOST_REQUIRE_NO_THROW(
397 ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<Cubic> >(
398 settlementDays, helpers, calendar, bdc, dayCounter, curveVolatilityType, curveDisplacement,
399 flatFirstPeriod, Cubic(),
402 globalAccuracy)));
403 } else {
404 BOOST_TEST_MESSAGE("Using Cubic interpolation with a fixed reference date");
405 BOOST_REQUIRE_NO_THROW(
406 ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<Cubic> >(
407 referenceDate, helpers, calendar, bdc, dayCounter, curveVolatilityType, curveDisplacement,
408 flatFirstPeriod, Cubic(),
411 globalAccuracy)));
412 }
413 break;
414 case 4:
415 if (isMoving) {
416 BOOST_TEST_MESSAGE("Using CubicFlat interpolation with a moving reference date");
417 BOOST_REQUIRE_NO_THROW(
418 ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<CubicFlat> >(
419 settlementDays, helpers, calendar, bdc, dayCounter, curveVolatilityType, curveDisplacement,
420 flatFirstPeriod, CubicFlat(),
423 accuracy, globalAccuracy)));
424 } else {
425 BOOST_TEST_MESSAGE("Using CubicFlat interpolation with a fixed reference date");
426 BOOST_REQUIRE_NO_THROW(
427 ovCurve = QuantLib::ext::make_shared<PiecewiseOptionletCurve<CubicFlat> >(
428 referenceDate, helpers, calendar, bdc, dayCounter, curveVolatilityType, curveDisplacement,
429 flatFirstPeriod, CubicFlat(),
432 accuracy, globalAccuracy)));
433 }
434 break;
435 default:
436 BOOST_FAIL("Unexpected interpolation type");
437 }
438 Handle<OptionletVolatilityStructure> hovs(ovCurve);
439
440 // Price each cap floor instrument using the piecewise optionlet curve and check it against the flat NPV
441 BOOST_TEST_MESSAGE("The stripped values and differences at each tenor are:");
442 Real strippedNpv;
443 for (Size i = 0; i < volatilityColumn.tenors.size(); i++) {
444
445 // May need to update instruments type if it is being chosen automatically in the bootstrap
446 if (helperType == CapFloorHelper::Automatic && quoteType != CapFloorHelper::Premium) {
447 Real volatility = volatilityColumn.volatilities[i];
448 CapFloor::Type capFloorType =
449 QuantLib::ext::dynamic_pointer_cast<CapFloorHelper>(helpers[i])->capFloor()->type();
450 if (capFloorType != instruments[i]->type()) {
451 // Need to update the instrument and the flat NPV for the test
452 instruments[i] =
453 MakeCapFloor(capFloorType, volatilityColumn.tenors[i], iborIndex, volatilityColumn.strike);
454 if (volatilityColumn.type == ShiftedLognormal) {
455 instruments[i]->setPricingEngine(QuantLib::ext::make_shared<BlackCapFloorEngine>(
456 testYieldCurves.discountEonia, volatility, dayCounter, volatilityColumn.displacement));
457 } else {
458 instruments[i]->setPricingEngine(QuantLib::ext::make_shared<BachelierCapFloorEngine>(
459 testYieldCurves.discountEonia, volatility, dayCounter));
460 }
461 flatNpvs[i] = instruments[i]->NPV();
462 }
463 }
464
465 // Price the instrument using the stripped optionlet structure
466 if (ovCurve->volatilityType() == ShiftedLognormal) {
467 instruments[i]->setPricingEngine(
468 QuantLib::ext::make_shared<BlackCapFloorEngine>(testYieldCurves.discountEonia, hovs));
469 } else {
470 instruments[i]->setPricingEngine(
471 QuantLib::ext::make_shared<BachelierCapFloorEngine>(testYieldCurves.discountEonia, hovs));
472 }
473 strippedNpv = instruments[i]->NPV();
474
475 BOOST_TEST_MESSAGE(" (Cap/Floor, Tenor, Volatility, Flat NPV, Stripped NPV, Flat - Stripped) = ("
476 << instruments[i]->type() << ", " << volatilityColumn.tenors[i] << ", " << fixed
477 << setprecision(13) << volatilityColumn.volatilities[i] << ", " << flatNpvs[i] << ", "
478 << strippedNpv << ", " << (flatNpvs[i] - strippedNpv) << ")");
479
480 BOOST_CHECK_SMALL(fabs(flatNpvs[i] - strippedNpv), tolerance);
481 }
482 }
483}
484
485BOOST_DATA_TEST_CASE_F(CommonVars, testCachedValues,
486 bdata::make(interpolationTypesCached) * bdata::make(flatFirstPeriodValues), interpolationType,
487 flatFirstPeriod) {
488
489 BOOST_TEST_MESSAGE("Testing stripping of single strike column against cached values");
490
493
494 // Use EUR cap floor test volatility data from file test/capfloormarketdata.hpp
495 // Take the highest strike column of the normal volatility matrix
496 CapFloorVolatilityEUR testVols;
497 vector<Period> tenors = testVols.tenors;
498 vector<Real> volatilities(tenors.size());
499 Size strikeIdx = testVols.strikes.size() - 1;
500 Rate strike = testVols.strikes[strikeIdx];
501 for (Size i = 0; i < tenors.size(); i++) {
502 volatilities[i] = testVols.nVols[i][strikeIdx];
503 }
504 VolatilityType volatilityType = Normal;
505 Real displacement = 0.0;
506
507 // Make first tenor 18M highlight differences introduced by flatFirstPeriod setting.
508 // With first tenor = 1Y and index tenor = 6M, we were not seeing a difference.
509 tenors[0] = 18 * Months;
510
511 BOOST_TEST_MESSAGE("Test inputs are:");
512 BOOST_TEST_MESSAGE(" Cap floor helper type: " << helperType);
513 BOOST_TEST_MESSAGE(" Cap floor strike: " << strike);
514 BOOST_TEST_MESSAGE(" Quote type: " << quoteType);
515 BOOST_TEST_MESSAGE(" Quote volatility type: " << volatilityType);
516 BOOST_TEST_MESSAGE(" Quote displacement: " << displacement);
517 BOOST_TEST_MESSAGE(" Interpolation type: " << to_string(interpolationType));
518 BOOST_TEST_MESSAGE(" Flat first period: " << boolalpha << flatFirstPeriod);
519
520 // Form the cap floor helper instrument for each tenor in the strike column
521 vector<QuantLib::ext::shared_ptr<helper> > helpers(tenors.size());
522 for (Size i = 0; i < tenors.size(); i++) {
523 Handle<Quote> quote(QuantLib::ext::make_shared<SimpleQuote>(volatilities[i]));
524 helpers[i] = QuantLib::ext::make_shared<CapFloorHelper>(helperType, tenors[i], strike, quote, iborIndex,
525 testYieldCurves.discountEonia, true, Date(), quoteType,
526 volatilityType, displacement);
527 }
528
529 // Create the piecewise optionlet curve, with the given interpolation type.
530 // Store the nodes of this optionlet curve to compare with cached values
531 VolatilityType curveVolatilityType = Normal;
532 Real curveDisplacement = 0.0;
533 QuantLib::ext::shared_ptr<OptionletVolatilityStructure> ovs;
534 vector<pair<Date, Real> > curveNodes;
535 switch (interpolationType.which()) {
536 case 0: {
537 BOOST_TEST_MESSAGE("Using Linear interpolation");
538 QuantLib::ext::shared_ptr<PiecewiseOptionletCurve<Linear> > ovCurve =
539 QuantLib::ext::make_shared<PiecewiseOptionletCurve<Linear> >(referenceDate, helpers, calendar, bdc, dayCounter,
540 curveVolatilityType, curveDisplacement,
541 flatFirstPeriod);
542 curveNodes = ovCurve->nodes();
543 ovs = ovCurve;
544 } break;
545 case 1: {
546 BOOST_TEST_MESSAGE("Using BackwardFlat interpolation");
547 QuantLib::ext::shared_ptr<PiecewiseOptionletCurve<BackwardFlat> > ovCurve =
548 QuantLib::ext::make_shared<PiecewiseOptionletCurve<BackwardFlat> >(referenceDate, helpers, calendar, bdc,
549 dayCounter, curveVolatilityType,
550 curveDisplacement, flatFirstPeriod);
551 curveNodes = ovCurve->nodes();
552 ovs = ovCurve;
553 } break;
554 case 2: {
555 BOOST_TEST_MESSAGE("Using LinearFlat interpolation");
556 QuantLib::ext::shared_ptr<PiecewiseOptionletCurve<LinearFlat> > ovCurve =
557 QuantLib::ext::make_shared<PiecewiseOptionletCurve<LinearFlat> >(referenceDate, helpers, calendar, bdc, dayCounter,
558 curveVolatilityType, curveDisplacement,
559 flatFirstPeriod);
560 curveNodes = ovCurve->nodes();
561 ovs = ovCurve;
562 } break;
563 default:
564 BOOST_FAIL("Unexpected interpolation type");
565 }
566
567 // Get the key for the cached results for the current test
568 Size key = 2 * interpolationType.which() + static_cast<Size>(flatFirstPeriod);
569
570 // Check stripped optionlet volatilities against cached values
571 BOOST_REQUIRE_EQUAL(curveNodes.size(), cachedDates.size());
572 BOOST_REQUIRE(cachedValues.count(key) == 1);
573 BOOST_REQUIRE_EQUAL(curveNodes.size(), cachedValues.at(key).size());
574 BOOST_TEST_MESSAGE("node_date,node_vol");
575 for (Size i = 0; i < curveNodes.size(); i++) {
576 // Check the date
577 BOOST_CHECK_EQUAL(curveNodes[i].first, cachedDates[i]);
578 // Check the value
579 BOOST_CHECK_SMALL(fabs(curveNodes[i].second - cachedValues.at(key)[i]), accuracy);
580 // Print out the curve
581 BOOST_TEST_MESSAGE(io::iso_date(curveNodes[i].first)
582 << "," << fixed << setprecision(12) << curveNodes[i].second);
583 }
584
585 // Check stripped optionlet volatilities on non-node dates against cached values
586 BOOST_REQUIRE(cachedNonNodeValues.count(key) == 1);
587 BOOST_TEST_MESSAGE("date,vol,cached_vol,diff");
588 for (Size i = 0; i < cachedNonNodeDates.size(); i++) {
589 Date d = cachedNonNodeDates[i];
590
591 // The last date has been picked past the maximum curve date to check extrapolation. Check that we get an
592 // error and then turn on extrapolation.
593 if (i == cachedNonNodeDates.size() - 1) {
594 BOOST_CHECK_THROW(ovs->volatility(d, strike), Error);
595 ovs->enableExtrapolation();
596 }
597
598 Volatility vol = ovs->volatility(d, strike);
599 Volatility cachedVol = cachedNonNodeValues.at(key)[i];
600 Volatility diff = fabs(vol - cachedVol);
601 // Check the value
602 BOOST_CHECK_SMALL(diff, accuracy);
603 // Print out the curve
604 BOOST_TEST_MESSAGE(io::iso_date(d)
605 << "," << fixed << setprecision(12) << vol << "," << cachedVol << "," << diff);
606
607 // The strike should not matter so check that the same test passes above and below the strike
608 Real shift = 0.0010;
609 diff = fabs(ovs->volatility(d, strike - shift) - cachedVol);
610 BOOST_CHECK_SMALL(diff, accuracy);
611 diff = fabs(ovs->volatility(d, strike + shift) - cachedVol);
612 BOOST_CHECK_SMALL(diff, accuracy);
613 }
614}
615
616BOOST_AUTO_TEST_SUITE_END()
617
618BOOST_AUTO_TEST_SUITE_END()
Helper for bootstrapping optionlet volatilities from cap floor volatilities.
structs containing capfloor market data that can be used in tests
QuoteType
Enum to indicate the type of the quote provided with the CapFloorHelper.
Cubic interpolation and flat extrapolation factory and traits.
Linear-interpolation and flat extrapolation factory and traits
flat interpolation decorator
std::ostream & operator<<(std::ostream &out, EquityReturnType t)
QuantLib::BootstrapHelper< QuantLib::OptionletVolatilityStructure > helper
BOOST_DATA_TEST_CASE_F(CommonVars, testPiecewiseOptionletStripping, bdata::make(generateVolatilityColumns()) *bdata::make(helperTypes) *bdata::make(quoteTypes) *bdata::make(interpolationTypes) *bdata::make(isMovingValues) *bdata::make(flatFirstPeriodValues), volatilityColumn, helperType, quoteType, interpolationType, isMoving, flatFirstPeriod)
One-dimensional curve of bootstrapped optionlet volatilities.
Fixture that can be used at top level.
structs containing yield curve market data that can be used in tests