Coverage Report

Created: 2026-06-23 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}