Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
blackvariancesurfacesparse.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 "toplevelfixture.hpp"
20#include <boost/make_shared.hpp>
21#include <boost/test/unit_test.hpp>
22#include <ql/time/calendars/target.hpp>
23#include <ql/time/daycounters/actualactual.hpp>
25
26using namespace boost::unit_test_framework;
27using namespace QuantLib;
28using namespace QuantExt;
29using namespace std;
30
31BOOST_FIXTURE_TEST_SUITE(QuantExtTestSuite, qle::test::TopLevelFixture)
32
33BOOST_AUTO_TEST_SUITE(BlackVarianceSurfaceSparse)
34
35BOOST_AUTO_TEST_CASE(testBlackVarianceSurface) {
36
37 BOOST_TEST_MESSAGE("Testing QuantExt::BlackVarianceSurfaceSparse with market data...");
38
39 SavedSettings backup;
40
41 // using data from https://papers.ssrn.com/sol3/papers.cfm?abstract_id=1694972
42 // Appendix A: Tables and Figures
43 // Table 1: SX5E Implied Volatility Quotes
44
45 Settings::instance().evaluationDate() = Date(1, Mar, 2010);
46 Date today = Settings::instance().evaluationDate();
47
48 Real spot = 2772.70;
49
50 // vector of 12 times
51 vector<Time> all_times({ 0.025, 0.101, 0.197, 0.274, 0.523, 0.772, 1.769, 2.267, 2.784, 3.781, 4.778, 5.774 });
52
53 // strike (%) and vols.
54 // Data is stored here as per the table (vector of vectors, first element is strike, then we have
55 // vols. Empty cells are 0.0. This data is re-organised below
56 vector<vector<Real> > volData(
57 { { 51.31, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 33.66, 32.91 },
58 { 58.64, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 31.78, 31.29, 30.08 },
59 { 65.97, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 30.19, 29.76, 29.75 },
60 { 73.30, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 28.63, 28.48, 28.48 },
61 { 76.97, 00.00, 00.00, 00.00, 32.62, 30.79, 30.01, 28.43 },
62 { 80.63, 00.00, 00.00, 00.00, 30.58, 29.36, 28.76, 27.53, 27.13, 27.11, 27.11, 27.22, 28.09 },
63 { 84.30, 00.00, 00.00, 00.00, 28.87, 27.98, 27.50, 26.66 },
64 { 86.13, 33.65 },
65 { 87.96, 32.16, 29.06, 27.64, 27.17, 26.63, 26.37, 25.75, 25.55, 25.80, 25.85, 26.11, 26.93 },
66 { 89.79, 30.43, 27.97, 26.72 },
67 { 91.63, 28.80, 26.90, 25.78, 25.57, 25.31, 25.19, 24.97 },
68 { 93.46, 27.24, 25.90, 24.89 },
69 { 95.29, 25.86, 24.88, 24.05, 24.07, 24.04, 24.11, 24.18, 24.10, 24.48, 24.69, 25.01, 25.84 },
70 { 97.12, 24.66, 23.90, 23.29 },
71 { 98.96, 23.58, 23.00, 22.53, 22.69, 22.84, 22.99, 23.47 },
72 { 100.79, 22.47, 22.13, 21.84 },
73 { 102.62, 21.59, 21.40, 21.23, 21.42, 21.73, 21.98, 22.83, 22.75, 23.22, 23.84, 23.92, 24.86 },
74 { 104.45, 20.91, 20.76, 20.69 },
75 { 106.29, 20.56, 20.24, 20.25, 20.39, 20.74, 21.04, 22.13 },
76 { 108.12, 20.45, 19.82, 19.84 },
77 { 109.95, 20.25, 19.59, 19.44, 19.62, 19.88, 20.22, 21.51, 21.61, 22.19, 22.69, 23.05, 23.99 },
78 { 111.78, 19.33, 19.29, 19.20 },
79 { 113.62, 00.00, 00.00, 00.00, 19.02, 19.14, 19.50, 20.91 },
80 { 117.28, 00.00, 00.00, 00.00, 18.85, 18.54, 18.88, 20.39, 20.58, 21.22, 21.86, 22.23, 23.21 },
81 { 120.95, 00.00, 00.00, 00.00, 18.67, 18.11, 18.39, 19.90 },
82 { 124.61, 00.00, 00.00, 00.00, 18.71, 17.85, 17.93, 19.45, 00.00, 20.54, 21.03, 21.64, 22.51 },
83 { 131.94, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 19.88, 20.54, 21.05, 21.90 },
84 { 139.27, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 19.30, 20.02, 20.54, 21.35 },
85 { 146.60, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 00.00, 18.49, 19.64, 20.12 } });
86
87 // the 3 vectors we pass into the vol term structure
88 vector<Date> dates;
89 vector<Real> strikes;
90 vector<Volatility> vols;
91
92 // populate them with the above table
93 for (auto vd : volData) {
94 Real strike = spot * vd[0];
95 for (Size i = 1; i < vd.size(); i++) {
96 Volatility vol = vd[i] / 100.0;
97 if (vol > 0.0001) {
98 Date d = today + int(all_times[i - 1] * 365); // Roughly correct
99 // we have a triple
100 dates.push_back(d);
101 strikes.push_back(strike);
102 vols.push_back(vol);
103 }
104 }
105 }
106
107 Calendar cal = TARGET();
108 DayCounter dc = ActualActual(ActualActual::ISDA);
109
110 auto surface = QuantLib::ext::make_shared<QuantExt::BlackVarianceSurfaceSparse>(today, cal, dates, strikes, vols, dc);
111
112 // 1. Check that we recover all of the above inputs
113 for (auto vd : volData) {
114 Real strike = spot * vd[0];
115 for (Size i = 1; i < vd.size(); i++) {
116 Volatility expectedVol = vd[i] / 100.0;
117 if (expectedVol > 0.0001) {
118 Date d = today + int(all_times[i - 1] * 365); // Same as above
119 Volatility vol = surface->blackVol(d, strike);
120 BOOST_CHECK_CLOSE(vol, expectedVol, 1e-12);
121 }
122 }
123 }
124
125 // 2. Check we don't throw for all points and get a positive vol
126 vector<Real> all_strikes;
127 vector<Date> all_dates;
128 for (auto vd : volData)
129 all_strikes.push_back(spot * vd[0]);
130 for (auto t : all_times)
131 all_dates.push_back(today + int(t * 365));
132
133 for (auto strike : all_strikes) {
134 for (auto d : all_dates)
135 BOOST_CHECK(surface->blackVol(d, strike) > 0.0001);
136 for (auto t : all_times)
137 BOOST_CHECK(surface->blackVol(t, strike) > 0.0001);
138 }
139}
140
141BOOST_AUTO_TEST_CASE(testBlackVarianceSurfaceConstantVol) {
142
143 BOOST_TEST_MESSAGE("Testing QuantExt::BlackVarianceSurfaceSparse with constant vol data...");
144
145 SavedSettings backup;
146
147 Settings::instance().evaluationDate() = Date(1, Mar, 2010);
148 Date today = Settings::instance().evaluationDate();
149
150 // the 3 vectors we pass into the vol term structure
151 // We setup a small grid with 10% everywhere, this should return 10% vol for
152 // any point, i.e. a flat surface
153 vector<Date> dates = { Date(1, Mar, 2011), Date(1, Mar, 2011), Date(1, Mar, 2012), Date(1, Mar, 2012),
154 Date(1, Mar, 2013) };
155 vector<Real> strikes = { 2000, 3000, 2500, 3500, 3000 };
156 vector<Volatility> vols(strikes.size(), 0.1); // 10% everywhere
157
158 Calendar cal = TARGET();
159 DayCounter dc = ActualActual(ActualActual::ISDA);
160
161 QuantExt::BlackVarianceSurfaceSparse surface(today, cal, dates, strikes, vols, dc);
162
163 // Check we don't throw for all points and get a vol of 10%
164 for (Time t = 0.2; t < 20; t += 0.2) {
165 for (Real strike = 1500; strike < 6000; strike += 100) {
166 BOOST_CHECK_CLOSE(surface.blackVol(t, strike), 0.1, 1e-12);
167 }
168 }
169}
170
171BOOST_AUTO_TEST_CASE(testBlackVarianceSurfaceInputs) {
172 BOOST_TEST_MESSAGE("Testing QuantExt::BlackVarianceSurfaceSparse with erroneous inputs");
173
174 SavedSettings backup;
175
176 Settings::instance().evaluationDate() = Date(1, Mar, 2010);
177 Date today = Settings::instance().evaluationDate();
178
179 // the 3 vectors we pass into the vol term structure
180 // We insure that the vectors don't match in size.
181 vector<Date> dates = { Date(1, Mar, 2011), Date(1, Mar, 2011), Date(1, Mar, 2012), Date(1, Mar, 2012) };
182 vector<Real> strikes = { 2000, 3000, 2500, 3500, 3000 };
183 vector<Volatility> vols(strikes.size(), 0.1);
184
185 Calendar cal = TARGET();
186 DayCounter dc = ActualActual(ActualActual::ISDA);
187
188 BOOST_CHECK_THROW(QuantExt::BlackVarianceSurfaceSparse(today, cal, dates, strikes, vols, dc), QuantLib::Error);
189}
190
191BOOST_AUTO_TEST_CASE(testBalckVarianceEdgeCases) {
192 // Asking on/past first/last points on strikes/expiries
193 BOOST_TEST_MESSAGE("Testing edge cases");
194
195 SavedSettings backup;
196
197 Settings::instance().evaluationDate() = Date(1, Mar, 2010);
198 Date today = Settings::instance().evaluationDate();
199
200 // the 3 vectors we pass into the vol term structure
201 // We setup a small grid with 10% everywhere, this should return 10% vol for
202 // any point, i.e. a flat surface
203 vector<Date> dates = { Date(1, Mar, 2011), Date(1, Mar, 2011), Date(1, Mar, 2012), Date(1, Mar, 2012),
204 Date(1, Mar, 2013) };
205 vector<Real> strikes = { 2000, 3000, 2500, 3500, 3000 };
206 vector<Volatility> vols(strikes.size(), 0.1); // 10% everywhere
207
208 Calendar cal = TARGET();
209 DayCounter dc = ActualActual(ActualActual::ISDA);
210
211 Time t = 0.0; // vol at reference should be 0
212 Real strike1 = 0.0; // 0 strike
213 Real strike2 = strikes.front(); // lowest strike given
214 Real strike3 = 1500.0; // between strikes strike
215 Real strike4 = strikes.back(); // highest strike
216
217 Real strikeNeg = -1000;
218 // at reference date
219 QuantExt::BlackVarianceSurfaceSparse surface(today, cal, dates, strikes, vols, dc);
220 BOOST_CHECK_CLOSE(surface.blackVol(t, strike1), 0.1, 1e-12);
221 BOOST_CHECK_CLOSE(surface.blackVol(t, strike2), 0.1, 1e-12);
222 BOOST_CHECK_CLOSE(surface.blackVol(t, strike3), 0.1, 1e-12);
223 BOOST_CHECK_CLOSE(surface.blackVol(t, strike4), 0.1, 1e-12);
224
225 // passed reference date
226 Time t1 = surface.timeFromReference(Date(1, Mar, 2014));
227 Real expectedVol = 0.1;
228 BOOST_CHECK_CLOSE(surface.blackVol(t1, strike2), expectedVol, 1e-12);
229
230 // negative strike
231 BOOST_CHECK_THROW(surface.blackVol(t, strikeNeg), QuantLib::Error);
232}
233
234BOOST_AUTO_TEST_CASE(testBlackVarianceSinglePoint) {
235 BOOST_TEST_MESSAGE("Testing surface from single point");
236 // Given a single point, every request should be expected extrapolated value.
237
238 SavedSettings backup;
239
240 Settings::instance().evaluationDate() = Date(1, Mar, 2010);
241 Date today = Settings::instance().evaluationDate();
242
243 // the 3 vectors we pass into the vol term structure
244 // We setup a single point with 10% - should be a flat surface
245 vector<Date> dates = { Date(1, Mar, 2011) };
246 vector<Real> strikes = { 2500 };
247 vector<Volatility> vols(strikes.size(), 0.1); // 10% everywhere
248
249 Calendar cal = TARGET();
250 DayCounter dc = ActualActual(ActualActual::ISDA);
251
252 QuantExt::BlackVarianceSurfaceSparse surface(today, cal, dates, strikes, vols, dc);
253
254 // Check we don't throw for all points and get a vol of 10%
255 for (Time t = 0.0; t < 20; t += 0.2) {
256 for (Real strike = 1500; strike < 6000; strike += 100) {
257 BOOST_CHECK_CLOSE(surface.blackVol(t, strike), 0.1, 1e-12);
258 }
259 }
260}
261
262BOOST_AUTO_TEST_CASE(testBlackVarianceSurfaceAxisInterp) {
263
264 BOOST_TEST_MESSAGE("Testing QuantExt::BlackVarianceSurfaceSparse axis interpolations");
265
266 SavedSettings backup;
267
268 Settings::instance().evaluationDate() = Date(1, Mar, 2010);
269 Date today = Settings::instance().evaluationDate();
270
271 // the 3 vectors we pass into the vol term structure
272 // We setup a 4 X 4 grid with different vols everywhere.
273 // We test interpolations on grid edges and in centre
274 vector<Date> dates = { Date(1, Mar, 2011), Date(1, Mar, 2011), Date(1, Mar, 2012), Date(1, Mar, 2012) };
275 vector<Real> strikes = { 2000, 3000, 2000, 3000 };
276 vector<Volatility> vols = { 0.105, 0.12, 0.17, 0.15 };
277
278 Calendar cal = TARGET();
279 DayCounter dc = ActualActual(ActualActual::ISDA);
280
281 auto surface = QuantLib::ext::make_shared<QuantExt::BlackVarianceSurfaceSparse>(today, cal, dates, strikes, vols, dc);
282
283 // query points
284 auto t1 = surface->timeFromReference(Date(1, Mar, 2011)); // on first date
285 auto t2 = surface->timeFromReference(Date(1, Sep, 2011)); // between 2 dates
286 auto t3 = surface->timeFromReference(Date(1, Mar, 2012)); // on last date
287 Real s1 = 2000; // on first strike
288 Real s2 = 2500; // between 2 strikes
289 Real s3 = 3000; // on last strike
290
291 // expected vals
292 Volatility e1 = 0.151634737915710;
293 Volatility e2 = 0.112749722837797;
294 Volatility e3 = 0.146315408895419;
295 Volatility e4 = 0.160312195418814;
296 Volatility e5 = 0.140795255664746;
297
298 // checks
299 BOOST_CHECK_CLOSE(e1, surface->blackVol(t2, s1), 1e-12);
300 BOOST_CHECK_CLOSE(e2, surface->blackVol(t1, s2), 1e-12);
301 BOOST_CHECK_CLOSE(e3, surface->blackVol(t2, s2), 1e-12);
302 BOOST_CHECK_CLOSE(e4, surface->blackVol(t3, s2), 1e-12);
303 BOOST_CHECK_CLOSE(e5, surface->blackVol(t2, s3), 1e-12);
304}
305
306BOOST_AUTO_TEST_CASE(testBlackVarianceSurfaceFlatExtrapolation) {
307
308 BOOST_TEST_MESSAGE("Testing QuantExt::BlackVarianceSurfaceSparse flat extrapolation");
309
310 SavedSettings backup;
311
312 Settings::instance().evaluationDate() = Date(1, Mar, 2010);
313 Date today = Settings::instance().evaluationDate();
314
315 // the 3 vectors we pass into the vol term structure
316 // We setup a 4 X 4 grid with different vols everywhere.
317 vector<Date> dates = { Date(1, Mar, 2011), Date(1, Mar, 2011), Date(1, Mar, 2012), Date(1, Mar, 2012) };
318 vector<Real> strikes = { 2000, 3000, 2000, 3000 };
319 vector<Volatility> vols = { 0.105, 0.12, 0.17, 0.15 };
320
321 Calendar cal = TARGET();
322 DayCounter dc = ActualActual(ActualActual::ISDA);
323
324 auto surface = QuantLib::ext::make_shared<QuantExt::BlackVarianceSurfaceSparse>(today, cal, dates, strikes, vols, dc, true,
325 true, true);
326
327 Real s1 = 2000; // on first strike
328 Real s2 = 2500; // between 2 strikes
329 Real s3 = 3000; // on last strike
330
331 Time tb = surface->times().back();
332 Real edgeVol1 = surface->blackVol(tb, s1);
333 Real edgeVol2 = surface->blackVol(tb, s2);
334 Real edgeVol3 = surface->blackVol(tb, s3);
335
336 Real edgeVar1 = surface->blackVariance(tb, s1);
337 Real edgeVar2 = surface->blackVariance(tb, s2);
338 Real edgeVar3 = surface->blackVariance(tb, s3);
339
340 for (Real t = 1; t < 10; t++) {
341 BOOST_CHECK_CLOSE(edgeVol1, surface->blackVol(tb + t, s1), 1e-12);
342 BOOST_CHECK_CLOSE(edgeVol2, surface->blackVol(tb + t, s2), 1e-12);
343 BOOST_CHECK_CLOSE(edgeVol3, surface->blackVol(tb + t, s3), 1e-12);
344
345 BOOST_CHECK_CLOSE(edgeVar1, surface->blackVariance(tb + t, s1) * tb / (tb + t), 1e-12);
346 BOOST_CHECK_CLOSE(edgeVar2, surface->blackVariance(tb + t, s2) * tb / (tb + t), 1e-12);
347 BOOST_CHECK_CLOSE(edgeVar3, surface->blackVariance(tb + t, s3) * tb / (tb + t), 1e-12);
348 }
349}
350
351BOOST_AUTO_TEST_SUITE_END()
352
353BOOST_AUTO_TEST_SUITE_END()
Black volatility surface modeled as variance surface.
vector< Real > strikes
BOOST_AUTO_TEST_CASE(testBlackVarianceSurface)
Fixture that can be used at top level.