/src/quantlib/ql/termstructures/volatility/zabr.cpp
Line | Count | Source |
1 | | /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | |
3 | | /* |
4 | | Copyright (C) 2014 Peter Caspers |
5 | | Copyright (C) 2026 Aaditya Panikath |
6 | | |
7 | | This file is part of QuantLib, a free-software/open-source library |
8 | | for financial quantitative analysts and developers - http://quantlib.org/ |
9 | | |
10 | | QuantLib is free software: you can redistribute it and/or modify it |
11 | | under the terms of the QuantLib license. You should have received a |
12 | | copy of the license along with this program; if not, please email |
13 | | <quantlib-dev@lists.sf.net>. The license is also available online at |
14 | | <https://www.quantlib.org/license.shtml>. |
15 | | |
16 | | This program is distributed in the hope that it will be useful, but WITHOUT |
17 | | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
18 | | FOR A PARTICULAR PURPOSE. See the license for more details. |
19 | | */ |
20 | | |
21 | | #include <ql/termstructures/volatility/zabr.hpp> |
22 | | #include <ql/termstructures/volatility/sabr.hpp> |
23 | | #include <ql/errors.hpp> |
24 | | #include <ql/math/comparison.hpp> |
25 | | #include <ql/math/distributions/normaldistribution.hpp> |
26 | | #include <ql/math/ode/adaptiverungekutta.hpp> |
27 | | #include <ql/methods/finitedifferences/operators/fdmlinearoplayout.hpp> |
28 | | #include <ql/methods/finitedifferences/meshers/fdm1dmesher.hpp> |
29 | | #include <ql/methods/finitedifferences/meshers/uniform1dmesher.hpp> |
30 | | #include <ql/methods/finitedifferences/meshers/concentrating1dmesher.hpp> |
31 | | #include <ql/experimental/finitedifferences/glued1dmesher.hpp> |
32 | | #include <ql/methods/finitedifferences/meshers/fdmmeshercomposite.hpp> |
33 | | #include <ql/methods/finitedifferences/operatortraits.hpp> |
34 | | #include <ql/methods/finitedifferences/utilities/fdmdirichletboundary.hpp> |
35 | | #include <ql/methods/finitedifferences/solvers/fdmbackwardsolver.hpp> |
36 | | #include <ql/experimental/finitedifferences/fdmdupire1dop.hpp> |
37 | | #include <ql/experimental/finitedifferences/fdmzabrop.hpp> |
38 | | |
39 | | using std::pow; |
40 | | |
41 | | namespace QuantLib { |
42 | | |
43 | | ZabrModel::ZabrModel(const Real expiryTime, const Real forward, |
44 | | const Real alpha, const Real beta, const Real nu, |
45 | | const Real rho, const Real gamma) |
46 | 0 | : expiryTime_(expiryTime), forward_(forward), alpha_(alpha), beta_(beta), |
47 | 0 | nu_(nu * std::pow(alpha_, 1.0 - gamma)), rho_(rho), gamma_(gamma) { |
48 | |
|
49 | 0 | validateSabrParameters(alpha, beta, nu, rho); |
50 | 0 | QL_REQUIRE(gamma >= 0.0 /*&& gamma<=1.0*/, |
51 | 0 | "gamma must be non negative: " << gamma << " not allowed"); |
52 | 0 | QL_REQUIRE(forward >= 0.0, |
53 | 0 | "forward must be non negative: " << forward << " not allowed"); |
54 | 0 | QL_REQUIRE(expiryTime > 0.0, "expiry time must be positive: " |
55 | 0 | << expiryTime << " not allowed"); |
56 | 0 | } |
57 | | |
58 | | Real ZabrModel::lognormalVolatilityHelper(const Real strike, |
59 | 0 | const Real x) const { |
60 | 0 | if (close(strike, forward_)) |
61 | 0 | return std::pow(forward_, beta_ - 1.0) * alpha_; |
62 | 0 | else |
63 | 0 | return std::log(forward_ / strike) / x; |
64 | 0 | } |
65 | | |
66 | 0 | Real ZabrModel::lognormalVolatility(const Real strike) const { |
67 | 0 | return lognormalVolatility(std::vector<Real>(1, strike))[0]; |
68 | 0 | } |
69 | | |
70 | 0 | std::vector<Real> ZabrModel::lognormalVolatility(const std::vector<Real> &strikes) const { |
71 | 0 | std::vector<Real> x_ = x(strikes); |
72 | 0 | std::vector<Real> result(strikes.size()); |
73 | 0 | std::transform(strikes.begin(), strikes.end(), x_.begin(), result.begin(), |
74 | 0 | [&](Real _k, Real _x) { return lognormalVolatilityHelper(_k, _x); }); |
75 | 0 | return result; |
76 | 0 | } |
77 | | |
78 | 0 | Real ZabrModel::normalVolatilityHelper(const Real strike, const Real x) const { |
79 | 0 | if (close(strike, forward_)) |
80 | 0 | return std::pow(forward_, beta_) * alpha_; |
81 | 0 | else |
82 | 0 | return (forward_ - strike) / x; |
83 | 0 | } |
84 | | |
85 | 0 | Real ZabrModel::normalVolatility(const Real strike) const { |
86 | 0 | return normalVolatility(std::vector<Real>(1, strike))[0]; |
87 | 0 | } |
88 | | |
89 | 0 | std::vector<Real> ZabrModel::normalVolatility(const std::vector<Real> &strikes) const { |
90 | 0 | std::vector<Real> x_ = x(strikes); |
91 | 0 | std::vector<Real> result(strikes.size()); |
92 | 0 | std::transform(strikes.begin(), strikes.end(), x_.begin(), result.begin(), |
93 | 0 | [&](Real _k, Real _x) { return normalVolatilityHelper(_k, _x); }); |
94 | 0 | return result; |
95 | 0 | } |
96 | | |
97 | 0 | Real ZabrModel::localVolatilityHelper(const Real f, const Real x) const { |
98 | 0 | return alpha_ * std::pow(std::fabs(f), beta_) / |
99 | 0 | F(y(f), std::pow(alpha_, gamma_ - 1.0) * |
100 | 0 | x); // TODO optimize this, y is comoputed together |
101 | | // with x already |
102 | 0 | } |
103 | | |
104 | 0 | Real ZabrModel::localVolatility(const Real f) const { |
105 | 0 | return localVolatility(std::vector<Real>(1, f))[0]; |
106 | 0 | } |
107 | | |
108 | 0 | std::vector<Real> ZabrModel::localVolatility(const std::vector<Real> &f) const { |
109 | 0 | std::vector<Real> x_ = x(f); |
110 | 0 | std::vector<Real> result(f.size()); |
111 | 0 | std::transform(f.begin(), f.end(), x_.begin(), result.begin(), |
112 | 0 | [&](Real _f, Real _x) { return localVolatilityHelper(_f, _x); }); |
113 | 0 | return result; |
114 | 0 | } |
115 | | |
116 | 0 | Real ZabrModel::fdPrice(const Real strike) const { |
117 | 0 | return fdPrice(std::vector<Real>(1, strike))[0]; |
118 | 0 | } |
119 | | |
120 | 0 | std::vector<Real> ZabrModel::fdPrice(const std::vector<Real> &strikes) const { |
121 | | |
122 | | // TODO check strikes to be increasing |
123 | | // TODO put these parameters somewhere |
124 | 0 | const Real start = |
125 | 0 | std::min(0.00001, strikes.front() * 0.5); // lowest strike for grid |
126 | 0 | const Real end = |
127 | 0 | std::max(0.10, strikes.back() * 1.5); // highest strike for grid |
128 | 0 | const Size size = 500; // grid points |
129 | 0 | const Real density = 0.1; // density for concentrating mesher |
130 | 0 | const Size steps = |
131 | 0 | (Size)std::ceil(expiryTime_ * 24); // number of steps in dimension t |
132 | 0 | const Size dampingSteps = 5; // thereof damping steps |
133 | |
|
134 | | #if defined(__GNUC__) && (__GNUC__ >= 12) |
135 | | #pragma GCC diagnostic push |
136 | | #pragma GCC diagnostic ignored "-Warray-bounds" |
137 | | #endif |
138 | | |
139 | | // Layout |
140 | 0 | std::vector<Size> dim(1, size); |
141 | 0 | const ext::shared_ptr<FdmLinearOpLayout> layout( |
142 | 0 | new FdmLinearOpLayout(dim)); |
143 | |
|
144 | | #if defined(__GNUC__) && (__GNUC__ >= 12) |
145 | | #pragma GCC diagnostic pop |
146 | | #endif |
147 | | |
148 | | // Mesher |
149 | 0 | const ext::shared_ptr<Fdm1dMesher> m1 = |
150 | 0 | ext::make_shared<Concentrating1dMesher>( |
151 | 0 | start, end, size, std::pair<Real, Real>(forward_, density), true); |
152 | | // const ext::shared_ptr<Fdm1dMesher> m1 = |
153 | | // ext::make_shared<Uniform1dMesher>(start,end,size); |
154 | | // const ext::shared_ptr<Fdm1dMesher> m1a = |
155 | | // ext::make_shared<Uniform1dMesher>(start,0.03,101); |
156 | | // const ext::shared_ptr<Fdm1dMesher> m1b = |
157 | | // ext::make_shared<Uniform1dMesher>(0.03,end,100); |
158 | | // const ext::shared_ptr<Fdm1dMesher> m1 = |
159 | | // ext::make_shared<Glued1dMesher>(*m1a,*m1b); |
160 | 0 | const std::vector<ext::shared_ptr<Fdm1dMesher> > meshers(1, m1); |
161 | 0 | const ext::shared_ptr<FdmMesher> mesher = |
162 | 0 | ext::make_shared<FdmMesherComposite>(layout, meshers); |
163 | | |
164 | | // Boundary conditions |
165 | 0 | FdmBoundaryConditionSet boundaries; |
166 | | |
167 | | // initial values |
168 | 0 | Array rhs(mesher->layout()->size()); |
169 | 0 | for (const auto& iter : *layout) { |
170 | 0 | Real k = mesher->location(iter, 0); |
171 | 0 | rhs[iter.index()] = std::max(forward_ - k, 0.0); |
172 | 0 | } |
173 | | |
174 | | // local vols (TODO how can we avoid these Array / vector copying?) |
175 | 0 | Array k = mesher->locations(0); |
176 | 0 | std::vector<Real> kv(k.size()); |
177 | 0 | std::copy(k.begin(), k.end(), kv.begin()); |
178 | 0 | std::vector<Real> locVolv = localVolatility(kv); |
179 | 0 | Array locVol(locVolv.size()); |
180 | 0 | std::copy(locVolv.begin(), locVolv.end(), locVol.begin()); |
181 | | |
182 | | // solver |
183 | 0 | auto map = |
184 | 0 | ext::make_shared<FdmDupire1dOp>(mesher, locVol); |
185 | 0 | FdmBackwardSolver solver(map, boundaries, |
186 | 0 | ext::shared_ptr<FdmStepConditionComposite>(), |
187 | 0 | FdmSchemeDesc::Douglas()); |
188 | 0 | solver.rollback(rhs, expiryTime_, 0.0, steps, dampingSteps); |
189 | | |
190 | | // interpolate solution |
191 | 0 | ext::shared_ptr<Interpolation> solution = ext::make_shared<CubicInterpolation>( |
192 | 0 | k.begin(), k.end(), rhs.begin(), CubicInterpolation::Spline, true, |
193 | 0 | CubicInterpolation::SecondDerivative, 0.0, |
194 | 0 | CubicInterpolation::SecondDerivative, 0.0); |
195 | | // ext::shared_ptr<Interpolation> solution = |
196 | | // ext::make_shared<LinearInterpolation>(k.begin(),k.end(),rhs.begin()); |
197 | 0 | solution->disableExtrapolation(); |
198 | 0 | std::vector<Real> result(strikes.size()); |
199 | 0 | std::transform(strikes.begin(), strikes.end(), result.begin(), *solution); |
200 | 0 | return result; |
201 | 0 | } |
202 | | |
203 | 0 | Real ZabrModel::fullFdPrice(const Real strike) const { |
204 | | |
205 | | // TODO what are good values here, still experimenting with them |
206 | 0 | Real eps = 0.01; |
207 | 0 | Real scaleFactor = 1.5; |
208 | 0 | Real normInvEps = InverseCumulativeNormal()(1.0 - eps); |
209 | 0 | Real alphaI = alpha_ * std::pow(forward_, beta_ - 1.0); |
210 | | // nu is already standardized within this class ... |
211 | 0 | Real v0 = alpha_ * std::exp(-scaleFactor * normInvEps * |
212 | 0 | std::sqrt(expiryTime_) * nu_); |
213 | 0 | Real v1 = alpha_ * |
214 | 0 | std::exp(scaleFactor * normInvEps * std::sqrt(expiryTime_) * nu_); |
215 | 0 | Real f0 = forward_ * std::exp(-scaleFactor * normInvEps * |
216 | 0 | std::sqrt(expiryTime_) * alphaI); |
217 | 0 | Real f1 = forward_ * std::exp(scaleFactor * normInvEps * |
218 | 0 | std::sqrt(expiryTime_) * alphaI); |
219 | 0 | v1 = std::min(v1, 2.0); |
220 | 0 | f0 = std::min(strike / 2.0, f0); |
221 | 0 | f1 = std::max(strike * 1.5, std::min(f1, std::max(2.0, strike * 1.5))); |
222 | |
|
223 | 0 | const Size sizef = 100; |
224 | 0 | const Size sizev = 100; |
225 | 0 | const Size steps = Size(24 * expiryTime_ + 1); |
226 | 0 | const Size dampingSteps = 5; |
227 | 0 | const Real densityf = 0.1; |
228 | 0 | const Real densityv = 0.1; |
229 | |
|
230 | 0 | QL_REQUIRE(strike >= f0 && strike <= f1, |
231 | 0 | "strike (" << strike << ") must be inside pde grid [" << f0 |
232 | 0 | << ";" << f1 << "]"); |
233 | | |
234 | | // Layout |
235 | 0 | std::vector<Size> dim; |
236 | 0 | dim.push_back(sizef); |
237 | 0 | dim.push_back(sizev); |
238 | 0 | const ext::shared_ptr<FdmLinearOpLayout> layout( |
239 | 0 | new FdmLinearOpLayout(dim)); |
240 | | |
241 | | // Mesher |
242 | | // two concentrating mesher around f and k to get the mesher for f |
243 | 0 | const Real x0 = std::min(forward_, strike); |
244 | 0 | const Real x1 = std::max(forward_, strike); |
245 | 0 | const Size sizefa = std::max<Size>( |
246 | 0 | 4, (Size)std::ceil(((x0 + x1) / 2.0 - f0) / (f1 - f0) * (Real)sizef)); |
247 | 0 | const Size sizefb = sizef - sizefa + 1; // common point, so we can spend |
248 | | // one more here |
249 | 0 | const ext::shared_ptr<Fdm1dMesher> mfa = |
250 | 0 | ext::make_shared<Concentrating1dMesher>( |
251 | 0 | f0, (x0 + x1) / 2.0, sizefa, std::pair<Real, Real>(x0, densityf), true); |
252 | 0 | const ext::shared_ptr<Fdm1dMesher> mfb = |
253 | 0 | ext::make_shared<Concentrating1dMesher>( |
254 | 0 | (x0 + x1) / 2.0, f1, sizefb, std::pair<Real, Real>(x1, densityf), true); |
255 | 0 | const ext::shared_ptr<Fdm1dMesher> mf = ext::make_shared<Glued1dMesher>(*mfa, *mfb); |
256 | | |
257 | | // concentraing mesher around f to get the forward mesher |
258 | | // const ext::shared_ptr<Fdm1dMesher> mf = |
259 | | // ext::make_shared<Concentrating1dMesher>( |
260 | | // f0, f1, sizef, std::pair<Real, Real>(forward_, densityf), true); |
261 | | |
262 | | // Volatility mesher |
263 | 0 | const ext::shared_ptr<Fdm1dMesher> mv = |
264 | 0 | ext::make_shared<Concentrating1dMesher>( |
265 | 0 | v0, v1, sizev, std::pair<Real, Real>(alpha_, densityv), true); |
266 | | |
267 | | // uniform meshers |
268 | | // const ext::shared_ptr<Fdm1dMesher> mf = |
269 | | // ext::make_shared<Uniform1dMesher>(f0,f1,sizef); |
270 | | // const ext::shared_ptr<Fdm1dMesher> mv = |
271 | | // ext::make_shared<Uniform1dMesher>(v0,v1,sizev); |
272 | |
|
273 | 0 | std::vector<ext::shared_ptr<Fdm1dMesher> > meshers; |
274 | 0 | meshers.push_back(mf); |
275 | 0 | meshers.push_back(mv); |
276 | 0 | const ext::shared_ptr<FdmMesher> mesher( |
277 | 0 | new FdmMesherComposite(layout, meshers)); |
278 | | |
279 | | // initial values |
280 | 0 | Array rhs(mesher->layout()->size()); |
281 | 0 | std::vector<Real> f_; |
282 | 0 | std::vector<Real> v_; |
283 | 0 | for (const auto& iter : *layout) { |
284 | 0 | Real f = mesher->location(iter, 0); |
285 | | // Real v = mesher->location(iter, 0); |
286 | 0 | rhs[iter.index()] = std::max(f - strike, 0.0); |
287 | 0 | if (iter.coordinates()[1] == 0U) |
288 | 0 | f_.push_back(mesher->location(iter, 0)); |
289 | 0 | if (iter.coordinates()[0] == 0U) |
290 | 0 | v_.push_back(mesher->location(iter, 1)); |
291 | 0 | } |
292 | | |
293 | | // Boundary conditions |
294 | 0 | FdmBoundaryConditionSet boundaries; |
295 | |
|
296 | 0 | ext::shared_ptr<FdmZabrOp> map( |
297 | 0 | new FdmZabrOp(mesher, beta_, nu_, rho_, gamma_)); |
298 | 0 | FdmBackwardSolver solver(map, boundaries, |
299 | 0 | ext::shared_ptr<FdmStepConditionComposite>(), |
300 | 0 | FdmSchemeDesc::/*CraigSneyd()*/ Hundsdorfer()); |
301 | |
|
302 | 0 | solver.rollback(rhs, expiryTime_, 0.0, steps, dampingSteps); |
303 | | |
304 | | // interpolate solution (this is not necessary when using concentrating |
305 | | // meshers with required point) |
306 | 0 | Matrix result(f_.size(), v_.size()); |
307 | 0 | for (Size j = 0; j < v_.size(); ++j) |
308 | 0 | std::copy(rhs.begin() + j * f_.size(), |
309 | 0 | rhs.begin() + (j + 1) * f_.size(), result.row_begin(j)); |
310 | 0 | ext::shared_ptr<BicubicSpline> interpolation = |
311 | 0 | ext::make_shared<BicubicSpline>( |
312 | 0 | f_.begin(), f_.end(), v_.begin(), v_.end(), result); |
313 | 0 | interpolation->disableExtrapolation(); |
314 | 0 | return (*interpolation)(forward_, alpha_); |
315 | 0 | } |
316 | | |
317 | 0 | Real ZabrModel::x(const Real strike) const { |
318 | 0 | return x(std::vector<Real>(1, strike))[0]; |
319 | 0 | } |
320 | | |
321 | 0 | std::vector<Real> ZabrModel::x(const std::vector<Real> &strikes) const { |
322 | |
|
323 | 0 | QL_REQUIRE(strikes[0] > 0.0 || beta_ < 1.0, |
324 | 0 | "strikes must be positive (" << strikes[0] << ") if beta = 1"); |
325 | 0 | for (auto i = strikes.begin() + 1; i != strikes.end(); ++i) |
326 | 0 | QL_REQUIRE(*i > *(i - 1), "strikes must be strictly ascending (" |
327 | 0 | << *(i - 1) << "," << *i << ")"); |
328 | | |
329 | 0 | AdaptiveRungeKutta<Real> rk(1.0E-8, 1.0E-5, |
330 | 0 | 0.0); // TODO move the parameters here as |
331 | | // parameters with default values to |
332 | | // the constructor |
333 | 0 | std::vector<Real> y(strikes.size()), result(strikes.size()); |
334 | 0 | std::transform(strikes.rbegin(), strikes.rend(), y.begin(), |
335 | 0 | [&](Real _k) { return this->y(_k); }); |
336 | |
|
337 | 0 | if (close(gamma_, 1.0)) { |
338 | 0 | for (Size m = 0; m < y.size(); m++) { |
339 | 0 | Real J = std::sqrt(1.0 + nu_ * nu_ * y[m] * y[m] - |
340 | 0 | 2.0 * rho_ * nu_ * y[m]); |
341 | 0 | result[y.size() - 1 - m] = |
342 | 0 | std::log((J + nu_ * y[m] - rho_) / (1.0 - rho_)) / nu_; |
343 | 0 | } |
344 | 0 | } else { |
345 | 0 | Size ynz = std::upper_bound(y.begin(), y.end(), 0.0) - y.begin(); |
346 | 0 | if (ynz > 0) |
347 | 0 | if (close(y[ynz - 1], 0.0)) |
348 | 0 | ynz--; |
349 | 0 | if (ynz == y.size()) |
350 | 0 | ynz--; |
351 | |
|
352 | 0 | for (int dir = 1; dir >= -1; dir -= 2) { |
353 | 0 | Real y0 = 0.0, u0 = 0.0; |
354 | 0 | for (int m = ynz + (dir == -1 ? -1 : 0); |
355 | 0 | dir == -1 ? m >= 0 : m < (int)y.size(); m += dir) { |
356 | 0 | Real u = rk([&](Real _y, Real _u){ return F(_y, _u); }, |
357 | 0 | u0, y0, y[m]); |
358 | 0 | result[y.size() - 1 - m] = u * pow(alpha_, 1.0 - gamma_); |
359 | 0 | u0 = u; |
360 | 0 | y0 = y[m]; |
361 | 0 | } |
362 | 0 | } |
363 | 0 | } |
364 | |
|
365 | 0 | return result; |
366 | 0 | } |
367 | | |
368 | 0 | Real ZabrModel::y(const Real strike) const { |
369 | |
|
370 | 0 | if (close(beta_, 1.0)) { |
371 | 0 | return std::log(forward_ / strike) * std::pow(alpha_, gamma_ - 2.0); |
372 | 0 | } else { |
373 | 0 | return (strike < 0.0 |
374 | 0 | ? Real(std::pow(forward_, 1.0 - beta_) + |
375 | 0 | std::pow(-strike, 1.0 - beta_)) |
376 | 0 | : Real(std::pow(forward_, 1.0 - beta_) - |
377 | 0 | std::pow(strike, 1.0 - beta_))) * |
378 | 0 | std::pow(alpha_, gamma_ - 2.0) / (1.0 - beta_); |
379 | 0 | } |
380 | 0 | } |
381 | | |
382 | 0 | Real ZabrModel::F(const Real y, const Real u) const { |
383 | 0 | Real A = 1.0 + (gamma_ - 2.0) * (gamma_ - 2.0) * nu_ * nu_ * y * y + |
384 | 0 | 2.0 * rho_ * (gamma_ - 2.0) * nu_ * y; |
385 | 0 | Real B = 2.0 * rho_ * (1.0 - gamma_) * nu_ + |
386 | 0 | 2.0 * (1.0 - gamma_) * (gamma_ - 2.0) * nu_ * nu_ * y; |
387 | 0 | Real C = (1.0 - gamma_) * (1.0 - gamma_) * nu_ * nu_; |
388 | 0 | return (-B * u + std::sqrt(B * B * u * u - 4.0 * A * (C * u * u - 1.0))) / |
389 | 0 | (2.0 * A); |
390 | 0 | } |
391 | | } |