1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3/*
4 Copyright (C) 2005, 2006 Klaus Spanderen
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 "libormarketmodel.hpp"
21#include "utilities.hpp"
22
23#include <ql/indexes/ibor/euribor.hpp>
24#include <ql/instruments/capfloor.hpp>
25#include <ql/termstructures/yield/zerocurve.hpp>
26#include <ql/termstructures/volatility/optionlet/capletvariancecurve.hpp>
27#include <ql/math/optimization/levenbergmarquardt.hpp>
28
29#include <ql/math/statistics/generalstatistics.hpp>
30#include <ql/math/randomnumbers/rngtraits.hpp>
31#include <ql/methods/montecarlo/multipathgenerator.hpp>
32
33#include <ql/pricingengines/swap/discountingswapengine.hpp>
34#include <ql/pricingengines/capfloor/blackcapfloorengine.hpp>
35#include <ql/pricingengines/capfloor/analyticcapfloorengine.hpp>
36
37#include <ql/models/shortrate/calibrationhelpers/caphelper.hpp>
38#include <ql/models/shortrate/calibrationhelpers/swaptionhelper.hpp>
39
40#include <ql/legacy/libormarketmodels/lfmcovarproxy.hpp>
41#include <ql/legacy/libormarketmodels/lmexpcorrmodel.hpp>
42#include <ql/legacy/libormarketmodels/lmlinexpcorrmodel.hpp>
43#include <ql/legacy/libormarketmodels/lmfixedvolmodel.hpp>
44#include <ql/legacy/libormarketmodels/lmextlinexpvolmodel.hpp>
45#include <ql/legacy/libormarketmodels/liborforwardmodel.hpp>
46#include <ql/legacy/libormarketmodels/lfmswaptionengine.hpp>
47#include <ql/legacy/libormarketmodels/lfmhullwhiteparam.hpp>
48
49#include <ql/time/daycounters/actual360.hpp>
50#include <ql/time/schedule.hpp>
51#include <ql/quotes/simplequote.hpp>
52
53using namespace QuantLib;
54using namespace boost::unit_test_framework;
55
56namespace libor_market_model_test {
57
58 ext::shared_ptr<IborIndex> makeIndex(std::vector<Date> dates, const std::vector<Rate>& rates) {
59 DayCounter dayCounter = Actual360();
60
61 RelinkableHandle<YieldTermStructure> termStructure;
62
63 ext::shared_ptr<IborIndex> index(new Euribor6M(termStructure));
64
65 Date todaysDate =
66 index->fixingCalendar().adjust(Date(4,September,2005));
67 Settings::instance().evaluationDate() = todaysDate;
68
69 dates[0] = index->fixingCalendar().advance(todaysDate,
70 n: index->fixingDays(), unit: Days);
71
72 termStructure.linkTo(h: ext::shared_ptr<YieldTermStructure>(
73 new ZeroCurve(dates, rates, dayCounter)));
74
75 return index;
76 }
77
78
79 ext::shared_ptr<IborIndex> makeIndex() {
80 std::vector<Date> dates = {{4,September,2005}, {4,September,2018}};
81 std::vector<Rate> rates = {0.039, 0.041};
82
83 return makeIndex(dates, rates);
84 }
85
86
87 ext::shared_ptr<OptionletVolatilityStructure>
88 makeCapVolCurve(const Date& todaysDate) {
89 Volatility vols[] = {14.40, 17.15, 16.81, 16.64, 16.17,
90 15.78, 15.40, 15.21, 14.86};
91
92 std::vector<Date> dates;
93 std::vector<Volatility> capletVols;
94 ext::shared_ptr<LiborForwardModelProcess> process(
95 new LiborForwardModelProcess(10, makeIndex()));
96
97 for (Size i=0; i < 9; ++i) {
98 capletVols.push_back(x: vols[i]/100);
99 dates.push_back(x: process->fixingDates()[i+1]);
100 }
101
102 return ext::make_shared<CapletVarianceCurve>(
103 args: todaysDate, args&: dates,
104 args&: capletVols, args: Actual360());
105 }
106
107}
108
109
110void LiborMarketModelTest::testSimpleCovarianceModels() {
111 BOOST_TEST_MESSAGE("Testing simple covariance models...");
112
113 using namespace libor_market_model_test;
114
115 const Size size = 10;
116 const Real tolerance = 1e-14;
117 Size i;
118
119 ext::shared_ptr<LmCorrelationModel> corrModel(
120 new LmExponentialCorrelationModel(size, 0.1));
121
122 Matrix recon = corrModel->correlation(t: 0.0)
123 - corrModel->pseudoSqrt(t: 0.0)*transpose(m: corrModel->pseudoSqrt(t: 0.0));
124
125 for (i=0; i<size; ++i) {
126 for (Size j=0; j<size; ++j) {
127 if (std::fabs(x: recon[i][j]) > tolerance)
128 BOOST_ERROR("Failed to reproduce correlation matrix"
129 << "\n calculated: " << recon[i][j]
130 << "\n expected: " << 0);
131 }
132 }
133
134 std::vector<Time> fixingTimes(size);
135 for (i=0; i<size; ++i) {
136 fixingTimes[i] = 0.5*i;
137 }
138
139 const Real a=0.2;
140 const Real b=0.1;
141 const Real c=2.1;
142 const Real d=0.3;
143
144 ext::shared_ptr<LmVolatilityModel> volaModel(
145 new LmLinearExponentialVolatilityModel(fixingTimes, a, b, c, d));
146
147 ext::shared_ptr<LfmCovarianceProxy> covarProxy(
148 new LfmCovarianceProxy(volaModel, corrModel));
149
150 ext::shared_ptr<LiborForwardModelProcess> process(
151 new LiborForwardModelProcess(size, makeIndex()));
152
153 ext::shared_ptr<LiborForwardModel> liborModel(
154 new LiborForwardModel(process, volaModel, corrModel));
155
156 for (Real t=0; t<4.6; t+=0.31) {
157 recon = covarProxy->covariance(t)
158 - covarProxy->diffusion(t)*transpose(m: covarProxy->diffusion(t));
159
160 for (Size i=0; i<size; ++i) {
161 for (Size j=0; j<size; ++j) {
162 if (std::fabs(x: recon[i][j]) > tolerance)
163 BOOST_ERROR("Failed to reproduce correlation matrix"
164 << "\n calculated: " << recon[i][j]
165 << "\n expected: " << 0);
166 }
167 }
168
169 Array volatility = volaModel->volatility(t);
170
171 for (Size k=0; k<size; ++k) {
172 Real expected = 0;
173 if (static_cast<Real>(k) > 2 * t) {
174 const Real T = fixingTimes[k];
175 expected=(a*(T-t)+d)*std::exp(x: -b*(T-t)) + c;
176 }
177
178 if (std::fabs(x: expected - volatility[k]) > tolerance)
179 BOOST_ERROR("Failed to reproduce volatities"
180 << "\n calculated: " << volatility[k]
181 << "\n expected: " << expected);
182 }
183 }
184}
185
186
187void LiborMarketModelTest::testCapletPricing() {
188 BOOST_TEST_MESSAGE("Testing caplet pricing...");
189
190 using namespace libor_market_model_test;
191
192 bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons();
193
194 const Size size = 10;
195 Real tolerance = usingAtParCoupons ? 1e-12 : 1e-5;
196
197 ext::shared_ptr<IborIndex> index = makeIndex();
198 ext::shared_ptr<LiborForwardModelProcess> process(
199 new LiborForwardModelProcess(size, index));
200
201 // set-up pricing engine
202 const ext::shared_ptr<OptionletVolatilityStructure> capVolCurve =
203 makeCapVolCurve(todaysDate: Settings::instance().evaluationDate());
204
205 Array variances = LfmHullWhiteParameterization(process, capVolCurve)
206 .covariance(t: 0.0).diagonal();
207
208 ext::shared_ptr<LmVolatilityModel> volaModel(
209 new LmFixedVolatilityModel(Sqrt(v: variances),
210 process->fixingTimes()));
211
212 ext::shared_ptr<LmCorrelationModel> corrModel(
213 new LmExponentialCorrelationModel(size, 0.3));
214
215 ext::shared_ptr<AffineModel> model(
216 new LiborForwardModel(process, volaModel, corrModel));
217
218 const Handle<YieldTermStructure> termStructure =
219 process->index()->forwardingTermStructure();
220
221 ext::shared_ptr<AnalyticCapFloorEngine> engine1(
222 new AnalyticCapFloorEngine(model, termStructure));
223
224 auto cap1 = Cap(process->cashFlows(),
225 std::vector<Rate>(size, 0.04));
226 cap1.setPricingEngine(engine1);
227
228 const Real expected = 0.015853935178;
229 const Real calculated = cap1.NPV();
230
231 if (std::fabs(x: expected - calculated) > tolerance)
232 BOOST_ERROR("Failed to reproduce npv"
233 << "\n calculated: " << calculated
234 << "\n expected: " << expected);
235}
236
237void LiborMarketModelTest::testCalibration() {
238 BOOST_TEST_MESSAGE("Testing calibration of a Libor forward model...");
239
240 using namespace libor_market_model_test;
241
242 const Size size = 14;
243 const Real tolerance = 8e-3;
244
245 Volatility capVols[] = {0.145708,0.158465,0.166248,0.168672,
246 0.169007,0.167956,0.166261,0.164239,
247 0.162082,0.159923,0.157781,0.155745,
248 0.153776,0.151950,0.150189,0.148582,
249 0.147034,0.145598,0.144248};
250
251 Volatility swaptionVols[] = {0.170595, 0.166844, 0.158306, 0.147444,
252 0.136930, 0.126833, 0.118135, 0.175963,
253 0.166359, 0.155203, 0.143712, 0.132769,
254 0.122947, 0.114310, 0.174455, 0.162265,
255 0.150539, 0.138734, 0.128215, 0.118470,
256 0.110540, 0.169780, 0.156860, 0.144821,
257 0.133537, 0.123167, 0.114363, 0.106500,
258 0.164521, 0.151223, 0.139670, 0.128632,
259 0.119123, 0.110330, 0.103114, 0.158956,
260 0.146036, 0.134555, 0.124393, 0.115038,
261 0.106996, 0.100064};
262
263 ext::shared_ptr<IborIndex> index = makeIndex();
264 ext::shared_ptr<LiborForwardModelProcess> process(
265 new LiborForwardModelProcess(size, index));
266 Handle<YieldTermStructure> termStructure = index->forwardingTermStructure();
267
268 // set-up the model
269 ext::shared_ptr<LmVolatilityModel> volaModel(
270 new LmExtLinearExponentialVolModel(process->fixingTimes(),
271 0.5,0.6,0.1,0.1));
272
273 ext::shared_ptr<LmCorrelationModel> corrModel(
274 new LmLinearExponentialCorrelationModel(size, 0.5, 0.8));
275
276 ext::shared_ptr<LiborForwardModel> model(
277 new LiborForwardModel(process, volaModel, corrModel));
278
279 Size swapVolIndex = 0;
280 DayCounter dayCounter=index->forwardingTermStructure()->dayCounter();
281
282 // set-up calibration helper
283 std::vector<ext::shared_ptr<CalibrationHelper> > calibrationHelpers;
284
285 Size i;
286 for (i=2; i < size; ++i) {
287 const Period maturity = i*index->tenor();
288 Handle<Quote> capVol(
289 ext::shared_ptr<Quote>(new SimpleQuote(capVols[i-2])));
290
291 auto caphelper =
292 ext::make_shared<CapHelper>(args: maturity, args&: capVol, args&: index, args: Annual,
293 args: index->dayCounter(), args: true, args&: termStructure,
294 args: BlackCalibrationHelper::ImpliedVolError);
295
296 caphelper->setPricingEngine(ext::shared_ptr<PricingEngine>(
297 new AnalyticCapFloorEngine(model, termStructure)));
298
299 calibrationHelpers.push_back(x: caphelper);
300
301 if (i<= size/2) {
302 // add a few swaptions to test swaption calibration as well
303 for (Size j=1; j <= size/2; ++j) {
304 const Period len = j*index->tenor();
305 Handle<Quote> swaptionVol(
306 ext::shared_ptr<Quote>(
307 new SimpleQuote(swaptionVols[swapVolIndex++])));
308
309 auto swaptionHelper =
310 ext::make_shared<SwaptionHelper>(args: maturity, args: len, args&: swaptionVol, args&: index,
311 args: index->tenor(), args&: dayCounter,
312 args: index->dayCounter(),
313 args&: termStructure,
314 args: BlackCalibrationHelper::ImpliedVolError);
315
316 swaptionHelper->setPricingEngine(
317 ext::shared_ptr<PricingEngine>(
318 new LfmSwaptionEngine(model,termStructure)));
319
320 calibrationHelpers.push_back(x: swaptionHelper);
321 }
322 }
323 }
324
325 LevenbergMarquardt om(1e-6, 1e-6, 1e-6);
326 model->calibrate(calibrationHelpers, method&: om, endCriteria: EndCriteria(2000, 100, 1e-6, 1e-6, 1e-6));
327
328 // measure the calibration error
329 Real calculated = 0.0;
330 for (i=0; i<calibrationHelpers.size(); ++i) {
331 Real diff = calibrationHelpers[i]->calibrationError();
332 calculated += diff*diff;
333 }
334
335 if (std::sqrt(x: calculated) > tolerance)
336 BOOST_ERROR("Failed to calibrate libor forward model"
337 << "\n calculated diff: " << std::sqrt(calculated)
338 << "\n expected : smaller than " << tolerance);
339}
340
341void LiborMarketModelTest::testSwaptionPricing() {
342 BOOST_TEST_MESSAGE("Testing forward swap and swaption pricing...");
343
344 using namespace libor_market_model_test;
345
346 bool usingAtParCoupons = IborCoupon::Settings::instance().usingAtParCoupons();
347
348 const Size size = 10;
349 const Size steps = 8*size;
350
351 Real tolerance = usingAtParCoupons ? 1e-12 : 1e-6;
352
353 std::vector<Date> dates = {{4,September,2005}, {4,September,2011}};
354 std::vector<Rate> rates = {0.04, 0.08};
355
356 ext::shared_ptr<IborIndex> index = makeIndex(dates, rates);
357
358 ext::shared_ptr<LiborForwardModelProcess> process(
359 new LiborForwardModelProcess(size, index));
360
361 ext::shared_ptr<LmCorrelationModel> corrModel(
362 new LmExponentialCorrelationModel(size, 0.5));
363
364 ext::shared_ptr<LmVolatilityModel> volaModel(
365 new LmLinearExponentialVolatilityModel(process->fixingTimes(),
366 0.291, 1.483, 0.116, 0.00001));
367
368 // set-up pricing engine
369 process->setCovarParam(ext::shared_ptr<LfmCovarianceParameterization>(
370 new LfmCovarianceProxy(volaModel, corrModel)));
371
372 // set-up a small Monte-Carlo simulation to price swations
373 typedef PseudoRandom::rsg_type rsg_type;
374 typedef MultiPathGenerator<rsg_type>::sample_type sample_type;
375
376 std::vector<Time> tmp = process->fixingTimes();
377 TimeGrid grid(tmp.begin(), tmp.end(), steps);
378
379 Size i;
380 std::vector<Size> location;
381 for (i=0; i < tmp.size(); ++i) {
382 location.push_back(
383 x: std::find(first: grid.begin(),last: grid.end(),val: tmp[i])-grid.begin());
384 }
385
386 rsg_type rsg = PseudoRandom::make_sequence_generator(
387 dimension: process->factors()*(grid.size()-1),
388 seed: BigNatural(42));
389
390 const Size nrTrails = 5000;
391 MultiPathGenerator<rsg_type> generator(process, grid, rsg, false);
392
393 ext::shared_ptr<LiborForwardModel>
394 liborModel(new LiborForwardModel(process, volaModel, corrModel));
395
396 Calendar calendar = index->fixingCalendar();
397 DayCounter dayCounter = index->forwardingTermStructure()->dayCounter();
398 BusinessDayConvention convention = index->businessDayConvention();
399
400 Date settlement = index->forwardingTermStructure()->referenceDate();
401
402 for (i=1; i < size; ++i) {
403 for (Size j=1; j <= size-i; ++j) {
404 Date fwdStart = settlement + Period(6*i, Months);
405 Date fwdMaturity = fwdStart + Period(6*j, Months);
406
407 Schedule schedule(fwdStart, fwdMaturity, index->tenor(), calendar,
408 convention, convention, DateGeneration::Forward, false);
409
410 Rate swapRate = 0.0404;
411 ext::shared_ptr<VanillaSwap> forwardSwap(
412 new VanillaSwap(Swap::Receiver, 1.0,
413 schedule, swapRate, dayCounter,
414 schedule, index, 0.0, index->dayCounter()));
415 forwardSwap->setPricingEngine(ext::shared_ptr<PricingEngine>(
416 new DiscountingSwapEngine(index->forwardingTermStructure())));
417
418 // check forward pricing first
419 const Real expected = forwardSwap->fairRate();
420 const Real calculated = liborModel->S_0(alpha: i-1,beta: i+j-1);
421
422 if (std::fabs(x: expected - calculated) > tolerance)
423 BOOST_ERROR("Failed to reproduce fair forward swap rate"
424 << "\n calculated: " << calculated
425 << "\n expected: " << expected);
426
427 swapRate = forwardSwap->fairRate();
428 forwardSwap = ext::make_shared<VanillaSwap>(
429 args: Swap::Receiver, args: 1.0,
430 args&: schedule, args&: swapRate, args&: dayCounter,
431 args&: schedule, args&: index, args: 0.0, args: index->dayCounter());
432 forwardSwap->setPricingEngine(ext::shared_ptr<PricingEngine>(
433 new DiscountingSwapEngine(index->forwardingTermStructure())));
434
435 if (i == j && i<=size/2) {
436 ext::shared_ptr<PricingEngine> engine(
437 new LfmSwaptionEngine(liborModel,
438 index->forwardingTermStructure()));
439 ext::shared_ptr<Exercise> exercise(
440 new EuropeanExercise(process->fixingDates()[i]));
441
442 auto swaption = ext::make_shared<Swaption>(args&: forwardSwap, args&: exercise);
443 swaption->setPricingEngine(engine);
444
445 GeneralStatistics stat;
446
447 for (Size n=0; n<nrTrails; ++n) {
448 sample_type path = (n % 2) != 0U ? generator.antithetic() : generator.next();
449
450 std::vector<Rate> rates(size);
451 for (Size k=0; k<process->size(); ++k) {
452 rates[k] = path.value[k][location[i]];
453 }
454 std::vector<DiscountFactor> dis =
455 process->discountBond(rates);
456
457 Real npv=0.0;
458 for (Size m=i; m < i+j; ++m) {
459 npv += (swapRate - rates[m])
460 * ( process->accrualEndTimes()[m]
461 - process->accrualStartTimes()[m])*dis[m];
462 }
463 stat.add(value: std::max(a: npv, b: 0.0));
464 }
465
466 if (std::fabs(x: swaption->NPV() - stat.mean())
467 > stat.errorEstimate()*2.35)
468 BOOST_ERROR("Failed to reproduce swaption npv"
469 << "\n calculated: " << stat.mean()
470 << "\n expected: " << swaption->NPV());
471 }
472 }
473 }
474}
475
476
477test_suite* LiborMarketModelTest::suite(SpeedLevel speed) {
478 auto* suite = BOOST_TEST_SUITE("Libor market model tests");
479
480 suite->add(QUANTLIB_TEST_CASE(
481 &LiborMarketModelTest::testSimpleCovarianceModels));
482 suite->add(QUANTLIB_TEST_CASE(&LiborMarketModelTest::testCapletPricing));
483 suite->add(QUANTLIB_TEST_CASE(&LiborMarketModelTest::testSwaptionPricing));
484
485 if (speed == Slow) {
486 suite->add(QUANTLIB_TEST_CASE(&LiborMarketModelTest::testCalibration));
487 }
488
489 return suite;
490}
491
492

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