Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
blackswaptionenginedeltagamma.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2016 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 "toplevelfixture.hpp"
20#include <boost/test/unit_test.hpp>
21
23
24#include <ql/indexes/ibor/euribor.hpp>
25#include <ql/instruments/makevanillaswap.hpp>
26#include <ql/pricingengines/swaption/blackswaptionengine.hpp>
27#include <ql/quotes/simplequote.hpp>
28#include <ql/termstructures/yield/flatforward.hpp>
29#include <ql/termstructures/yield/piecewisezerospreadedtermstructure.hpp>
30
31#include <boost/make_shared.hpp>
32#include <boost/timer/timer.hpp>
33
34using namespace QuantLib;
35using namespace QuantExt;
36
37using namespace boost::unit_test_framework;
38using std::vector;
39
40namespace {
41struct TestData {
42 TestData() : refDate(Date(22, Aug, 2016)) {
43 Settings::instance().evaluationDate() = refDate;
44 baseDiscount = Handle<YieldTermStructure>(QuantLib::ext::make_shared<FlatForward>(
45 refDate, Handle<Quote>(QuantLib::ext::make_shared<SimpleQuote>(0.02)), Actual365Fixed()));
46 baseForward = Handle<YieldTermStructure>(QuantLib::ext::make_shared<FlatForward>(
47 refDate, Handle<Quote>(QuantLib::ext::make_shared<SimpleQuote>(0.03)), Actual365Fixed()));
48 pillarDates.push_back(refDate + 1 * Years);
49 pillarDates.push_back(refDate + 2 * Years);
50 pillarDates.push_back(refDate + 3 * Years);
51 pillarDates.push_back(refDate + 4 * Years);
52 pillarDates.push_back(refDate + 5 * Years);
53 pillarDates.push_back(refDate + 7 * Years);
54 pillarDates.push_back(refDate + 10 * Years);
55 pillarDates.push_back(refDate + 15 * Years);
56 pillarDates.push_back(refDate + 20 * Years);
57 std::vector<Handle<Quote>> tmpDiscSpr, tmpFwdSpr;
58 for (Size i = 0; i < pillarDates.size(); ++i) {
59 QuantLib::ext::shared_ptr<SimpleQuote> qd = QuantLib::ext::make_shared<SimpleQuote>(0.0);
60 QuantLib::ext::shared_ptr<SimpleQuote> qf = QuantLib::ext::make_shared<SimpleQuote>(0.0);
61 discountSpreads.push_back(qd);
62 forwardSpreads.push_back(qf);
63 tmpDiscSpr.push_back(Handle<Quote>(qd));
64 tmpFwdSpr.push_back(Handle<Quote>(qf));
65 pillarTimes.push_back(baseDiscount->timeFromReference(pillarDates[i]));
66 }
67 discountCurve =
68 Handle<YieldTermStructure>(QuantLib::ext::make_shared<InterpolatedPiecewiseZeroSpreadedTermStructure<Linear>>(
69 baseDiscount, tmpDiscSpr, pillarDates));
70 forwardCurve =
71 Handle<YieldTermStructure>(QuantLib::ext::make_shared<InterpolatedPiecewiseZeroSpreadedTermStructure<Linear>>(
72 baseForward, tmpFwdSpr, pillarDates));
73 discountCurve->enableExtrapolation();
74 forwardCurve->enableExtrapolation();
75 forwardIndex = QuantLib::ext::make_shared<Euribor>(6 * Months, forwardCurve);
76 lnVol = QuantLib::ext::make_shared<SimpleQuote>(0.20);
77 slnVol = QuantLib::ext::make_shared<SimpleQuote>(0.10);
78 nVol = QuantLib::ext::make_shared<SimpleQuote>(0.0075);
79 slnShift = 0.03;
80 }
81 Date refDate;
82 Handle<YieldTermStructure> baseDiscount, baseForward, discountCurve, forwardCurve;
83 QuantLib::ext::shared_ptr<IborIndex> forwardIndex;
84 std::vector<Date> pillarDates;
85 std::vector<QuantLib::ext::shared_ptr<SimpleQuote>> discountSpreads, forwardSpreads;
86 std::vector<Real> pillarTimes;
87 QuantLib::ext::shared_ptr<SimpleQuote> lnVol, slnVol, nVol;
88 Real slnShift;
89}; // TestData
90
91bool check(const Real reference, const Real value) {
92 if (std::fabs(reference) >= 1E-4) {
93 return std::fabs((reference - value) / reference) < 5E-2;
94 } else {
95 return std::fabs(reference - value) < 5E-5;
96 }
97}
98
99class Timer {
100 boost::timer::cpu_timer timer_;
101 double elapsed_;
102
103public:
104 void start() { timer_ = boost::timer::cpu_timer(); }
105 void stop() { elapsed_ = timer_.elapsed().wall * 1e-9; }
106 double elapsed() const { return elapsed_; }
107};
108
109void performTest(const TestData& d, const QuantLib::ext::shared_ptr<PricingEngine>& engine0,
110 const QuantLib::ext::shared_ptr<PricingEngine>& engine, const bool receiveFixed, const Real spread,
111 const std::string& config) {
112
113 BOOST_TEST_MESSAGE(
114 "Testing npv, atm and vega calculation in BlackSwaptionEngineDeltaGamma against QuantLib engine (" << config
115 << ")...");
116
117 Size n = d.pillarTimes.size();
118
119 QuantLib::ext::shared_ptr<VanillaSwap> swap = MakeVanillaSwap(11 * Years, d.forwardIndex, 0.04, 8 * Years)
120 .receiveFixed(receiveFixed)
121 .withNominal(10.0)
122 .withFloatingLegSpread(spread);
123 Date exerciseDate = d.refDate + 8 * Years;
124 QuantLib::ext::shared_ptr<Exercise> exercise = QuantLib::ext::make_shared<EuropeanExercise>(exerciseDate);
125 QuantLib::ext::shared_ptr<Swaption> swaption = QuantLib::ext::make_shared<Swaption>(swap, exercise);
126
127 swaption->setPricingEngine(engine0);
128 Real atm0 = swaption->result<Real>("atmForward");
129 Real vega0 = swaption->result<Real>("vega");
130 Real npv0 = swaption->NPV();
131
132 swaption->setPricingEngine(engine);
133 Real atm = swaption->result<Real>("atmForward");
134 Matrix vega = swaption->result<Matrix>("vega");
135 Real npv = swaption->NPV();
136
137 // check vega matrix dimension
138 if (vega.rows() != n || vega.columns() != n)
139 BOOST_ERROR("vegaLn dimensions (" << vega.rows() << "x" << vega.columns() << ") are wrong, should be " << n
140 << "x" << n);
141 Real sumVega = 0.0;
142 for (Size i = 0; i < n; ++i) {
143 for (Size j = 0; j < n; ++j) {
144 sumVega += vega[i][j];
145 }
146 }
147
148 // check atm, npv, vega (sum, bucketing was inspected manually, it reuses the logic from gamma rebucketing)
149 Real tol = 1E-8;
150 if (std::fabs(atm0 - atm) > tol)
151 BOOST_ERROR("atm (" << atm << ") can not be verified, expected " << atm0);
152 if (std::fabs(npv0 - npv) > tol)
153 BOOST_ERROR("npv (" << npv << ") can not be verified, expected " << npv0);
154 if (std::fabs(vega0 - sumVega) > tol)
155 BOOST_ERROR("vega (" << sumVega << ") can not be verified, expected " << vega0);
156
157 BOOST_TEST_MESSAGE("Testing delta calculation in BlackSwaptionEngineDeltaGamma against bump and revalue results ("
158 << config << ")...");
159
160 std::vector<Real> resultDeltaDsc = swaption->result<std::vector<Real>>("deltaDiscount");
161 std::vector<Real> resultDeltaFwd = swaption->result<std::vector<Real>>("deltaForward");
162 Matrix resultGamma = swaption->result<Matrix>("gamma");
163
164 // bump and revalue checks
165
166 // use QL engine to compute reference bump and revalue results
167 swaption->setPricingEngine(engine0);
168
169 // check results for correct dimensions
170 if (resultDeltaDsc.size() != n)
171 BOOST_ERROR("deltaDiscount result vector has a wrong dimension (" << resultDeltaDsc.size() << "), expected "
172 << n);
173 if (resultDeltaFwd.size() != n)
174 BOOST_ERROR("deltaForward result vector has a wrong dimension (" << resultDeltaFwd.size() << "), expected "
175 << n);
176
177 if (resultGamma.rows() != 2 * n || resultGamma.columns() != 2 * n)
178 BOOST_ERROR("gamma result vector has wrong dimensions (" << resultGamma.rows() << "x" << resultGamma.columns()
179 << "), expected " << n << "x" << n);
180
181 // delta (npv)
182
183 const Real bump = 1E-7;
184
185 for (Size i = 0; i < n; ++i) {
186 d.discountSpreads[i]->setValue(bump);
187 Real deltaDsc = (swaption->NPV() - npv0) / bump;
188 d.discountSpreads[i]->setValue(0.0);
189 d.forwardSpreads[i]->setValue(bump);
190 Real deltaFwd = (swaption->NPV() - npv0) / bump;
191 d.forwardSpreads[i]->setValue(0.0);
192 if (!check(deltaDsc, resultDeltaDsc[i]))
193 BOOST_ERROR("delta on pillar " << d.pillarTimes[i]
194 << " (discount curve) could not be verified, analytical: "
195 << resultDeltaDsc[i] << ", bump and revalue: " << deltaDsc);
196 if (!check(deltaFwd, resultDeltaFwd[i]))
197 BOOST_ERROR("delta on pillar " << d.pillarTimes[i] << " (forward curve) could not be verified, analytical: "
198 << resultDeltaFwd[i] << ", bump and revalue: " << deltaFwd);
199 }
200
201 // gamma (npv)
202
203 BOOST_TEST_MESSAGE("Testing gamma calculation in BlackSwaptionEngineDeltaGamma against bump and revalue results ("
204 << config << ")...");
205
206 const Real bump2 = 1E-5;
207 Matrix bumpGamma(n * 2, n * 2, 0.0);
208
209 // dsc-dsc
210 for (Size i = 0; i < n; ++i) {
211 // j < i
212 for (Size j = 0; j < i; ++j) {
213 d.discountSpreads[i]->setValue(bump2);
214 d.discountSpreads[j]->setValue(bump2);
215 Real npvpp = swaption->NPV();
216 d.discountSpreads[j]->setValue(0.0);
217 Real npvp0 = swaption->NPV();
218 d.discountSpreads[i]->setValue(0.0);
219 d.discountSpreads[j]->setValue(bump2);
220 Real npv0p = swaption->NPV();
221 d.discountSpreads[j]->setValue(0.0);
222 Real gamma = (npvpp - npvp0 - npv0p + npv0) / (bump2 * bump2);
223 bumpGamma[i][j] = bumpGamma[j][i] = gamma;
224 }
225 // j == i
226 d.discountSpreads[i]->setValue(2.0 * bump2);
227 Real npvpp = swaption->NPV();
228 d.discountSpreads[i]->setValue(bump2);
229 Real npvp = swaption->NPV();
230 d.discountSpreads[i]->setValue(0.0);
231 Real gamma = (npvpp - 2.0 * npvp + npv0) / (bump2 * bump2);
232 bumpGamma[i][i] = gamma;
233 }
234
235 // dsc-fwd
236 for (Size i = 0; i < n; ++i) {
237 // j <= i
238 for (Size j = 0; j < n; ++j) {
239 d.discountSpreads[i]->setValue(bump2);
240 d.forwardSpreads[j]->setValue(bump2);
241 Real npvpp = swaption->NPV();
242 d.forwardSpreads[j]->setValue(0.0);
243 Real npvp0 = swaption->NPV();
244 d.discountSpreads[i]->setValue(0.0);
245 d.forwardSpreads[j]->setValue(bump2);
246 Real npv0p = swaption->NPV();
247 d.forwardSpreads[j]->setValue(0.0);
248 Real gamma = (npvpp - npvp0 - npv0p + npv0) / (bump2 * bump2);
249 bumpGamma[i][n + j] = bumpGamma[n + j][i] = gamma;
250 }
251 }
252
253 // fwd-fwd
254 for (Size i = 0; i < n; ++i) {
255 // j < i
256 for (Size j = 0; j < i; ++j) {
257 d.forwardSpreads[i]->setValue(bump2);
258 d.forwardSpreads[j]->setValue(bump2);
259 Real npvpp = swaption->NPV();
260 d.forwardSpreads[j]->setValue(0.0);
261 Real npvp0 = swaption->NPV();
262 d.forwardSpreads[i]->setValue(0.0);
263 d.forwardSpreads[j]->setValue(bump2);
264 Real npv0p = swaption->NPV();
265 d.forwardSpreads[j]->setValue(0.0);
266 Real gamma = (npvpp - npvp0 - npv0p + npv0) / (bump2 * bump2);
267 bumpGamma[n + i][n + j] = bumpGamma[n + j][n + i] = gamma;
268 }
269 // j == i
270 d.forwardSpreads[i]->setValue(2.0 * bump2);
271 Real npvpp = swaption->NPV();
272 d.forwardSpreads[i]->setValue(bump2);
273 Real npvp = swaption->NPV();
274 d.forwardSpreads[i]->setValue(0.0);
275 Real gamma = (npvpp - 2.0 * npvp + npv0) / (bump2 * bump2);
276 bumpGamma[n + i][n + i] = gamma;
277 }
278
279 for (Size i = 0; i < 2 * n; ++i) {
280 for (Size j = 0; j < 2 * n; ++j) {
281 if (!check(bumpGamma[i][j], resultGamma[i][j]))
282 BOOST_ERROR("gamma entry (" << i << "," << j << ") is " << resultGamma[i][j]
283 << ", bump and revalue result is " << bumpGamma[i][j]);
284 }
285 }
286
287 // totals (parallel shift over all curves)
288 // this tests, if we have identified all non-zero first and second order partial derivatives
289
290 for (Size i = 0; i < n; ++i) {
291 d.discountSpreads[i]->setValue(bump);
292 d.forwardSpreads[i]->setValue(bump);
293 }
294
295 Real totalDeltaBump = (swaption->NPV() - npv0) / bump;
296
297 for (Size i = 0; i < n; ++i) {
298 d.discountSpreads[i]->setValue(2.0 * bump2);
299 d.forwardSpreads[i]->setValue(2.0 * bump2);
300 }
301 Real npvPP = swaption->NPV();
302 for (Size i = 0; i < n; ++i) {
303 d.discountSpreads[i]->setValue(bump2);
304 d.forwardSpreads[i]->setValue(bump2);
305 }
306 Real npvP = swaption->NPV();
307 for (Size i = 0; i < n; ++i) {
308 d.discountSpreads[i]->setValue(0.0);
309 d.forwardSpreads[i]->setValue(0.0);
310 }
311
312 Real totalGammaBump = (npvPP - 2.0 * npvP + npv0) / (bump2 * bump2);
313
314 Real totalDelta = 0.0, totalGamma = 0.0;
315 for (Size i = 0; i < n; ++i) {
316 totalDelta += resultDeltaDsc[i];
317 totalDelta += resultDeltaFwd[i];
318 }
319
320 for (Size i = 0; i < 2 * n; ++i) {
321 for (Size j = 0; j < 2 * n; ++j) {
322 totalGamma += resultGamma[i][j];
323 }
324 }
325
326 if (!check(totalDeltaBump, totalDelta))
327 BOOST_ERROR("total delta (" << totalDelta << ") can not be verified against bump and revalue result ("
328 << totalDeltaBump << ")");
329
330 if (!check(totalGammaBump, totalGamma))
331 BOOST_ERROR("total gamma (" << totalGamma << ") can not be verified against bump and revalue result ("
332 << totalGammaBump << ")");
333
334} // performTest
335
336} // anonymous namespace
337
338BOOST_FIXTURE_TEST_SUITE(QuantExtTestSuite, qle::test::TopLevelFixture)
339
340BOOST_AUTO_TEST_SUITE(BlackSwaptionEngineDeltaGammaTest)
341
342BOOST_AUTO_TEST_CASE(testNpvDeltasGammaVegas) {
343
344 TestData d;
345
346 QuantLib::ext::shared_ptr<PricingEngine> engineLn0 =
347 QuantLib::ext::make_shared<BlackSwaptionEngine>(d.discountCurve, Handle<Quote>(d.lnVol));
348 QuantLib::ext::shared_ptr<PricingEngine> engineSln0 =
349 QuantLib::ext::make_shared<BlackSwaptionEngine>(d.discountCurve, Handle<Quote>(d.slnVol), Actual365Fixed(), d.slnShift);
350 QuantLib::ext::shared_ptr<PricingEngine> engineN0 =
351 QuantLib::ext::make_shared<BachelierSwaptionEngine>(d.discountCurve, Handle<Quote>(d.nVol));
352
353 QuantLib::ext::shared_ptr<PricingEngine> engineLn =
354 QuantLib::ext::make_shared<BlackSwaptionEngineDeltaGamma>(d.discountCurve, Handle<Quote>(d.lnVol), Actual365Fixed(),
355 0.0, d.pillarTimes, d.pillarTimes, d.pillarTimes, true, true);
356 QuantLib::ext::shared_ptr<PricingEngine> engineSln = QuantLib::ext::make_shared<BlackSwaptionEngineDeltaGamma>(
357 d.discountCurve, Handle<Quote>(d.slnVol), Actual365Fixed(), d.slnShift, d.pillarTimes, d.pillarTimes,
358 d.pillarTimes, true, true);
359 QuantLib::ext::shared_ptr<PricingEngine> engineN =
360 QuantLib::ext::make_shared<BachelierSwaptionEngineDeltaGamma>(d.discountCurve, Handle<Quote>(d.nVol), Actual365Fixed(),
361 d.pillarTimes, d.pillarTimes, d.pillarTimes, true, true);
362
363 performTest(d, engineLn0, engineLn, false, 0.0, "lognormal model, payer");
364 performTest(d, engineSln0, engineSln, false, 0.0, "shifted lognormal model, payer");
365 performTest(d, engineN0, engineN, false, 0.0, "normal model, payer");
366
367 performTest(d, engineLn0, engineLn, true, 0.0, "lognormal model, receiver");
368 performTest(d, engineSln0, engineSln, true, 0.0, "shifted lognormal model, receiver");
369 performTest(d, engineN0, engineN, true, 0.0, "normal model, receiver");
370
371 // the tests with non-zero spread fail, fix it later in the engine, for now we check for zero spreads there
372
373 // performTest(d, engineLn0, engineLn, false, 0.01, "lognormal model, payer, spread");
374 // performTest(d, engineSln0, engineSln, false, 0.01, "shifted lognormal model, payer, spread");
375 // performTest(d, engineN0, engineN, false, 0.01, "normal model, payer, spread");
376
377 // performTest(d, engineLn0, engineLn, true, 0.01, "lognormal model, receiver, spread");
378 // performTest(d, engineSln0, engineSln, true, 0.01, "shifted lognormal model, receiver, spread");
379 // performTest(d, engineN0, engineN, true, 0.01, "normal model, receiver, spread");
380
381 BOOST_CHECK(true);
382}
383
384BOOST_AUTO_TEST_SUITE_END()
385
386BOOST_AUTO_TEST_SUITE_END()
Swaption engine providing analytical deltas for vanilla swaps.
BOOST_AUTO_TEST_CASE(testNpvDeltasGammaVegas)
Fixture that can be used at top level.