/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 | 0 | QL_REQUIRE(!strikes.empty(), "ZabrModel::fdPrice(): empty strikes"); |
122 | 0 | for (Size i = 0; i < strikes.size() - 1; ++i) { |
123 | 0 | QL_REQUIRE(strikes[i] < strikes[i + 1], "ZabrModel::fdPrice(): non-ascending strikes"); |
124 | 0 | } |
125 | | // TODO put these parameters somewhere |
126 | 0 | const Real start = |
127 | 0 | std::min(0.00001, strikes.front() * 0.5); // lowest strike for grid |
128 | 0 | const Real end = |
129 | 0 | std::max(0.10, strikes.back() * 1.5); // highest strike for grid |
130 | 0 | const Size size = 500; // grid points |
131 | 0 | const Real density = 0.1; // density for concentrating mesher |
132 | 0 | const Size steps = |
133 | 0 | (Size)std::ceil(expiryTime_ * 24); // number of steps in dimension t |
134 | 0 | const Size dampingSteps = 5; // thereof damping steps |
135 | |
|
136 | | #if defined(__GNUC__) && (__GNUC__ >= 12) |
137 | | #pragma GCC diagnostic push |
138 | | #pragma GCC diagnostic ignored "-Warray-bounds" |
139 | | #endif |
140 | | |
141 | | // Layout |
142 | 0 | std::vector<Size> dim(1, size); |
143 | 0 | const ext::shared_ptr<FdmLinearOpLayout> layout( |
144 | 0 | new FdmLinearOpLayout(dim)); |
145 | |
|
146 | | #if defined(__GNUC__) && (__GNUC__ >= 12) |
147 | | #pragma GCC diagnostic pop |
148 | | #endif |
149 | | |
150 | | // Mesher |
151 | 0 | const ext::shared_ptr<Fdm1dMesher> m1 = |
152 | 0 | ext::make_shared<Concentrating1dMesher>( |
153 | 0 | start, end, size, std::pair<Real, Real>(forward_, density), true); |
154 | | // const ext::shared_ptr<Fdm1dMesher> m1 = |
155 | | // ext::make_shared<Uniform1dMesher>(start,end,size); |
156 | | // const ext::shared_ptr<Fdm1dMesher> m1a = |
157 | | // ext::make_shared<Uniform1dMesher>(start,0.03,101); |
158 | | // const ext::shared_ptr<Fdm1dMesher> m1b = |
159 | | // ext::make_shared<Uniform1dMesher>(0.03,end,100); |
160 | | // const ext::shared_ptr<Fdm1dMesher> m1 = |
161 | | // ext::make_shared<Glued1dMesher>(*m1a,*m1b); |
162 | 0 | const std::vector<ext::shared_ptr<Fdm1dMesher> > meshers(1, m1); |
163 | 0 | const ext::shared_ptr<FdmMesher> mesher = |
164 | 0 | ext::make_shared<FdmMesherComposite>(layout, meshers); |
165 | | |
166 | | // Boundary conditions |
167 | 0 | FdmBoundaryConditionSet boundaries; |
168 | | |
169 | | // initial values |
170 | 0 | Array rhs(mesher->layout()->size()); |
171 | 0 | for (const auto& iter : *layout) { |
172 | 0 | Real k = mesher->location(iter, 0); |
173 | 0 | rhs[iter.index()] = std::max(forward_ - k, 0.0); |
174 | 0 | } |
175 | | |
176 | | // local vols (TODO how can we avoid these Array / vector copying?) |
177 | 0 | Array k = mesher->locations(0); |
178 | 0 | std::vector<Real> kv(k.size()); |
179 | 0 | std::copy(k.begin(), k.end(), kv.begin()); |
180 | 0 | std::vector<Real> locVolv = localVolatility(kv); |
181 | 0 | Array locVol(locVolv.size()); |
182 | 0 | std::copy(locVolv.begin(), locVolv.end(), locVol.begin()); |
183 | | |
184 | | // solver |
185 | 0 | auto map = |
186 | 0 | ext::make_shared<FdmDupire1dOp>(mesher, locVol); |
187 | 0 | FdmBackwardSolver solver(map, boundaries, |
188 | 0 | ext::shared_ptr<FdmStepConditionComposite>(), |
189 | 0 | FdmSchemeDesc::Douglas()); |
190 | 0 | solver.rollback(rhs, expiryTime_, 0.0, steps, dampingSteps); |
191 | | |
192 | | // interpolate solution |
193 | 0 | ext::shared_ptr<Interpolation> solution = ext::make_shared<CubicInterpolation>( |
194 | 0 | k.begin(), k.end(), rhs.begin(), CubicInterpolation::Spline, true, |
195 | 0 | CubicInterpolation::SecondDerivative, 0.0, |
196 | 0 | CubicInterpolation::SecondDerivative, 0.0); |
197 | | // ext::shared_ptr<Interpolation> solution = |
198 | | // ext::make_shared<LinearInterpolation>(k.begin(),k.end(),rhs.begin()); |
199 | 0 | solution->disableExtrapolation(); |
200 | 0 | std::vector<Real> result(strikes.size()); |
201 | 0 | std::transform(strikes.begin(), strikes.end(), result.begin(), *solution); |
202 | 0 | return result; |
203 | 0 | } |
204 | | |
205 | 0 | Real ZabrModel::fullFdPrice(const Real strike) const { |
206 | | |
207 | | // TODO what are good values here, still experimenting with them |
208 | 0 | Real eps = 0.01; |
209 | 0 | Real scaleFactor = 1.5; |
210 | 0 | Real normInvEps = InverseCumulativeNormal()(1.0 - eps); |
211 | 0 | Real alphaI = alpha_ * std::pow(forward_, beta_ - 1.0); |
212 | | // nu is already standardized within this class ... |
213 | 0 | Real v0 = alpha_ * std::exp(-scaleFactor * normInvEps * |
214 | 0 | std::sqrt(expiryTime_) * nu_); |
215 | 0 | Real v1 = alpha_ * |
216 | 0 | std::exp(scaleFactor * normInvEps * std::sqrt(expiryTime_) * nu_); |
217 | 0 | Real f0 = forward_ * std::exp(-scaleFactor * normInvEps * |
218 | 0 | std::sqrt(expiryTime_) * alphaI); |
219 | 0 | Real f1 = forward_ * std::exp(scaleFactor * normInvEps * |
220 | 0 | std::sqrt(expiryTime_) * alphaI); |
221 | 0 | v1 = std::min(v1, 2.0); |
222 | 0 | f0 = std::min(strike / 2.0, f0); |
223 | 0 | f1 = std::max(strike * 1.5, std::min(f1, std::max(2.0, strike * 1.5))); |
224 | |
|
225 | 0 | const Size sizef = 100; |
226 | 0 | const Size sizev = 100; |
227 | 0 | const Size steps = Size(24 * expiryTime_ + 1); |
228 | 0 | const Size dampingSteps = 5; |
229 | 0 | const Real densityf = 0.1; |
230 | 0 | const Real densityv = 0.1; |
231 | |
|
232 | 0 | QL_REQUIRE(strike >= f0 && strike <= f1, |
233 | 0 | "strike (" << strike << ") must be inside pde grid [" << f0 |
234 | 0 | << ";" << f1 << "]"); |
235 | | |
236 | | // Layout |
237 | 0 | std::vector<Size> dim; |
238 | 0 | dim.push_back(sizef); |
239 | 0 | dim.push_back(sizev); |
240 | 0 | const ext::shared_ptr<FdmLinearOpLayout> layout( |
241 | 0 | new FdmLinearOpLayout(dim)); |
242 | | |
243 | | // Mesher |
244 | | // two concentrating mesher around f and k to get the mesher for f |
245 | 0 | const Real x0 = std::min(forward_, strike); |
246 | 0 | const Real x1 = std::max(forward_, strike); |
247 | 0 | const Size sizefa = std::max<Size>( |
248 | 0 | 4, (Size)std::ceil(((x0 + x1) / 2.0 - f0) / (f1 - f0) * (Real)sizef)); |
249 | 0 | const Size sizefb = sizef - sizefa + 1; // common point, so we can spend |
250 | | // one more here |
251 | 0 | const ext::shared_ptr<Fdm1dMesher> mfa = |
252 | 0 | ext::make_shared<Concentrating1dMesher>( |
253 | 0 | f0, (x0 + x1) / 2.0, sizefa, std::pair<Real, Real>(x0, densityf), true); |
254 | 0 | const ext::shared_ptr<Fdm1dMesher> mfb = |
255 | 0 | ext::make_shared<Concentrating1dMesher>( |
256 | 0 | (x0 + x1) / 2.0, f1, sizefb, std::pair<Real, Real>(x1, densityf), true); |
257 | 0 | const ext::shared_ptr<Fdm1dMesher> mf = ext::make_shared<Glued1dMesher>(*mfa, *mfb); |
258 | | |
259 | | // concentraing mesher around f to get the forward mesher |
260 | | // const ext::shared_ptr<Fdm1dMesher> mf = |
261 | | // ext::make_shared<Concentrating1dMesher>( |
262 | | // f0, f1, sizef, std::pair<Real, Real>(forward_, densityf), true); |
263 | | |
264 | | // Volatility mesher |
265 | 0 | const ext::shared_ptr<Fdm1dMesher> mv = |
266 | 0 | ext::make_shared<Concentrating1dMesher>( |
267 | 0 | v0, v1, sizev, std::pair<Real, Real>(alpha_, densityv), true); |
268 | | |
269 | | // uniform meshers |
270 | | // const ext::shared_ptr<Fdm1dMesher> mf = |
271 | | // ext::make_shared<Uniform1dMesher>(f0,f1,sizef); |
272 | | // const ext::shared_ptr<Fdm1dMesher> mv = |
273 | | // ext::make_shared<Uniform1dMesher>(v0,v1,sizev); |
274 | |
|
275 | 0 | std::vector<ext::shared_ptr<Fdm1dMesher> > meshers; |
276 | 0 | meshers.push_back(mf); |
277 | 0 | meshers.push_back(mv); |
278 | 0 | const ext::shared_ptr<FdmMesher> mesher( |
279 | 0 | new FdmMesherComposite(layout, meshers)); |
280 | | |
281 | | // initial values |
282 | 0 | Array rhs(mesher->layout()->size()); |
283 | 0 | std::vector<Real> f_; |
284 | 0 | std::vector<Real> v_; |
285 | 0 | for (const auto& iter : *layout) { |
286 | 0 | Real f = mesher->location(iter, 0); |
287 | | // Real v = mesher->location(iter, 0); |
288 | 0 | rhs[iter.index()] = std::max(f - strike, 0.0); |
289 | 0 | if (iter.coordinates()[1] == 0U) |
290 | 0 | f_.push_back(mesher->location(iter, 0)); |
291 | 0 | if (iter.coordinates()[0] == 0U) |
292 | 0 | v_.push_back(mesher->location(iter, 1)); |
293 | 0 | } |
294 | | |
295 | | // Boundary conditions |
296 | 0 | FdmBoundaryConditionSet boundaries; |
297 | |
|
298 | 0 | ext::shared_ptr<FdmZabrOp> map( |
299 | 0 | new FdmZabrOp(mesher, beta_, nu_, rho_, gamma_)); |
300 | 0 | FdmBackwardSolver solver(map, boundaries, |
301 | 0 | ext::shared_ptr<FdmStepConditionComposite>(), |
302 | 0 | FdmSchemeDesc::/*CraigSneyd()*/ Hundsdorfer()); |
303 | |
|
304 | 0 | solver.rollback(rhs, expiryTime_, 0.0, steps, dampingSteps); |
305 | | |
306 | | // interpolate solution (this is not necessary when using concentrating |
307 | | // meshers with required point) |
308 | 0 | Matrix result(f_.size(), v_.size()); |
309 | 0 | for (Size j = 0; j < v_.size(); ++j) |
310 | 0 | std::copy(rhs.begin() + j * f_.size(), |
311 | 0 | rhs.begin() + (j + 1) * f_.size(), result.row_begin(j)); |
312 | 0 | ext::shared_ptr<BicubicSpline> interpolation = |
313 | 0 | ext::make_shared<BicubicSpline>( |
314 | 0 | f_.begin(), f_.end(), v_.begin(), v_.end(), result); |
315 | 0 | interpolation->disableExtrapolation(); |
316 | 0 | return (*interpolation)(forward_, alpha_); |
317 | 0 | } |
318 | | |
319 | 0 | Real ZabrModel::x(const Real strike) const { |
320 | 0 | return x(std::vector<Real>(1, strike))[0]; |
321 | 0 | } |
322 | | |
323 | 0 | std::vector<Real> ZabrModel::x(const std::vector<Real> &strikes) const { |
324 | 0 | QL_REQUIRE(!strikes.empty(), "ZabrModel::x(): empty strikes."); |
325 | 0 | QL_REQUIRE(strikes[0] > 0.0 || beta_ < 1.0, |
326 | 0 | "strikes must be positive (" << strikes[0] << ") if beta = 1"); |
327 | 0 | for (auto i = strikes.begin() + 1; i != strikes.end(); ++i) |
328 | 0 | QL_REQUIRE(*i > *(i - 1), "strikes must be strictly ascending (" |
329 | 0 | << *(i - 1) << "," << *i << ")"); |
330 | | |
331 | 0 | AdaptiveRungeKutta<Real> rk(1.0E-8, 1.0E-5, |
332 | 0 | 0.0); // TODO move the parameters here as |
333 | | // parameters with default values to |
334 | | // the constructor |
335 | 0 | std::vector<Real> y(strikes.size()), result(strikes.size()); |
336 | 0 | std::transform(strikes.rbegin(), strikes.rend(), y.begin(), |
337 | 0 | [&](Real _k) { return this->y(_k); }); |
338 | |
|
339 | 0 | if (close(gamma_, 1.0)) { |
340 | 0 | for (Size m = 0; m < y.size(); m++) { |
341 | 0 | Real J = std::sqrt(1.0 + nu_ * nu_ * y[m] * y[m] - |
342 | 0 | 2.0 * rho_ * nu_ * y[m]); |
343 | 0 | result[y.size() - 1 - m] = |
344 | 0 | std::log((J + nu_ * y[m] - rho_) / (1.0 - rho_)) / nu_; |
345 | 0 | } |
346 | 0 | } else { |
347 | 0 | int ynz = static_cast<int>(std::upper_bound(y.begin(), y.end(), 0.0) - y.begin()); |
348 | 0 | if (ynz > 0) |
349 | 0 | if (close(y[ynz - 1], 0.0)) |
350 | 0 | ynz--; |
351 | 0 | if (ynz == static_cast<int>(y.size())) |
352 | 0 | ynz--; |
353 | |
|
354 | 0 | for (int dir = 1; dir >= -1; dir -= 2) { |
355 | 0 | Real y0 = 0.0, u0 = 0.0; |
356 | 0 | for (int m = ynz + (dir == -1 ? -1 : 0); |
357 | 0 | dir == -1 ? m >= 0 : m < (int)y.size(); m += dir) { |
358 | 0 | Real u = rk([&](Real _y, Real _u){ return F(_y, _u); }, |
359 | 0 | u0, y0, y[m]); |
360 | 0 | result[y.size() - 1 - m] = u * pow(alpha_, 1.0 - gamma_); |
361 | 0 | u0 = u; |
362 | 0 | y0 = y[m]; |
363 | 0 | } |
364 | 0 | } |
365 | 0 | } |
366 | |
|
367 | 0 | return result; |
368 | 0 | } |
369 | | |
370 | 0 | Real ZabrModel::y(const Real strike) const { |
371 | |
|
372 | 0 | if (close(beta_, 1.0)) { |
373 | 0 | return std::log(forward_ / strike) * std::pow(alpha_, gamma_ - 2.0); |
374 | 0 | } else { |
375 | 0 | return (strike < 0.0 |
376 | 0 | ? Real(std::pow(forward_, 1.0 - beta_) + |
377 | 0 | std::pow(-strike, 1.0 - beta_)) |
378 | 0 | : Real(std::pow(forward_, 1.0 - beta_) - |
379 | 0 | std::pow(strike, 1.0 - beta_))) * |
380 | 0 | std::pow(alpha_, gamma_ - 2.0) / (1.0 - beta_); |
381 | 0 | } |
382 | 0 | } |
383 | | |
384 | 0 | Real ZabrModel::F(const Real y, const Real u) const { |
385 | 0 | Real A = 1.0 + (gamma_ - 2.0) * (gamma_ - 2.0) * nu_ * nu_ * y * y + |
386 | 0 | 2.0 * rho_ * (gamma_ - 2.0) * nu_ * y; |
387 | 0 | Real B = 2.0 * rho_ * (1.0 - gamma_) * nu_ + |
388 | 0 | 2.0 * (1.0 - gamma_) * (gamma_ - 2.0) * nu_ * nu_ * y; |
389 | 0 | Real C = (1.0 - gamma_) * (1.0 - gamma_) * nu_ * nu_; |
390 | 0 | return (-B * u + std::sqrt(B * B * u * u - 4.0 * A * (C * u * u - 1.0))) / |
391 | 0 | (2.0 * A); |
392 | 0 | } |
393 | | } |