Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
deltagammavar.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
20#include <qle/math/trace.hpp>
21
22#include <ql/math/comparison.hpp>
23#include <ql/math/distributions/normaldistribution.hpp>
24#include <ql/math/matrixutilities/choleskydecomposition.hpp>
25#include <ql/math/matrixutilities/symmetricschurdecomposition.hpp>
26#include <ql/math/solvers1d/brent.hpp>
27
28namespace QuantExt {
29
30namespace detail {
31void check(const Real p) { QL_REQUIRE(p >= 0.0 && p <= 1.0, "p (" << p << ") must be in [0,1] in VaR calculation"); }
32
33void check(const Matrix& omega, const Array& delta) {
34 QL_REQUIRE(omega.rows() == omega.columns(),
35 "omega (" << omega.rows() << "x" << omega.columns() << ") must be square in VaR calculation");
36 QL_REQUIRE(delta.size() == omega.rows(), "delta vector size (" << delta.size() << ") must match omega ("
37 << omega.rows() << "x" << omega.columns() << ")");
38}
39
40void check(const Matrix& omega, const Array& delta, const Matrix& gamma) {
41 QL_REQUIRE(gamma.rows() == omega.rows() && gamma.columns() == omega.columns(),
42 "gamma (" << gamma.rows() << "x" << gamma.columns() << ") must have same dimensions as omega ("
43 << omega.rows() << "x" << omega.columns() << ")");
44}
45} // namespace detail
46
47namespace {
48void moments(const Matrix& omega, const Array& delta, const Matrix& gamma, Real& num, Real& mu, Real& variance) {
49 detail::check(omega, delta, gamma);
50
51 // see Carol Alexander, Market Risk, Vol IV
52 // Formulas IV5.30 and IV5.31 are buggy though:
53 // IV.5.30 should have ... + 3 \delta' \Omega \Gamma \Omega \delta in the numerator
54 // IV.5.31 should have ... + 12 \delta \Omega (\Gamma \Omega)^2 \delta + 3\sigma^4 in the numerator
55
56 num = std::max(QuantExt::detail::absMax(delta), QuantExt::detail::absMax(gamma));
57 if (close_enough(num, 0.0))
58 return;
59
60 Array tmpDelta = 1.0 / num * delta;
61 Matrix tmpGamma = 1.0 / num * gamma;
62
63 Real dOd = DotProduct(tmpDelta, omega * tmpDelta);
64 Matrix go = tmpGamma * omega;
65 Matrix go2 = go * go;
66 Real trGo2 = Trace(go2);
67
68 mu = 0.5 * Trace(go);
69 variance = dOd + 0.5 * trGo2;
70}
71
72void moments(const Matrix& omega, const Array& delta, const Matrix& gamma, Real& num, Real& mu, Real& variance,
73 Real& tau, Real& kappa) {
74 detail::check(omega, delta, gamma);
75
76 // see Carol Alexander, Market Risk, Vol IV
77 // Formulas IV5.30 and IV5.31 are buggy though:
78 // IV.5.30 should have ... + 3 \delta' \Omega \Gamma \Omega \delta in the numerator
79 // IV.5.31 should have ... + 12 \delta \Omega (\Gamma \Omega)^2 \delta + 3\sigma^4 in the numerator
80
81 num = std::max(QuantExt::detail::absMax(delta), QuantExt::detail::absMax(gamma));
82 if (close_enough(num, 0.0))
83 return;
84
85 Array tmpDelta = 1.0 / num * delta;
86 Matrix tmpGamma = 1.0 / num * gamma;
87
88 Real dOd = DotProduct(tmpDelta, omega * tmpDelta);
89 Matrix go = tmpGamma * omega;
90 Matrix go2 = go * go;
91 Real trGo2 = Trace(go2);
92
93 mu = 0.5 * Trace(go);
94 variance = dOd + 0.5 * trGo2;
95
96 Matrix go3 = go2 * go;
97 Matrix go4 = go2 * go2;
98 Matrix ogo = omega * go;
99 Matrix o_go2 = omega * go2;
100 Real trGo3 = Trace(go3);
101 Real trGo4 = Trace(go4);
102
103 tau = (trGo3 + 3.0 * DotProduct(tmpDelta, ogo * tmpDelta)) / std::pow(variance, 1.5);
104 kappa = (3.0 * trGo4 + 12.0 * DotProduct(tmpDelta, o_go2 * tmpDelta)) / (variance * variance);
105}
106
107std::pair<Real, Real> bracketRoot(const std::function<Real(Real)>& p, const Real h, const Real growth, const Real tol,
108 const Size maxSteps, const Real leftBoundary, const Real rightBoundary) {
109 // very simple bracketing algorithm, TODO can we do that smarter?
110 // the start point is chosen as close to zero as possible
111 Real start = std::min(std::max(leftBoundary + QL_EPSILON, 0.0), rightBoundary - QL_EPSILON);
112 Real xl = start, xr = start;
113 Real yl, yr;
114 Real h0 = h;
115 yl = yr = p(start);
116 if (std::abs(yl) < tol)
117 return std::make_pair(start, start);
118 Size iter = 0;
119 bool movingright = true, movingleft = true;
120 while (iter++ < maxSteps && (movingleft || movingright)) {
121 // left
122 if (movingleft) {
123 Real xlh = std::max(xl - h0, leftBoundary + QL_EPSILON);
124 Real tmpl = p(xlh);
125 if (std::abs(tmpl) < tol)
126 return std::make_pair(xlh, xlh);
127 if (yl * tmpl < 0)
128 return std::make_pair(std::min(xlh, xl), xl);
129 if (xl - h0 > leftBoundary) {
130 xl -= h0;
131 yl = tmpl;
132 } else {
133 movingleft = false;
134 }
135 }
136 // right
137 if (movingright) {
138 Real xrh = std::min(xr + h0, rightBoundary - QL_EPSILON);
139 Real tmpr = p(xrh);
140 if (std::abs(tmpr) < tol)
141 return std::make_pair(xrh, xrh);
142 if (yr * tmpr < 0)
143 return std::make_pair(xr, std::max(xrh, xr));
144 if (xr + h0 < rightBoundary) {
145 xr += h0;
146 yr = tmpr;
147 } else {
148 movingright = false;
149 }
150 }
151 // stepsize growth
152 h0 *= 1.0 + growth;
153 }
154
155 // no solution
156 return std::make_pair(Null<Real>(), Null<Real>());
157}
158
159Real F(const Array& lambda, const Array& delta, const Real x) {
160 auto KPrimeMinusX = [&lambda, &delta, &x](const Real k) {
161 Real sum = 0.0;
162 for (Size j = 0; j < lambda.size(); ++j) {
163 Real denom = (1.0 - 2.0 * lambda[j] * k);
164 sum += lambda[j] / denom + delta[j] * delta[j] * k * (1.0 - k * lambda[j]) / (denom * denom);
165 }
166 return sum - x;
167 };
168
169 auto K = [&lambda, &delta](const Real k) {
170 Real sum = 0.0;
171 for (Size j = 0; j < lambda.size(); ++j) {
172 Real tmp = (1.0 - 2.0 * lambda[j] * k);
173 sum += -0.5 * std::log(tmp) + 0.5 * delta[j] * delta[j] * k * k / tmp;
174 }
175 return sum;
176 };
177
178 auto KPrime2 = [&lambda, &delta](const Real k) {
179 Real sum = 0.0;
180 for (Size j = 0; j < lambda.size(); ++j) {
181 Real denom = (1.0 - 2.0 * lambda[j] * k);
182 Real denom2 = denom * denom;
183 sum += 2.0 * lambda[j] * lambda[j] / denom2 + delta[j] * delta[j] / (denom2 * denom);
184 }
185 return sum;
186 };
187
188 auto KPrime3 = [&lambda, &delta](const Real k) {
189 Real sum = 0.0;
190 for (Size j = 0; j < lambda.size(); ++j) {
191 Real denom = (1.0 - 2.0 * lambda[j] * k);
192 Real denom2 = denom * denom;
193 Real l2 = lambda[j] * lambda[j];
194 sum += 8.0 * lambda[j] * l2 / (denom2 * denom) + 6.0 * delta[j] * delta[j] * lambda[j] / (denom2 * denom2);
195 }
196 return sum;
197 };
198
199 auto KPrime4 = [&lambda, &delta](const Real k) {
200 Real sum = 0.0;
201 for (Size j = 0; j < lambda.size(); ++j) {
202 Real denom = (1.0 - 2.0 * lambda[j] * k);
203 Real denom2 = denom * denom;
204 Real denom4 = denom2 * denom2;
205 Real l2 = lambda[j] * lambda[j];
206 sum += 48.0 * l2 * l2 / (denom2 * denom2) + 48.0 * delta[j] * delta[j] * l2 / (denom4 * denom);
207 }
208 return sum;
209 };
210
211 Real b1 = 0.0, b2 = 0.0, khat, yTol = 1E-7, xTol = 1E-10; // TODO check parameter
212
213 // determine the interval (c1,c2) where K is valid
214 QL_REQUIRE(!lambda.empty(), "lambda is empty");
215 Real c1 = -QL_MAX_REAL, c2 = QL_MAX_REAL;
216 Real minl = *std::min_element(lambda.begin(), lambda.end());
217 Real maxl = *std::max_element(lambda.begin(), lambda.end());
218 if (minl < 0.0 && !close_enough(minl, 0.0))
219 c1 = 1.0 / (2.0 * minl);
220 if (maxl > 0.0 && !close_enough(maxl, 0.0))
221 c2 = 1.0 / (2.0 * maxl);
222
223 std::vector<Real> singularities;
224 for (Size i = 0; i < lambda.size(); ++i) {
225 if (!close_enough(lambda[i], 0.0)) {
226 singularities.push_back(1.0 / (2.0 * lambda[i]));
227 }
228 }
229
230 // try to bracket saddlepoint, avoiding singularities
231 singularities.push_back(-QL_MAX_REAL);
232 singularities.push_back(QL_MAX_REAL);
233 std::sort(singularities.begin(), singularities.end());
234 Size index = std::upper_bound(singularities.begin(), singularities.end(), 0.0) - singularities.begin();
235 int direction = 1;
236 Size stepsize = 1, counter = 0, indextmp = index;
237 while (counter < singularities.size() - 1) {
238 if (indextmp > 0 && indextmp < singularities.size()) {
239 // shrink the candidate interval to the admissaible region (c1,c2)
240 Real a1 = std::max(std::min(singularities[indextmp - 1], c2), c1);
241 Real a2 = std::max(std::min(singularities[indextmp], c2), c1);
242 if (!close_enough(a1, a2)) {
243 // TODO check parameters in bracketRoot
244 std::tie(b1, b2) =
245 bracketRoot(KPrimeMinusX, std::min(1E-5, (a2 - a1) / 10.0), 1E-2, yTol, 100000, a1, a2);
246 if (b1 != Null<Real>())
247 break;
248 }
249 ++counter;
250 }
251 indextmp = index + direction * stepsize;
252 direction *= -1;
253 if (direction > 0)
254 ++stepsize;
255 QL_REQUIRE(stepsize < singularities.size(), "deltaGammaVarSaddlepoint: unexpected stepsize");
256 }
257
258 QL_REQUIRE(b1 != Null<Real>(), "deltaGammaVarSaddlepoint: could not bracket root to find K'(k) = x");
259
260 if (close_enough(b1, b2)) {
261 khat = b1;
262 } else {
263 Brent b;
264 khat = b.solve(KPrimeMinusX, xTol, (b1 + b2) / 2.0, b1, b2);
265 }
266
267 Real eta = khat * std::sqrt(KPrime2(khat));
268 Real tmp = 2.0 * (khat * x - K(khat));
269 Real xi = (khat >= 0.0 ? 1.0 : -1.0) * std::sqrt(tmp);
270
271 QuantLib::CumulativeNormalDistribution cnd;
272 QuantLib::NormalDistribution nd;
273
274 if (std::abs(khat) > 1E-5) {
275 Real res = cnd(xi) - nd(xi) * (1.0 / eta - 1.0 / xi);
276 // handle nan, TODO what is reasonable here?
277 if (std::isnan(res))
278 res = 1.0;
279 return res;
280 } else {
281 // see Daniels, TODO test this case (not yet covered by unit tests?)
282 Real eta2 = eta * eta;
283 Real eta3 = eta2 * eta;
284 Real eta4 = eta2 * eta2;
285 Real eta6 = eta3 * eta3;
286 Real KP2 = KPrime2(khat);
287 Real a2 = KPrime2(khat) / KP2;
288 Real a3 = KPrime3(khat) / std::pow(KP2, 1.5);
289 Real a4 = KPrime4(khat) / (KP2 * KP2);
290 Real res = 1.0 - std::exp(K(khat) - khat * x + 0.5 * xi * xi) *
291 ((1.0 - cnd(eta)) * (1.0 - a3 * eta3 / 6.0 + a4 * eta4 / 24.0 + a2 * eta6 / 72.0) +
292 nd(eta) * (a3 * (eta2 - 1.0) / 6.0 - a4 * eta * (eta2 - 1.0) / 24.0 -
293 eta3 * (eta4 - eta2 + 3.0) / 72.0));
294 // handle nan, see above, TODO what is reasonable here?
295 if (std::isnan(res)) {
296 res = 1.0;
297 }
298 return res;
299 }
300
301} // F
302
303} // namespace
304
305Real deltaVar(const Matrix& omega, const Array& delta, const Real p, const CovarianceSalvage& sal) {
306 detail::check(p);
307 detail::check(omega, delta);
308 Real num = detail::absMax(delta);
309 if (close_enough(num, 0.0))
310 return 0.0;
311 Array tmpDelta = delta / num;
312 return std::sqrt(DotProduct(tmpDelta, sal.salvage(omega).first * tmpDelta)) *
313 QuantLib::InverseCumulativeNormal()(p) * num;
314} // deltaVar
315
316Real deltaGammaVarNormal(const Matrix& omega, const Array& delta, const Matrix& gamma, const Real p,
317 const CovarianceSalvage& sal) {
318 detail::check(p);
319 Real s = QuantLib::InverseCumulativeNormal()(p);
320 Real num = 0.0, mu = 0.0, variance = 0.0;
321 moments(sal.salvage(omega).first, delta, gamma, num, mu, variance);
322 if (close_enough(num, 0.0) || close_enough(variance, 0.0))
323 return 0.0;
324 return (std::sqrt(variance) * s + mu) * num;
325
326} // deltaGammaVarNormal
327
328Real deltaGammaVarCornishFisher(const Matrix& omega, const Array& delta, const Matrix& gamma, const Real p,
329 const CovarianceSalvage& sal) {
330 detail::check(p);
331 Real s = QuantLib::InverseCumulativeNormal()(p);
332 Real num = 0.0, mu = 0.0, variance = 0.0, tau = 0.0, kappa = 0.0;
333 moments(sal.salvage(omega).first, delta, gamma, num, mu, variance, tau, kappa);
334 if (close_enough(num, 0.0) || close_enough(variance, 0.0))
335 return 0.0;
336
337 Real xTilde =
338 s + tau / 6.0 * (s * s - 1.0) + kappa / 24.0 * s * (s * s - 3.0) - tau * tau / 36.0 * s * (2.0 * s * s - 5.0);
339 return (xTilde * std::sqrt(variance) + mu) * num;
340} // deltaGammaVarCornishFisher
341
342Real deltaGammaVarSaddlepoint(const Matrix& omega, const Array& delta, const Matrix& gamma, const Real p,
343 const CovarianceSalvage& sal) {
344
345 /* References:
346
347 Lugannani, R.and S.Rice (1980),
348 Saddlepoint Approximations for the Distribution of the Sum of Independent Random Variables,
349 Advances in Applied Probability, 12,475-490.
350
351 Daniels, H. E. (1987), Tail Probability Approximations, International Statistical Review, 55, 37-48.
352 */
353
354 detail::check(p);
355 detail::check(omega, delta, gamma);
356
357 auto S = sal.salvage(omega);
358 Matrix L = S.second;
359 if (L.rows() == 0) {
360 L = CholeskyDecomposition(omega, true);
361 }
362
363 Matrix hLGL = 0.5 * transpose(L) * gamma * L;
364 SymmetricSchurDecomposition schur(hLGL);
365 Array lambda = schur.eigenvalues();
366
367 // we scale the problem to ensure numerical stability; for this we divide delta and gamma by a
368 // factor such that the largest eigenvalue has absolute value 1.0
369
370 // find the largest absolute eigenvalue
371 Real scaling = 0.0;
372 for (const auto l : lambda) {
373 Real a = std::abs(l);
374 if (!close_enough(a, 0.0))
375 scaling = std::max(scaling, a);
376 }
377
378 // if all eigenvalues are zero we do not scale anything
379 if (close_enough(scaling, 0.0))
380 scaling = 1.0;
381
382 // scaled delta and gamma
383 Array tmpDelta = delta / scaling;
384 Matrix tmpGamma = gamma / scaling;
385
386 // adjusted eigenvalues
387 for (auto& l : lambda) {
388 l /= scaling;
389 }
390
391 // compute quantile
392 Array deltaBar = transpose(schur.eigenvectors()) * transpose(L) * tmpDelta;
393
394 // At this point, the delta-gamma PL can be written as scaling * ( deltaBar^T z + z^T GammaBar z ), where GammaBar
395 // is a diagonal matrix with the lambdas on the diagonal and z a vector of independent standard normal variates.
396 // We now compare the 2-norms of deltaBar and gammaBar and fall back on a simple delta VaR calculation, if the
397 // norm of gammaBar is negligible compared to the norm of deltaBar; this prevents numerical instabilities in the
398 // saddlepoint search, where the function k'(x)-x can become very steep around the saddlepoint in this situation.
399 Real normDeltaBar = 0.0, normGammaBar = 0.0;
400 for (Size i = 0; i < deltaBar.size(); ++i) {
401 normDeltaBar += deltaBar[i] * deltaBar[i];
402 normGammaBar += lambda[i] * lambda[i];
403 }
404 if (normGammaBar / normDeltaBar < 1E-10)
405 return QuantExt::deltaVar(S.first, delta, p);
406
407 // continue with the saddlepoint approach
408 auto FMinusP = [&lambda, &deltaBar, &p](const Real x) { return F(lambda, deltaBar, x) - p; };
409 Real quantile;
410 try {
411 // TODO check hardcoded tolerance, guess and step
412 Brent b;
413 quantile = b.solve(FMinusP, 1E-6, 0.0, 1.0);
414 } catch (const std::exception& e) {
415 QL_FAIL("deltaGammaVarSaddlepoint: could no solve for quantile p = " << p << ": " << e.what());
416 }
417
418 // undo scaling and return result
419 return quantile * scaling;
420
421} // deltaGammaVarSaddlepoint
422
423} // namespace QuantExt
functions to compute delta or delta-gamma VaR numbers
Real absMax(const A &a)
void check(const Real p)
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
RandomVariable variance(const RandomVariable &r)
Real deltaGammaVarCornishFisher(const Matrix &omega, const Array &delta, const Matrix &gamma, const Real p, const CovarianceSalvage &sal)
Real deltaGammaVarNormal(const Matrix &omega, const Array &delta, const Matrix &gamma, const Real p, const CovarianceSalvage &sal)
function that computes a delta-gamma normal VaR
Real Trace(const Matrix &m)
Definition: trace.hpp:34
Real deltaVar(const Matrix &omega, const Array &delta, const Real p, const CovarianceSalvage &sal)
function that computes a delta VaR
Real deltaGammaVarSaddlepoint(const Matrix &omega, const Array &delta, const Matrix &gamma, const Real p, const CovarianceSalvage &sal)
Real sum(const Cash &c, const Cash &d)
Definition: bondbasket.cpp:107
virtual std::pair< Matrix, Matrix > salvage(const Matrix &m) const =0
trace of a quadratic matrix