Coverage Report

Created: 2025-11-04 06:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/experimental/volatility/noarbsabr.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
6
 This file is part of QuantLib, a free-software/open-source library
7
 for financial quantitative analysts and developers - http://quantlib.org/
8
9
 QuantLib is free software: you can redistribute it and/or modify it
10
 under the terms of the QuantLib license.  You should have received a
11
 copy of the license along with this program; if not, please email
12
 <quantlib-dev@lists.sf.net>. The license is also available online at
13
 <https://www.quantlib.org/license.shtml>.
14
15
 This program is distributed in the hope that it will be useful, but WITHOUT
16
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
 FOR A PARTICULAR PURPOSE.  See the license for more details.
18
*/
19
20
#include <ql/experimental/volatility/noarbsabr.hpp>
21
22
#include <ql/math/solvers1d/brent.hpp>
23
#include <ql/math/modifiedbessel.hpp>
24
#include <boost/math/special_functions/gamma.hpp>
25
#include <boost/functional/hash.hpp>
26
27
namespace QuantLib {
28
29
class NoArbSabrModel::integrand {
30
    const NoArbSabrModel* model;
31
    Real strike;
32
  public:
33
    integrand(const NoArbSabrModel* model, Real strike)
34
0
    : model(model), strike(strike) {}
35
0
    Real operator()(Real f) const {
36
0
        return std::max(f - strike, 0.0) * model->p(f);
37
0
    }
38
};
39
40
class NoArbSabrModel::p_integrand {
41
    const NoArbSabrModel* model;
42
  public:
43
    explicit p_integrand(const NoArbSabrModel* model)
44
0
    : model(model) {}
45
0
    Real operator()(Real f) const {
46
0
        return model->p(f);
47
0
    }
48
};
49
50
NoArbSabrModel::NoArbSabrModel(const Real expiryTime, const Real forward,
51
                               const Real alpha, const Real beta, const Real nu,
52
                               const Real rho)
53
0
    : expiryTime_(expiryTime), externalForward_(forward), alpha_(alpha),
54
0
      beta_(beta), nu_(nu), rho_(rho), forward_(forward),
55
0
      numericalForward_(forward) {
56
57
0
    QL_REQUIRE(expiryTime > 0.0 && expiryTime <= detail::NoArbSabrModel::expiryTime_max,
58
0
               "expiryTime (" << expiryTime << ") out of bounds");
59
0
    QL_REQUIRE(forward > 0.0, "forward (" << forward << ") must be positive");
60
0
    QL_REQUIRE(beta >= detail::NoArbSabrModel::beta_min && beta <= detail::NoArbSabrModel::beta_max,
61
0
               "beta (" << beta << ") out of bounds");
62
0
    Real sigmaI = alpha * std::pow(forward, beta - 1.0);
63
0
    QL_REQUIRE(sigmaI >= detail::NoArbSabrModel::sigmaI_min &&
64
0
                   sigmaI <= detail::NoArbSabrModel::sigmaI_max,
65
0
               "sigmaI = alpha*forward^(beta-1.0) ("
66
0
                   << sigmaI << ") out of bounds, alpha=" << alpha
67
0
                   << " beta=" << beta << " forward=" << forward);
68
0
    QL_REQUIRE(nu >= detail::NoArbSabrModel::nu_min && nu <= detail::NoArbSabrModel::nu_max,
69
0
               "nu (" << nu << ") out of bounds");
70
0
    QL_REQUIRE(rho >= detail::NoArbSabrModel::rho_min && rho <= detail::NoArbSabrModel::rho_max,
71
0
               "rho (" << rho << ") out of bounds");
72
73
    // determine a region sufficient for integration in the normal case
74
75
0
    fmin_ = fmax_ = forward_;
76
0
    for (Real tmp = p(fmax_);
77
0
         tmp > std::max(detail::NoArbSabrModel::i_accuracy / std::max(1.0, fmax_ - fmin_),
78
0
                        detail::NoArbSabrModel::density_threshold);
79
0
         tmp = p(fmax_)) {
80
0
        fmax_ *= 2.0;
81
0
    }
82
0
    for (Real tmp = p(fmin_);
83
0
         tmp > std::max(detail::NoArbSabrModel::i_accuracy / std::max(1.0, fmax_ - fmin_),
84
0
                        detail::NoArbSabrModel::density_threshold);
85
0
         tmp = p(fmin_)) {
86
0
        fmin_ *= 0.5;
87
0
    }
88
0
    fmin_ = std::max(detail::NoArbSabrModel::strike_min, fmin_);
89
90
0
    QL_REQUIRE(fmax_ > fmin_, "could not find a reasonable integration domain");
91
92
0
    integrator_ =
93
0
        ext::make_shared<GaussLobattoIntegral>(
94
0
            detail::NoArbSabrModel::i_max_iterations, detail::NoArbSabrModel::i_accuracy);
95
96
0
    detail::D0Interpolator d0(forward_, expiryTime_, alpha_, beta_, nu_, rho_);
97
0
    absProb_ = d0();
98
99
0
    try {
100
0
        Brent b;
101
0
        Real start = std::sqrt(externalForward_ - detail::NoArbSabrModel::strike_min);
102
0
        Real tmp =
103
0
            b.solve([&](Real x){ return forwardError(x); },
104
0
                    detail::NoArbSabrModel::forward_accuracy, start,
105
0
                    std::min(detail::NoArbSabrModel::forward_search_step, start / 2.0));
106
0
        forward_ = tmp * tmp + detail::NoArbSabrModel::strike_min;
107
0
    } catch (Error&) {
108
        // fall back to unadjusted forward
109
0
        forward_ = externalForward_;
110
0
    }
111
112
0
    Real d = forwardError(std::sqrt(forward_ - detail::NoArbSabrModel::strike_min));
113
0
    numericalForward_ = d + externalForward_;
114
0
}
115
116
0
Real NoArbSabrModel::optionPrice(const Real strike) const {
117
0
    if (p(std::max(forward_, strike)) < detail::NoArbSabrModel::density_threshold)
118
0
        return 0.0;
119
0
    return (1.0 - absProb_) *
120
0
        ((*integrator_)(integrand(this, strike),
121
0
                        strike, std::max(fmax_, 2.0 * strike)) /
122
0
            numericalIntegralOverP_);
123
0
}
124
125
0
Real NoArbSabrModel::digitalOptionPrice(const Real strike) const {
126
0
    if (strike < QL_MIN_POSITIVE_REAL)
127
0
        return 1.0;
128
0
    if (p(std::max(forward_, strike)) < detail::NoArbSabrModel::density_threshold)
129
0
        return 0.0;
130
0
    return (1.0 - absProb_)
131
0
        * ((*integrator_)(p_integrand(this),
132
0
                          strike, std::max(fmax_, 2.0 * strike)) /
133
0
           numericalIntegralOverP_);
134
0
}
135
136
0
Real NoArbSabrModel::forwardError(const Real forward) const {
137
0
    forward_ = forward * forward + detail::NoArbSabrModel::strike_min;
138
0
    numericalIntegralOverP_ = (*integrator_)(p_integrand(this),
139
0
                                             fmin_, fmax_);
140
0
    return optionPrice(0.0) - externalForward_;
141
0
}
142
143
0
Real NoArbSabrModel::p(const Real f) const {
144
145
0
    if (f < detail::NoArbSabrModel::density_lower_bound ||
146
0
        forward_ < detail::NoArbSabrModel::density_lower_bound)
147
0
        return 0.0;
148
149
0
    Real fOmB = std::pow(f, 1.0 - beta_);
150
0
    Real FOmB = std::pow(forward_, 1.0 - beta_);
151
152
0
    Real zf = fOmB / (alpha_ * (1.0 - beta_));
153
0
    Real zF = FOmB / (alpha_ * (1.0 - beta_));
154
0
    Real z = zF - zf;
155
156
    // Real JzF = std::sqrt(1.0 - 2.0 * rho_ * nu_ * zF + nu_ * nu_ * zF * zF);
157
0
    Real Jmzf = std::sqrt(1.0 + 2.0 * rho_ * nu_ * zf + nu_ * nu_ * zf * zf);
158
0
    Real Jz = std::sqrt(1.0 - 2.0 * rho_ * nu_ * z + nu_ * nu_ * z * z);
159
160
0
    Real xz = std::log((Jz - rho_ + nu_ * z) / (1.0 - rho_)) / nu_;
161
0
    Real Bp_B = beta_ / FOmB;
162
    // Real Bpp_B = beta_ * (2.0 * beta_ - 1.0) / (FOmB * FOmB);
163
0
    Real kappa1 = 0.125 * nu_ * nu_ * (2.0 - 3.0 * rho_ * rho_) -
164
0
                  0.25 * rho_ * nu_ * alpha_ * Bp_B;
165
    // Real kappa2 = alpha_ * alpha_ * (0.25 * Bpp_B - 0.375 * Bp_B * Bp_B);
166
0
    Real gamma = 1.0 / (2.0 * (1.0 - beta_));
167
0
    Real sqrtOmR = std::sqrt(1.0 - rho_ * rho_);
168
0
    Real h = 0.5 * beta_ * rho_ / ((1.0 - beta_) * Jmzf * Jmzf) *
169
0
             (nu_ * zf * std::log(zf * Jz / zF) +
170
0
              (1 + rho_ * nu_ * zf) / sqrtOmR *
171
0
                  (std::atan((nu_ * z - rho_) / sqrtOmR) +
172
0
                   std::atan(rho_ / sqrtOmR)));
173
174
0
    Real res =
175
0
        std::pow(Jz, -1.5) / (alpha_ * std::pow(f, beta_) * expiryTime_) *
176
0
        std::pow(zf, 1.0 - gamma) * std::pow(zF, gamma) *
177
0
        std::exp(-(xz * xz) / (2.0 * expiryTime_) +
178
0
                 (h + kappa1 * expiryTime_)) *
179
0
        modifiedBesselFunction_i_exponentiallyWeighted(gamma,
180
0
                                                       Real(zF * zf / expiryTime_));
181
0
    return res;
182
0
}
183
184
namespace detail {
185
186
D0Interpolator::D0Interpolator(const Real forward, const Real expiryTime,
187
                               const Real alpha, const Real beta, const Real nu,
188
                               const Real rho)
189
0
    : forward_(forward), expiryTime_(expiryTime), alpha_(alpha), beta_(beta),
190
0
      nu_(nu), rho_(rho), gamma_(1.0 / (2.0 * (1.0 - beta_))) {
191
192
0
    sigmaI_ = alpha_ * std::pow(forward_, beta_ - 1.0);
193
194
0
    tauG_ = {
195
0
        0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0,
196
0
        3.25, 3.5, 3.75, 4.0, 4.25, 4.5, 4.75, 5.0, 5.25, 5.5, 5.75, 6.0, 6.25,
197
0
        6.5, 6.75, 7.0, 7.25, 7.5, 7.75, 8.0, 8.25, 8.5, 8.75, 9.0, 9.25, 9.5,
198
0
        9.75, 10.0, 10.25, 10.5, 10.75, 11.0, 11.25, 11.5, 11.75, 12.0, 12.25,
199
0
        12.5, 12.75, 13.0, 13.25, 13.5, 13.75, 14.0, 14.25, 14.5, 14.75, 15.0,
200
0
        15.25, 15.5, 15.75, 16.0, 16.25, 16.5, 16.75, 17.0, 17.25, 17.5, 17.75,
201
0
        18.0, 18.25, 18.5, 18.75, 19.0, 19.25, 19.5, 19.75, 20.0, 20.25, 20.5,
202
0
        20.75, 21.0, 21.25, 21.5, 21.75, 22.0, 22.25, 22.5, 22.75, 23.0, 23.25,
203
0
        23.5, 23.75, 24.0, 24.25, 24.5, 24.75, 25.0, 25.25, 25.5, 25.75, 26.0,
204
0
        26.25, 26.5, 26.75, 27.0, 27.25, 27.5, 27.75, 28.0, 28.25, 28.5, 28.75,
205
0
        29.0, 29.25, 29.5, 29.75, 30.0
206
0
    };
207
208
0
    sigmaIG_ = {
209
0
        1.0, 0.8, 0.7, 0.6, 0.5, 0.45, 0.4, 0.35, 0.3, 0.27, 0.24, 0.21,
210
0
        0.18, 0.15, 0.125, 0.1, 0.075, 0.05
211
0
    };
212
213
0
    rhoG_ = { 0.75, 0.50, 0.25, 0.00, -0.25, -0.50, -0.75 };
214
215
0
    nuG_ = { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8 };
216
217
0
    betaG_ = { 0.01, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9 };
218
0
}
219
220
0
Real D0Interpolator::operator()() const {
221
222
    // we do not need to check the indices here, because this is already
223
    // done in the NoArbSabr constructor
224
225
0
    Size tauInd = std::upper_bound(tauG_.begin(), tauG_.end(), expiryTime_) -
226
0
                                   tauG_.begin();
227
0
    if (tauInd == tauG_.size())
228
0
        --tauInd; // tau at upper bound
229
0
    Real expiryTimeTmp = expiryTime_;
230
0
    if (tauInd == 0) {
231
0
        ++tauInd;
232
0
        expiryTimeTmp = tauG_.front();
233
0
    }
234
0
    Real tauL = (expiryTimeTmp - tauG_[tauInd - 1]) /
235
0
                (tauG_[tauInd] - tauG_[tauInd - 1]);
236
237
0
    Size sigmaIInd =
238
0
        sigmaIG_.size() -
239
0
        (std::upper_bound(sigmaIG_.rbegin(), sigmaIG_.rend(), sigmaI_) -
240
0
         sigmaIG_.rbegin());
241
0
    if (sigmaIInd == 0)
242
0
        ++sigmaIInd; // sigmaI at upper bound
243
0
    Real sigmaIL = (sigmaI_ - sigmaIG_[sigmaIInd - 1]) /
244
0
                   (sigmaIG_[sigmaIInd] - sigmaIG_[sigmaIInd - 1]);
245
246
0
    Size rhoInd =
247
0
        rhoG_.size() -
248
0
        (std::upper_bound(rhoG_.rbegin(), rhoG_.rend(), rho_) - rhoG_.rbegin());
249
0
    if (rhoInd == 0) {
250
0
        rhoInd++;
251
0
    }
252
0
    if (rhoInd == rhoG_.size()) {
253
0
        rhoInd--;
254
0
    }
255
0
    Real rhoL =
256
0
        (rho_ - rhoG_[rhoInd - 1]) / (rhoG_[rhoInd] - rhoG_[rhoInd - 1]);
257
258
    // for nu = 0 we know phi = 0.5*z_F^2
259
0
    Size nuInd = std::upper_bound(nuG_.begin(), nuG_.end(), nu_) - nuG_.begin();
260
0
    if (nuInd == nuG_.size())
261
0
        --nuInd; // nu at upper bound
262
0
    Real tmpNuG = nuInd > 0 ? nuG_[nuInd - 1] : 0.0;
263
0
    Real nuL = (nu_ - tmpNuG) / (nuG_[nuInd] - tmpNuG);
264
265
    // for beta = 1 we know phi = 0.0
266
0
    Size betaInd =
267
0
        std::upper_bound(betaG_.begin(), betaG_.end(), beta_) - betaG_.begin();
268
0
    Real tmpBetaG;
269
0
    if (betaInd == betaG_.size())
270
0
        tmpBetaG = 1.0;
271
0
    else
272
0
        tmpBetaG = betaG_[betaInd];
273
0
    Real betaL =
274
0
        (beta_ - betaG_[betaInd - 1]) / (tmpBetaG - betaG_[betaInd - 1]);
275
276
0
    Real phiRes = 0.0;
277
0
    for (int iTau = -1; iTau <= 0; ++iTau) {
278
0
        for (int iSigma = -1; iSigma <= 0; ++iSigma) {
279
0
            for (int iRho = -1; iRho <= 0; ++iRho) {
280
0
                for (int iNu = -1; iNu <= 0; ++iNu) {
281
0
                    for (int iBeta = -1; iBeta <= 0; ++iBeta) {
282
0
                        Real phiTmp;
283
0
                        if (iNu == -1 && nuInd == 0) {
284
0
                            phiTmp =
285
0
                                0.5 /
286
0
                                (sigmaI_ * sigmaI_ * (1.0 - beta_) *
287
0
                                 (1.0 - beta_)); // this is 0.5*z_F^2, see above
288
0
                        } else {
289
0
                            if (iBeta == 0 && betaInd == betaG_.size()) {
290
0
                                phiTmp =
291
0
                                    phi(detail::NoArbSabrModel::tiny_prob);
292
0
                            } else {
293
0
                                int ind = (tauInd + iTau +
294
0
                                           (sigmaIInd + iSigma +
295
0
                                            (rhoInd + iRho +
296
0
                                             (nuInd + iNu + ((betaInd + iBeta) *
297
0
                                                             nuG_.size())) *
298
0
                                                 rhoG_.size()) *
299
0
                                                sigmaIG_.size()) *
300
0
                                               tauG_.size());
301
0
                                QL_REQUIRE(ind >= 0 && ind < 1209600,
302
0
                                           "absorption matrix index ("
303
0
                                               << ind << ") invalid");
304
0
                                phiTmp = phi((Real)sabrabsprob[ind] /
305
0
                                             detail::NoArbSabrModel::nsim);
306
0
                            }
307
0
                        }
308
0
                        phiRes += phiTmp * (iTau == -1 ? (1.0 - tauL) : tauL) *
309
0
                                  (iSigma == -1 ? (1.0 - sigmaIL) : sigmaIL) *
310
0
                                  (iRho == -1 ? (1.0 - rhoL) : rhoL) *
311
0
                                  (iNu == -1 ? (1.0 - nuL) : nuL) *
312
0
                                  (iBeta == -1 ? (1.0 - betaL) : betaL);
313
0
                    }
314
0
                }
315
0
            }
316
0
        }
317
0
    }
318
0
    return d0(phiRes);
319
0
}
320
321
0
Real D0Interpolator::phi(const Real d0) const {
322
0
    if (d0 < 1e-14)
323
0
        return detail::NoArbSabrModel::phiByTau_cutoff * expiryTime_;
324
0
    return boost::math::gamma_q_inv(gamma_, d0) * expiryTime_;
325
0
}
326
327
0
Real D0Interpolator::d0(const Real phi) const {
328
0
    return boost::math::gamma_q(gamma_, std::max(0.0, phi / expiryTime_));
329
0
}
330
331
} // namespace detail
332
333
} // namespace QuantLib