Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
blackvolsurfacebfrr.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2021 Quaternion Risk Management Ltd
3 All rights reserved.
4
5 This file is part of ORE, a free-software/open-source library
6 for transparent pricing and risk analysis - http://opensourcerisk.org
7
8 ORE is free software: you can redistribute it and/or modify it
9 under the terms of the Modified BSD License. You should have received a
10 copy of the license along with this program.
11 The license is also available online at <http://opensourcerisk.org>
12
13 This program is distributed on the basis that it will form a useful
14 contribution to risk analytics and model standardisation, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
20
21#include <ql/experimental/fx/blackdeltacalculator.hpp>
22#include <ql/math/comparison.hpp>
23#include <ql/math/interpolations/cubicinterpolation.hpp>
24#include <ql/math/interpolations/linearinterpolation.hpp>
25#include <ql/math/optimization/levenbergmarquardt.hpp>
26#include <ql/pricingengines/blackformula.hpp>
27
28namespace QuantExt {
29
30namespace detail {
31
32Real transformVol(const Real v) { return std::log(v); }
33Real untransformVol(const Real w) { return std::exp(w); }
34
36 const Real spot, const Real domDisc, const Real forDisc, const Real expiryTime, const std::vector<Real>& deltas,
37 const std::vector<Real>& putVols, const std::vector<Real>& callVols, const Real atmVol,
38 const DeltaVolQuote::DeltaType dt, const DeltaVolQuote::AtmType at,
39 const BlackVolatilitySurfaceBFRR::SmileInterpolation smileInterpolation, const Real accuracy,
40 const Size maxIterations)
41 : spot_(spot), domDisc_(domDisc), forDisc_(forDisc), expiryTime_(expiryTime), deltas_(deltas), putVols_(putVols),
42 callVols_(callVols), atmVol_(atmVol), dt_(dt), at_(at), smileInterpolation_(smileInterpolation),
43 accuracy_(accuracy), maxIterations_(maxIterations) {
44
46
47 std::vector<Real> x, y;
48
49 /* Convert the puts, atm, calls to strikes using the given conventions and then to simple delta
50 using the given conventions. Store the simple deltas as x-values for the interpolation and
51 the log of the vols as y-values */
52
53 for (Size i = 0; i < deltas_.size(); ++i) {
54 try {
55 BlackDeltaCalculator c(Option::Put, dt_, spot_, domDisc_, forDisc_, putVols_[i] * std::sqrt(expiryTime_));
56 x.push_back(simpleDeltaFromStrike(c.strikeFromDelta(-deltas[i])));
57 y.push_back(transformVol(putVols_[i]));
58 } catch (const std::exception& e) {
59 QL_FAIL("SimpleDeltaInterpolatedSmile: strikeFromDelta("
60 << -deltas[i] << ") could not be computed for spot=" << spot_
61 << ", forward=" << spot_ / domDisc_ * forDisc_ << " (domRate=" << -std::log(domDisc_) / expiryTime_
62 << ", forRate=" << -std::log(forDisc) / expiryTime_ << "), putVol=" << putVols_[i]
63 << ", expiry=" << expiryTime_ << ": " << e.what());
64 }
65 }
66
67 try {
68 BlackDeltaCalculator c(Option::Call, dt_, spot_, domDisc_, forDisc_, atmVol * std::sqrt(expiryTime_));
69 x.push_back(simpleDeltaFromStrike(c.atmStrike(at_)));
70 y.push_back(transformVol(atmVol_));
71 } catch (const std::exception& e) {
72 QL_FAIL("SimpleDeltaIinterpolatedSmile: atmStrike could not be computed for spot="
73 << spot_ << ", forward=" << spot_ / domDisc_ * forDisc_
74 << " (domRate=" << -std::log(domDisc_) / expiryTime_ << ", forRate=" << -std::log(forDisc) / expiryTime_
75 << "), atmVol=" << atmVol << ", expiry=" << expiryTime_ << ": " << e.what());
76 }
77
78 for (Size i = deltas_.size(); i > 0; --i) {
79 try {
80 BlackDeltaCalculator c(Option::Call, dt_, spot_, domDisc_, forDisc_,
81 callVols_[i - 1] * std::sqrt(expiryTime_));
82 x.push_back(simpleDeltaFromStrike(c.strikeFromDelta(deltas[i - 1])));
83 y.push_back(transformVol(callVols_[i - 1]));
84 } catch (const std::exception& e) {
85 QL_FAIL("SimpleDeltaInterpolatedSmile: strikeFromDelta("
86 << deltas[i - 1] << ") could not be computed for spot=" << spot_
87 << ", forward=" << spot_ / domDisc_ * forDisc_ << " (domRate=" << -std::log(domDisc_) / expiryTime_
88 << ", forRate=" << -std::log(forDisc) / expiryTime_ << "), callVol=" << callVols_[i - 1]
89 << ", expiry=" << expiryTime_ << ": " << e.what());
90 }
91 }
92
93 /* sort the strikes */
94
95 std::vector<Size> perm(x.size());
96 std::iota(perm.begin(), perm.end(), 0);
97 std::sort(perm.begin(), perm.end(), [&x](Size a, Size b) { return x[a] < x[b]; });
98 for (Size i = 0; i < perm.size(); ++i) {
99 x_.push_back(x[perm[i]]);
100 y_.push_back(y[perm[i]]);
101 }
102
103 /* check the strikes are not (numerically) identical */
104
105 for (Size i = 0; i < x_.size() - 1; ++i) {
106 QL_REQUIRE(!close_enough(x_[i], x_[i + 1]), "SmileDeltaInterpolatedSmile: interpolation points x["
107 << i << "] = x[" << (i + 1) << "] = " << x_[i]
108 << " are numerically identical.");
109 }
110
111 /* Create the interpolation object */
112
114 interpolation_ = QuantLib::ext::make_shared<LinearInterpolation>(x_.begin(), x_.end(), y_.begin());
116 interpolation_ = QuantLib::ext::make_shared<CubicInterpolation>(
117 x_.begin(), x_.end(), y_.begin(), CubicInterpolation::Spline, false, CubicInterpolation::SecondDerivative,
118 0.0, CubicInterpolation::SecondDerivative, 0.0);
119 } else {
120 QL_FAIL("invalid interpolation, this is unexpected");
121 }
122
123 interpolation_->enableExtrapolation();
124}
125
126Real SimpleDeltaInterpolatedSmile::strikeFromDelta(const Option::Type type, const Real delta,
127 const DeltaVolQuote::DeltaType dt) {
128 Real result = forward_, lastResult;
129 Size iterations = 0;
130 do {
131 Real stddev = std::sqrt(expiryTime_) * volatility(result);
132 try {
133 BlackDeltaCalculator c(type, dt, spot_, domDisc_, forDisc_, stddev);
134 lastResult = result;
135 result = c.strikeFromDelta((type == Option::Call ? 1.0 : -1.0) * delta);
136 } catch (const std::exception& e) {
137 QL_FAIL("SimpleDeltaInterpolatedSmile::strikeFromDelta("
138 << (type == Option::Call ? 1.0 : -1.0) * delta << ") could not be computed for spot=" << spot_
139 << ", forward=" << spot_ / domDisc_ * forDisc_ << " (domRate=" << -std::log(domDisc_) / expiryTime_
140 << ", forRate=" << -std::log(forDisc_) / expiryTime_ << "), vol=" << stddev / std::sqrt(expiryTime_)
141 << ", expiry=" << expiryTime_ << ": " << e.what());
142 }
143 } while (std::abs((result - lastResult) / lastResult) > accuracy_ && ++iterations < maxIterations_);
144 QL_REQUIRE(iterations < maxIterations_,
145 "SmileDeltaInterpolatedSmile::strikeFromDelta("
146 << (type == Option::Call ? 1.0 : -1.0) * delta << "): max iterations (" << maxIterations_
147 << "), no solution found for accuracy " << accuracy_ << ", last iterations: " << lastResult << "/"
148 << result << ", spot=" << spot_ << ", forward=" << spot_ / domDisc_ * forDisc_
149 << " (domRate=" << -std::log(domDisc_) / expiryTime_
150 << ", forRate=" << -std::log(forDisc_) / expiryTime_ << "), expiry=" << expiryTime_);
151 return result;
152}
153
154Real SimpleDeltaInterpolatedSmile::atmStrike(const DeltaVolQuote::DeltaType dt, const DeltaVolQuote::AtmType at) {
155 Real result = forward_, lastResult;
156 Size iterations = 0;
157 do {
158 Real stddev = std::sqrt(expiryTime_) * volatility(result);
159 try {
160 BlackDeltaCalculator c(Option::Call, dt, spot_, domDisc_, forDisc_, stddev);
161 lastResult = result;
162 result = c.atmStrike(at);
163 } catch (const std::exception& e) {
164 QL_FAIL("SimpleDeltaInterpolatedSmile::atmStrike() could not be computed for spot="
165 << spot_ << ", forward=" << spot_ / domDisc_ * forDisc_ << " (domRate="
166 << -std::log(domDisc_) / expiryTime_ << ", forRate=" << -std::log(forDisc_) / expiryTime_
167 << "), vol=" << stddev / std::sqrt(expiryTime_) << ", expiry=" << expiryTime_ << ": " << e.what());
168 }
169 } while (std::abs((result - lastResult) / lastResult) > accuracy_ && ++iterations < maxIterations_);
170 QL_REQUIRE(iterations < maxIterations_,
171 "SmileDeltaInterpolatedSmile::atmStrike(): max iterations ("
172 << maxIterations_ << "), no solution found for accuracy " << accuracy_
173 << ", last iterations: " << lastResult << "/" << result << ", spot=" << spot_
174 << ", forward=" << spot_ / domDisc_ * forDisc_ << " (domRate=" << -std::log(domDisc_) / expiryTime_
175 << ", forRate=" << -std::log(forDisc_) / expiryTime_ << "), expiry=" << expiryTime_);
176 return result;
177}
178
180 Real tmp = untransformVol((*interpolation_)(simpleDelta));
181 QL_REQUIRE(std::isfinite(tmp), "SimpleDeltaInterpolatedSmile::volatilityAtSimpleDelta() non-finite result ("
182 << tmp << ") for simple delta " << simpleDelta);
183 return tmp;
184}
185
188 if (!std::isfinite(tmp)) {
189 std::ostringstream os;
190 for (Size i = 0; i < x_.size(); ++i) {
191 os << "(" << x_[i] << "," << y_[i] << ")";
192 }
193 QL_FAIL("SimpleDeltaInterpolatedSmile::volatility() non-finite result ("
194 << tmp << ") for strike " << strike << ", simple delta is " << simpleDeltaFromStrike(strike)
195 << ", interpolated value is " << (*interpolation_)(simpleDeltaFromStrike(strike))
196 << ", interpolation data point are " << os.str());
197 }
198 return tmp;
199}
200
202 if (close_enough(strike, 0.0))
203 return 0.0;
204 CumulativeNormalDistribution Phi;
205 return Phi(std::log(strike / forward_) / (atmVol_ * std::sqrt(expiryTime_)));
206}
207
208QuantLib::ext::shared_ptr<SimpleDeltaInterpolatedSmile>
209createSmile(const Real spot, const Real domDisc, const Real forDisc, const Real expiryTime,
210 const std::vector<Real>& deltas, const std::vector<Real>& bfQuotes, const std::vector<Real>& rrQuotes,
211 const Real atmVol, const DeltaVolQuote::DeltaType dt, const DeltaVolQuote::AtmType at,
212 const Option::Type riskReversalInFavorOf, const bool butterflyIsBrokerStyle,
213 const BlackVolatilitySurfaceBFRR::SmileInterpolation smileInterpolation) {
214
215 Real phirr = riskReversalInFavorOf == Option::Call ? 1.0 : -1.0;
216 QuantLib::ext::shared_ptr<SimpleDeltaInterpolatedSmile> resultSmile;
217
218 if (!butterflyIsBrokerStyle) {
219
220 // butterfly is not broker style: we can directly compute the call/put vols ...
221
222 std::vector<Real> vol_p, vol_c;
223
224 for (Size i = 0; i < deltas.size(); ++i) {
225 QL_REQUIRE(atmVol + bfQuotes[i] - 0.5 * std::abs(rrQuotes[i]) > 0.0,
226 "createSmile: atmVol ("
227 << atmVol << ") + bf (" << bfQuotes[i] << ") - rr (" << rrQuotes[i]
228 << ") must be positive when creating smile from smile bf quotes, tte=" << expiryTime);
229 vol_p.push_back(atmVol + bfQuotes[i] - 0.5 * phirr * rrQuotes[i]);
230 vol_c.push_back(atmVol + bfQuotes[i] + 0.5 * phirr * rrQuotes[i]);
231 }
232
233 // ... and set up the interpolated smile
234
235 resultSmile = QuantLib::ext::make_shared<SimpleDeltaInterpolatedSmile>(
236 spot, domDisc, forDisc, expiryTime, deltas, vol_p, vol_c, atmVol, dt, at, smileInterpolation);
237
238 } else {
239
240 Real forward = spot / domDisc * forDisc;
241
242 /* handle broker style butterflys: first determine the strikes and the (non-discounted) premiums
243 of the broker butterflies */
244
245 std::vector<Real> kb_c, kb_p, vb;
246
247 for (Size i = 0; i < deltas.size(); ++i) {
248 Real stddevb = (atmVol + bfQuotes[i]) * std::sqrt(expiryTime);
249 QL_REQUIRE(stddevb > 0.0,
250 "createSmile: atmVol ("
251 << atmVol << ") + bf (" << bfQuotes[i]
252 << ") must be positive when creating smile from broker bf quotes, tte=" << expiryTime);
253 BlackDeltaCalculator cp(Option::Type::Put, dt, spot, domDisc, forDisc, stddevb);
254 BlackDeltaCalculator cc(Option::Type::Call, dt, spot, domDisc, forDisc, stddevb);
255 kb_p.push_back(cp.strikeFromDelta(-deltas[i]));
256 kb_c.push_back(cc.strikeFromDelta(deltas[i]));
257 vb.push_back(blackFormula(Option::Put, kb_p.back(), forward, stddevb) +
258 blackFormula(Option::Call, kb_c.back(), forward, stddevb));
259 }
260
261 /* set initial guess for smile butterfly vol := broker butterfly vol
262 we optimise in z = log( bf - 0.5 * abs(rr) + atmVol ) */
263
264 Array guess(deltas.size());
265 for (Size i = 0; i < deltas.size(); ++i) {
266 guess[i] = std::log(std::max(0.0001, bfQuotes[i] - 0.5 * std::abs(rrQuotes[i]) + atmVol));
267 }
268
269 /* define the target function to match up the butterfly market values and smile values */
270
271 struct TargetFunction : public QuantLib::CostFunction {
272 TargetFunction(Real atmVol, Real phirr, Real spot, Real domDisc, Real forDisc, Real forward,
273 Real expiryTime, DeltaVolQuote::DeltaType dt, DeltaVolQuote::AtmType at,
274 const std::vector<Real>& rrQuotes, const std::vector<Real>& deltas,
275 const std::vector<Real>& kb_p, const std::vector<Real>& kb_c, const std::vector<Real>& vb,
277 : atmVol(atmVol), phirr(phirr), spot(spot), domDisc(domDisc), forDisc(forDisc), forward(forward),
278 expiryTime(expiryTime), dt(dt), at(at), rrQuotes(rrQuotes), deltas(deltas), kb_p(kb_p), kb_c(kb_c),
279 vb(vb), smileInterpolation(smileInterpolation) {}
280
281 Real atmVol, phirr, spot, domDisc, forDisc, forward, expiryTime;
282 DeltaVolQuote::DeltaType dt;
283 DeltaVolQuote::AtmType at;
284 const std::vector<Real>&rrQuotes, deltas, kb_p, kb_c, vb;
286
287 mutable Real bestValue = QL_MAX_REAL;
288 mutable QuantLib::ext::shared_ptr<SimpleDeltaInterpolatedSmile> bestSmile;
289
290 Array values(const Array& x) const override {
291
292 constexpr Real large_error = 1E6;
293
294 Array smileBfVol(x.size());
295 for (Size i = 0; i < x.size(); ++i)
296 smileBfVol[i] = std::exp(x[i]) + 0.5 * std::abs(rrQuotes[i]) - atmVol;
297
298 // compute the call/put vols ....
299
300 std::vector<Real> vol_c, vol_p;
301
302 for (Size i = 0; i < deltas.size(); ++i) {
303 vol_p.push_back(atmVol + smileBfVol[i] - 0.5 * phirr * rrQuotes[i]);
304 vol_c.push_back(atmVol + smileBfVol[i] + 0.5 * phirr * rrQuotes[i]);
305 QL_REQUIRE(vol_p.back() > 0.0, " createSmile: internal error: put vol = "
306 << vol_p.back() << " during broker bf fitting");
307 QL_REQUIRE(vol_c.back() > 0.0, " createSmile: internal error: call vol = "
308 << vol_c.back() << " during broker bf fitting");
309 }
310
311 // ... set up the interpolated smile ...
312
313 QuantLib::ext::shared_ptr<SimpleDeltaInterpolatedSmile> tmpSmile;
314 try {
315 tmpSmile = QuantLib::ext::make_shared<SimpleDeltaInterpolatedSmile>(
316 spot, domDisc, forDisc, expiryTime, deltas, vol_p, vol_c, atmVol, dt, at, smileInterpolation);
317 } catch (...) {
318 // if we run into a problem we return max error and continue with the optimization
319 return Array(deltas.size(), large_error);
320 }
321
322 // ... and price the market butterfly on the constructed smile
323
324 std::vector<Real> vs;
325 for (Size i = 0; i < deltas.size(); ++i) {
326 Real pvol, cvol;
327 try {
328 pvol = tmpSmile->volatility(kb_p[i]);
329 cvol = tmpSmile->volatility(kb_c[i]);
330 } catch (...) {
331 // as above, if there is a problem, return max error
332 return Array(deltas.size(), large_error);
333 }
334 vs.push_back(blackFormula(Option::Put, kb_p[i], forward, pvol * std::sqrt(expiryTime)) +
335 blackFormula(Option::Call, kb_c[i], forward, cvol * std::sqrt(expiryTime)));
336 }
337
338 // now set the target function to the relative difference of smile vs. market price
339
340 Array result(deltas.size());
341 for (Size i = 0; i < deltas.size(); ++i) {
342 result[i] = (vs[i] - vb[i]) / vb[i];
343 if (!std::isfinite(result[i]))
344 result[i] = large_error;
345 }
346
347 Real value = std::sqrt(std::accumulate(result.begin(), result.end(), 0.0,
348 [](Real acc, Real x) { return acc + x * x; })) /
349 result.size();
350
351 if (value < bestValue) {
352 bestValue = value;
353 bestSmile = tmpSmile;
354 }
355
356 return result;
357 }
358 }; // TargetFunction declaration;
359
360 TargetFunction targetFunction{atmVol, phirr, spot, domDisc, forDisc, forward, expiryTime, dt,
361 at, rrQuotes, deltas, kb_p, kb_c, vb, smileInterpolation};
362 NoConstraint noConstraint;
363 LevenbergMarquardt lm;
364 EndCriteria endCriteria(100, 10, 1E-8, 1E-8, 1E-8);
365 Problem problem(targetFunction, noConstraint, guess);
366 lm.minimize(problem, endCriteria);
367
368 QL_REQUIRE(targetFunction.bestValue < 0.01, "createSmile at expiry "
369 << expiryTime << " failed: target function value ("
370 << problem.functionValue() << ") not close to zero");
371
372 resultSmile = targetFunction.bestSmile;
373 }
374
375 // sanity check of result smile before return it
376
377 static const std::vector<Real> samplePoints = {0.01, 0.05, 0.1, 0.2, 0.5, 0.8, 0.9, 0.95, 0.99};
378 for (auto const& simpleDelta : samplePoints) {
379 Real vol = resultSmile->volatilityAtSimpleDelta(simpleDelta);
380 QL_REQUIRE(vol > 0.0001 && vol < 5.0, "createSmile at expiry " << expiryTime << ": volatility at simple delta "
381 << simpleDelta << " (" << vol
382 << ") is not plausible.");
383 }
384
385 return resultSmile;
386}
387
388} // namespace detail
389
391 Date referenceDate, const std::vector<Date>& dates, const std::vector<Real>& deltas,
392 const std::vector<std::vector<Real>>& bfQuotes, const std::vector<std::vector<Real>>& rrQuotes,
393 const std::vector<Real>& atmQuotes, const DayCounter& dayCounter, const Calendar& calendar,
394 const Handle<Quote>& spot, const Size spotDays, const Calendar spotCalendar,
395 const Handle<YieldTermStructure>& domesticTS, const Handle<YieldTermStructure>& foreignTS,
396 const DeltaVolQuote::DeltaType dt, const DeltaVolQuote::AtmType at, const Period& switchTenor,
397 const DeltaVolQuote::DeltaType ltdt, const DeltaVolQuote::AtmType ltat, const Option::Type riskReversalInFavorOf,
398 const bool butterflyIsBrokerStyle, const SmileInterpolation smileInterpolation)
399 : BlackVolatilityTermStructure(referenceDate, calendar, Following, dayCounter), dates_(dates), deltas_(deltas),
400 bfQuotes_(bfQuotes), rrQuotes_(rrQuotes), atmQuotes_(atmQuotes), spot_(spot), spotDays_(spotDays),
401 spotCalendar_(spotCalendar), domesticTS_(domesticTS), foreignTS_(foreignTS), dt_(dt), at_(at),
402 switchTenor_(switchTenor), ltdt_(ltdt), ltat_(ltat), riskReversalInFavorOf_(riskReversalInFavorOf),
403 butterflyIsBrokerStyle_(butterflyIsBrokerStyle), smileInterpolation_(smileInterpolation) {
404
405 // checks
406
407 QL_REQUIRE(!dates_.empty(), "BlackVolatilitySurfaceBFRR: no expiry dates given");
408 QL_REQUIRE(!deltas_.empty(), "BlackVolatilitySurfaceBFRR: no deltas given");
409
410 for (Size i = 0; i < deltas_.size() - 1; ++i) {
411 QL_REQUIRE(deltas_[i + 1] > deltas_[i] && !close_enough(deltas_[i], deltas_[i + 1]),
412 "BlackVolatilitySurfaceBFRR: deltas are not strictly ascending at index " << i << ": " << deltas_[i]
413 << ", " << deltas_[i + 1]);
414 }
415
416 QL_REQUIRE(bfQuotes_.size() == dates_.size(), "BlackVolatilitySurfaceBFRR: bfQuotes ("
417 << bfQuotes_.size() << ") mismatch with expiry dates ("
418 << dates_.size() << ")");
419 QL_REQUIRE(rrQuotes_.size() == dates_.size(), "BlackVolatilitySurfaceBFRR: rrQuotes ("
420 << rrQuotes_.size() << ") mismatch with expiry dates ("
421 << dates_.size() << ")");
422 QL_REQUIRE(atmQuotes_.size() == dates_.size(), "BlackVolatilitySurfaceBFRR: atmQuotes ("
423 << atmQuotes_.size() << ") mismatch with expiry dates ("
424 << dates_.size() << ")");
425 for (auto const& q : bfQuotes) {
426 QL_REQUIRE(q.size() == deltas_.size(), "BlackVolatilitySurfaceBFRR: bfQuotes inner vector ("
427 << q.size() << ") mismatch with deltas (" << deltas_.size() << ")");
428 }
429 for (auto const& q : rrQuotes) {
430 QL_REQUIRE(q.size() == deltas_.size(), "BlackVolatilitySurfaceBFRR: bfQuotes inner vector ("
431 << q.size() << ") mismatch with deltas (" << deltas_.size() << ")");
432 }
433
434 // register with observables
435
436 registerWith(spot_);
437 registerWith(domesticTS_);
438 registerWith(foreignTS_);
439}
440
441const std::vector<bool>& BlackVolatilitySurfaceBFRR::smileHasError() const {
442 calculate();
443 return smileHasError_;
444}
445
446const std::vector<std::string>& BlackVolatilitySurfaceBFRR::smileErrorMessage() const {
447 calculate();
448 return smileErrorMessage_;
449}
450
452
453 // calculate switch time
454
455 switchTime_ = switchTenor_ == 0 * Days ? QL_MAX_REAL : timeFromReference(optionDateFromTenor(switchTenor_));
456
457 // calculate times associated to expiry dates
458
459 expiryTimes_.clear();
460 settlementDates_.clear();
461 for (auto const& d : dates_) {
462 expiryTimes_.push_back(timeFromReference(d));
463 settlementDates_.push_back(spotCalendar_.advance(d, spotDays_ * Days));
464 }
465
466 // resize cache for smiles on expiry dates
467
468 smiles_.resize(expiryTimes_.size());
469 smileHasError_.resize(expiryTimes_.size());
470 smileErrorMessage_.resize(expiryTimes_.size());
471
472 // calculate discount factors for spot settlement date and the settlement lag
473
474 Date settlDate = spotCalendar_.advance(referenceDate(), spotDays_ * Days);
475 settlDomDisc_ = domesticTS_->discount(settlDate);
476 settlForDisc_ = foreignTS_->discount(settlDate);
477 settlLag_ = timeFromReference(settlDate);
478
479 // clear caches
480
481 clearCaches();
482
483 // reset deltas to use
484
486}
487
489 BlackVolatilityTermStructure::update();
490 LazyObject::update();
491}
492
494 std::fill(smiles_.begin(), smiles_.end(), nullptr);
495 std::fill(smileHasError_.begin(), smileHasError_.end(), false);
496 std::fill(smileErrorMessage_.begin(), smileErrorMessage_.end(), std::string());
498}
499
500Volatility BlackVolatilitySurfaceBFRR::blackVolImpl(Time t, Real strike) const {
501
502 calculate();
503
504 /* minimum supported time is 1D, i.e. if t is smaller, we return the vol at 1D */
505
506 t = std::max(t, 1.0 / 365.0);
507
508 t = t <= expiryTimes_.back() ? t : expiryTimes_.back();
509
510 /* if we have cached the interpolated smile at t, we use that */
511
512 auto s = cachedInterpolatedSmiles_.find(t);
513 if (s != cachedInterpolatedSmiles_.end()) {
514 return s->second->volatility(strike);
515 }
516
517 /* find the indices ip and im such that t_im <= t < t_ip, im will be null if t < first expiry,
518 ip will be null if t >= last expiry */
519
520 Size index_p = std::upper_bound(expiryTimes_.begin(), expiryTimes_.end(), t) - expiryTimes_.begin();
521 Size index_m = index_p == 0 ? Null<Size>() : index_p - 1;
522 if (index_p == expiryTimes_.size())
523 index_p = Null<Size>();
524
525 /* the smiles at the indices might have errors, then go to the next ones without errors */
526
527 while (index_m != Null<Size>() && index_m > 0 && smileHasError_[index_m])
528 --index_m;
529
530 while (index_p != Null<Size>() && index_p < expiryTimes_.size() - 1 && smileHasError_[index_p])
531 ++index_p;
532
533 if (index_m != Null<Size>() && smileHasError_[index_m])
534 index_m = Null<Size>();
535
536 if (index_p != Null<Size>() && smileHasError_[index_p])
537 index_p = Null<Size>();
538
539 if (index_m == Null<Size>() && index_p == Null<Size>()) {
540
541 /* no valid smiles, try to remove the smallest delta until there is only one delta left, then throw an error */
542
543 if (currentDeltas_.size() <= 1) {
544 QL_FAIL("BlackVolatilitySurfaceBFRR::blackVolImpl(" << t << "," << strike
545 << "): no valid smiles, check the market data input.");
546 }
547
548 currentDeltas_.erase(currentDeltas_.begin());
549 clearCaches();
550 return blackVolImpl(t, strike);
551 }
552
553 /* build the smiles on the indices, if we do not have them yet */
554
555 if (index_m != Null<Size>() && smiles_[index_m] == nullptr) {
556 DeltaVolQuote::AtmType at;
557 DeltaVolQuote::DeltaType dt;
558 if (expiryTimes_[index_m] < switchTime_ && !close_enough(switchTime_, expiryTimes_[index_m])) {
559 at = at_;
560 dt = dt_;
561 } else {
562 at = ltat_;
563 dt = ltdt_;
564 }
565 try {
566 smiles_[index_m] = detail::createSmile(
567 spot_->value(), domesticTS_->discount(settlementDates_[index_m]) / settlDomDisc_,
569 bfQuotes_[index_m], rrQuotes_[index_m], atmQuotes_[index_m], dt, at, riskReversalInFavorOf_,
571 } catch (const std::exception& e) {
572 smileHasError_[index_m] = true;
573 smileErrorMessage_[index_m] = e.what();
574 return blackVolImpl(t, strike);
575 }
576 }
577
578 if (index_p != Null<Size>() && smiles_[index_p] == nullptr) {
579 DeltaVolQuote::AtmType at;
580 DeltaVolQuote::DeltaType dt;
581 if (expiryTimes_[index_p] < switchTime_ && !close_enough(switchTime_, expiryTimes_[index_p])) {
582 at = at_;
583 dt = dt_;
584 } else {
585 at = ltat_;
586 dt = ltdt_;
587 }
588 try {
589 smiles_[index_p] = detail::createSmile(
590 spot_->value(), domesticTS_->discount(settlementDates_[index_p]) / settlDomDisc_,
592 bfQuotes_[index_p], rrQuotes_[index_p], atmQuotes_[index_p], dt, at, riskReversalInFavorOf_,
594 } catch (const std::exception& e) {
595 smileHasError_[index_p] = true;
596 smileErrorMessage_[index_p] = e.what();
597 return blackVolImpl(t, strike);
598 }
599 }
600
601 /* Choose a consistent set of smile conventions for all maturities, we follow Clark, 4.2.3 and set
602 delta type = forward delta, with pa if the original (short term) delta convention has pa.
603 The atm type ist set to delta neutral. */
604
605 DeltaVolQuote::DeltaType dt_c =
606 dt_ == (DeltaVolQuote::Spot || dt_ == DeltaVolQuote::Fwd) ? DeltaVolQuote::Fwd : DeltaVolQuote::PaFwd;
607 DeltaVolQuote::AtmType at_c = DeltaVolQuote::AtmDeltaNeutral;
608
609 /* find the vols on both smiles for the artificial smile conventions */
610
611 Real atmVol_m = 0.0, atmVol_p = 0.0;
612 std::vector<Real> putVols_m, callVols_m, putVols_p, callVols_p;
613
614 if (index_m != Null<Size>()) {
615 try {
616 atmVol_m = smiles_[index_m]->volatility(smiles_[index_m]->atmStrike(dt_c, at_c));
617 for (auto const& d : currentDeltas_) {
618 putVols_m.push_back(
619 smiles_[index_m]->volatility(smiles_[index_m]->strikeFromDelta(Option::Put, d, dt_c)));
620 callVols_m.push_back(
621 smiles_[index_m]->volatility(smiles_[index_m]->strikeFromDelta(Option::Call, d, dt_c)));
622 }
623 } catch (const std::exception& e) {
624 smileHasError_[index_m] = true;
625 smileErrorMessage_[index_m] = e.what();
626 return blackVolImpl(t, strike);
627 }
628 }
629
630 if (index_p != Null<Size>()) {
631 try {
632 atmVol_p = smiles_[index_p]->volatility(smiles_[index_p]->atmStrike(dt_c, at_c));
633 for (auto const& d : currentDeltas_) {
634 putVols_p.push_back(
635 smiles_[index_p]->volatility(smiles_[index_p]->strikeFromDelta(Option::Put, d, dt_c)));
636 callVols_p.push_back(
637 smiles_[index_p]->volatility(smiles_[index_p]->strikeFromDelta(Option::Call, d, dt_c)));
638 }
639 } catch (const std::exception& e) {
640 smileHasError_[index_p] = true;
641 smileErrorMessage_[index_p] = e.what();
642 return blackVolImpl(t, strike);
643 }
644 }
645
646 /* find the interpolated vols */
647
648 Real atmVol_i;
649 std::vector<Real> putVols_i, callVols_i;
650
651 if (index_p == Null<Size>()) {
652 // extrapolate beyond last expiry
653 atmVol_i = atmVol_m;
654 putVols_i = putVols_m;
655 callVols_i = callVols_m;
656 QL_REQUIRE(atmVol_i > 0.0, "BlackVolatilitySurfaceBFRR: negative front-extrapolated atm vol " << atmVol_i);
657 for (Size i = 0; i < currentDeltas_.size(); ++i) {
658 QL_REQUIRE(putVols_i[i] > 0.0,
659 "BlackVolatilitySurfaceBFRR: negative front-extrapolated put vol " << putVols_i[i]);
660 QL_REQUIRE(callVols_i[i] > 0.0,
661 "BlackVolatilitySurfaceBFRR: negative front-extrapolated call vol " << callVols_i[i]);
662 }
663 } else if (index_m == Null<Size>()) {
664 // extrapolate before first expiry
665 atmVol_i = atmVol_p;
666 putVols_i = putVols_p;
667 callVols_i = callVols_p;
668 QL_REQUIRE(atmVol_i > 0.0, "BlackVolatilitySurfaceBFRR: negative back-extrapolated atm vol " << atmVol_i);
669 for (Size i = 0; i < currentDeltas_.size(); ++i) {
670 QL_REQUIRE(putVols_i[i] > 0.0,
671 "BlackVolatilitySurfaceBFRR: negative back-extrapolated put vol " << putVols_i[i]);
672 QL_REQUIRE(callVols_i[i] > 0.0,
673 "BlackVolatilitySurfaceBFRR: negative back-extrapolated call vol " << callVols_i[i]);
674 }
675 } else {
676 // interpolate between two expiries
677 Real a = (t - expiryTimes_[index_m]) / (expiryTimes_[index_p] - expiryTimes_[index_m]);
678 atmVol_i = (1.0 - a) * atmVol_m + a * atmVol_p;
679 QL_REQUIRE(atmVol_i > 0.0, "BlackVolatilitySurfaceBFRR: negative atm vol "
680 << atmVol_i << " = " << (1.0 - a) << " * " << atmVol_m << " + " << a << " * "
681 << atmVol_p);
682 for (Size i = 0; i < currentDeltas_.size(); ++i) {
683 putVols_i.push_back((1.0 - a) * putVols_m[i] + a * putVols_p[i]);
684 callVols_i.push_back((1.0 - a) * callVols_m[i] + a * callVols_p[i]);
685 QL_REQUIRE(putVols_i.back() > 0.0, "BlackVolatilitySurfaceBFRR: negative put vol for delta="
686 << currentDeltas_[i] << ", " << putVols_i.back() << " = "
687 << (1.0 - a) << " * " << putVols_m[i] << " + " << a << " * "
688 << putVols_p[i]);
689 QL_REQUIRE(callVols_i.back() > 0.0, "BlackVolatilitySurfaceBFRR: negative call vol for delta="
690 << currentDeltas_[i] << ", " << callVols_i.back() << " = "
691 << (1.0 - a) << " * " << callVols_m[i] << " + " << a << " * "
692 << callVols_p[i]);
693 }
694 }
695
696 /* build a new smile using the interpolated vols and artificial conventions
697 (querying the dom / for TS at t + settlLag_ is not entirely correct, because
698 - of possibly different dcs in the curves and the vol ts and
699 - because settLag_ is the time from today
700 to today's settlement date,
701 but the best we can realistically do in this context, because we don't have a date
702 corresponding to t) */
703
704 QuantLib::ext::shared_ptr<detail::SimpleDeltaInterpolatedSmile> smile;
705
706 try {
707 smile = QuantLib::ext::make_shared<detail::SimpleDeltaInterpolatedSmile>(
708 spot_->value(), domesticTS_->discount(t + settlLag_) / settlDomDisc_,
709 foreignTS_->discount(t + settlLag_) / settlForDisc_, t, currentDeltas_, putVols_i, callVols_i, atmVol_i,
710 dt_c, at_c, smileInterpolation_);
711 } catch (const std::exception& e) {
712 // the interpolated smile failed to build => mark the "m" smile as a failure if available and retry, otherwise
713 // market the "p" smile as a failure and retry
714 Size failureIndex = index_m != Null<Size>() ? index_m : index_p;
715 smileHasError_[failureIndex] = true;
716 smileErrorMessage_[failureIndex] = e.what();
717 return blackVolImpl(t, strike);
718 }
719
720 /* store the new smile in the cache */
721
722 cachedInterpolatedSmiles_[t] = smile;
723
724 /* return the volatility from the smile we just created */
725
726 return smile->volatility(strike);
727}
728
729} // namespace QuantExt
Black volatility surface based on bf/rr quotes.
std::vector< std::string > smileErrorMessage_
std::map< Real, QuantLib::ext::shared_ptr< detail::SimpleDeltaInterpolatedSmile > > cachedInterpolatedSmiles_
std::vector< std::vector< Real > > bfQuotes_
const std::vector< std::vector< Real > > & rrQuotes() const
Handle< YieldTermStructure > foreignTS_
const std::vector< std::string > & smileErrorMessage() const
Handle< YieldTermStructure > domesticTS_
BlackVolatilitySurfaceBFRR(Date referenceDate, const std::vector< Date > &dates, const std::vector< Real > &deltas, const std::vector< std::vector< Real > > &bfQuotes, const std::vector< std::vector< Real > > &rrQuotes, const std::vector< Real > &atmQuotes, const DayCounter &dayCounter, const Calendar &calendar, const Handle< Quote > &spot, const Size spotDays, const Calendar spotCalendar, const Handle< YieldTermStructure > &domesticTS, const Handle< YieldTermStructure > &foreignTS, const DeltaVolQuote::DeltaType dt=DeltaVolQuote::DeltaType::Spot, const DeltaVolQuote::AtmType at=DeltaVolQuote::AtmType::AtmDeltaNeutral, const Period &switchTenor=2 *Years, const DeltaVolQuote::DeltaType ltdt=DeltaVolQuote::DeltaType::Fwd, const DeltaVolQuote::AtmType ltat=DeltaVolQuote::AtmType::AtmDeltaNeutral, const Option::Type riskReversalInFavorOf=Option::Call, const bool butterflyIsBrokerStyle=true, const SmileInterpolation smileInterpolation=SmileInterpolation::Cubic)
std::vector< std::vector< Real > > rrQuotes_
Volatility blackVolImpl(Time t, Real strike) const override
const std::vector< bool > & smileHasError() const
const std::vector< std::vector< Real > > & bfQuotes() const
std::vector< QuantLib::ext::shared_ptr< detail::SimpleDeltaInterpolatedSmile > > smiles_
SimpleDeltaInterpolatedSmile(const Real spot, const Real domDisc, const Real forDisc, const Real expiryTime, const std::vector< Real > &deltas, const std::vector< Real > &putVols, const std::vector< Real > &callVols, const Real atmVol, const DeltaVolQuote::DeltaType dt, const DeltaVolQuote::AtmType at, const BlackVolatilitySurfaceBFRR::SmileInterpolation smileInterpolation, const Real accuracy=1E-6, const Size maxIterations=1000)
Real atmStrike(const DeltaVolQuote::DeltaType dt, const DeltaVolQuote::AtmType at)
Real strikeFromDelta(const Option::Type type, const Real delta, const DeltaVolQuote::DeltaType dt)
BlackVolatilitySurfaceBFRR::SmileInterpolation smileInterpolation_
QuantLib::ext::shared_ptr< Interpolation > interpolation_
QuantLib::ext::shared_ptr< SimpleDeltaInterpolatedSmile > createSmile(const Real spot, const Real domDisc, const Real forDisc, const Real expiryTime, const std::vector< Real > &deltas, const std::vector< Real > &bfQuotes, const std::vector< Real > &rrQuotes, const Real atmVol, const DeltaVolQuote::DeltaType dt, const DeltaVolQuote::AtmType at, const Option::Type riskReversalInFavorOf, const bool butterflyIsBrokerStyle, const BlackVolatilitySurfaceBFRR::SmileInterpolation smileInterpolation)
Real untransformVol(const Real w)
Real transformVol(const Real v)
Filter close_enough(const RandomVariable &x, const RandomVariable &y)