Coverage Report

Created: 2026-10-31 06:47

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
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
}