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