Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
fxvolcurve.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2016 Quaternion Risk Management Ltd
3 Copyright (C) 2023 Skandinaviska Enskilda Banken AB (publ)
4 All rights reserved.
5
6 This file is part of ORE, a free-software/open-source library
7 for transparent pricing and risk analysis - http://opensourcerisk.org
8
9 ORE is free software: you can redistribute it and/or modify it
10 under the terms of the Modified BSD License. You should have received a
11 copy of the license along with this program.
12 The license is also available online at <http://opensourcerisk.org>
13
14 This program is distributed on the basis that it will form a useful
15 contribution to risk analytics and model standardisation, but WITHOUT
16 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
18*/
19
26
36
37#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
38#include <ql/termstructures/volatility/equityfx/blackvariancecurve.hpp>
39#include <ql/time/calendars/target.hpp>
40#include <ql/time/daycounters/actual365fixed.hpp>
41
42#include <string.h>
43#include <algorithm>
44
45using namespace QuantLib;
46using namespace std;
47
48namespace {
49
50// utility to get a handle out of a Curve object
51template <class T, class K> Handle<T> getHandle(const string& spec, const map<string, QuantLib::ext::shared_ptr<K>>& m) {
52 auto it = m.find(spec);
53 QL_REQUIRE(it != m.end(), "FXVolCurve: Can't find spec " << spec);
54 return it->second->handle();
55}
56
57} // namespace
58
59namespace ore {
60namespace data {
61
62// second ctor
65 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves,
66 const std::map<string, QuantLib::ext::shared_ptr<FXVolCurve>>& fxVols,
67 const map<string, QuantLib::ext::shared_ptr<CorrelationCurve>>& correlationCurves,
68 const bool buildCalibrationInfo) {
69 init(asof, spec, loader, curveConfigs, fxSpots, yieldCurves, fxVols, correlationCurves, buildCalibrationInfo);
70}
71
73 QuantLib::ext::shared_ptr<FXVolatilityCurveConfig> config, const FXTriangulation& fxSpots,
74 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves) {
75 vector<Period> unsortedExp;
76
77 vector<std::pair<Real, string>> putDeltas, callDeltas;
78 bool hasATM = false;
79
80 for (auto const& delta : config->deltas()) {
81 DeltaString d(delta);
82 if (d.isAtm())
83 hasATM = true;
84 else if (d.isPut())
85 putDeltas.push_back(std::make_pair(d.delta(), delta));
86 else if (d.isCall())
87 callDeltas.push_back(std::make_pair(d.delta(), delta));
88 }
89
90 Calendar cal = config->calendar();
91
92 // sort puts 10P, 15P, 20P, ... and calls 45C, 40C, 35C, ... (notice put deltas have a negative sign)
93 auto comp = [](const std::pair<Real, string>& x, const std::pair<Real, string>& y) { return x.first > y.first; };
94 std::sort(putDeltas.begin(), putDeltas.end(), comp);
95 std::sort(callDeltas.begin(), callDeltas.end(), comp);
96
97 vector<Date> dates;
98 Matrix blackVolMatrix;
99
100 string base = "FX_OPTION/RATE_LNVOL/" + sourceCcy_ + "/" + targetCcy_ + "/";
101
102 // build quote names
103 std::vector<std::string> deltaNames;
104 for (auto const& d : putDeltas) {
105 deltaNames.push_back(d.second);
106 }
107 if (hasATM) {
108 deltaNames.push_back("ATM");
109 }
110 for (auto const& d : callDeltas) {
111 deltaNames.push_back(d.second);
112 }
113
114 if (expiriesWildcard_) {
115
116 // we save relevant delta quotes to avoid looping twice
117 std::vector<QuantLib::ext::shared_ptr<MarketDatum>> data;
118 std::vector<std::string> expiriesStr;
119 // get list of possible expiries
120 std::ostringstream ss;
121 ss << MarketDatum::InstrumentType::FX_OPTION << "/RATE_LNVOL/" << spec.unitCcy()<< "/"
122 << spec.ccy()<< "/*";
123 Wildcard w(ss.str());
124 for (const auto& md : loader.get(w, asof)) {
125 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
126 QuantLib::ext::shared_ptr<FXOptionQuote> q = QuantLib::ext::dynamic_pointer_cast<FXOptionQuote>(md);
127 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to FXOptionQuote");
128 QL_REQUIRE(q->unitCcy() == spec.unitCcy(),
129 "FXOptionQuote unit ccy '" << q->unitCcy() << "' <> FXVolatilityCurveSpec unit ccy '" << spec.unitCcy() << "'");
130 QL_REQUIRE(q->ccy() == spec.ccy(),
131 "FXOptionQuote ccy '" << q->ccy() << "' <> FXVolatilityCurveSpec ccy '" << spec.ccy() << "'");
132 Strike s = parseStrike(q->strike());
134 vector<string> tokens;
135 boost::split(tokens, md->name(), boost::is_any_of("/"));
136 QL_REQUIRE(tokens.size() == 6, "6 tokens expected in " << md->name());
137 if (expiriesWildcard_->matches(tokens[4])) {
138 data.push_back(md);
139 auto it = std::find(expiries_.begin(), expiries_.end(), q->expiry());
140 if (it == expiries_.end()) {
141 expiries_.push_back(q->expiry());
142 expiriesStr.push_back(tokens[4]);
143 }
144 }
145 }
146 }
147 unsortedExp = expiries_;
148 std::sort(expiries_.begin(), expiries_.end());
149
150 // we try to find all necessary quotes for each expiry
151 vector<Size> validExpiryIdx;
152 Matrix tmpMatrix(expiries_.size(), config->deltas().size());
153 for (Size i = 0; i < expiries_.size(); i++) {
154 Size idx = std::find(unsortedExp.begin(), unsortedExp.end(), expiries_[i]) - unsortedExp.begin();
155 string e = expiriesStr[idx];
156 for (Size j = 0; j < deltaNames.size(); ++j) {
157 string qs = base + e + "/" + deltaNames[j];
158 QuantLib::ext::shared_ptr<MarketDatum> md;
159 for (auto& m : data) {
160 if (m->name() == qs) {
161 md = m;
162 break;
163 }
164 }
165 QuantLib::ext::shared_ptr<FXOptionQuote> q = QuantLib::ext::dynamic_pointer_cast<FXOptionQuote>(md);
166 if (!q) {
167 DLOG("missing " << qs << ", expiry " << e << " will be excluded");
168 break;
169 }
170 tmpMatrix[i][j] = q->quote()->value();
171 // if we have found all the quotes then this is a valid expiry
172 if (j == deltaNames.size() - 1) {
173 dates.push_back(cal.advance(asof, expiries_[i]));
174 validExpiryIdx.push_back(i);
175 }
176 }
177 }
178
179 QL_REQUIRE(validExpiryIdx.size() > 0, "no valid FxVol expiries found");
180 DLOG("found " << validExpiryIdx.size() << " valid expiries:");
181 for (auto& e : validExpiryIdx)
182 DLOG(expiries_[e]);
183 // we build a matrix with just the valid expiries
184 blackVolMatrix = Matrix(validExpiryIdx.size(), config->deltas().size());
185 for (Size i = 0; i < validExpiryIdx.size(); i++) {
186 for (Size j = 0; j < deltaNames.size(); ++j) {
187 blackVolMatrix[i][j] = tmpMatrix[validExpiryIdx[i]][j];
188 }
189 }
190
191 } else {
192 expiries_ = parseVectorOfValues<Period>(expiriesNoDuplicates_, &parsePeriod);
193 unsortedExp = parseVectorOfValues<Period>(expiriesNoDuplicates_, &parsePeriod);
194 std::sort(expiries_.begin(), expiries_.end());
195
196 blackVolMatrix = Matrix(expiries_.size(), config->deltas().size());
197 for (Size i = 0; i < expiries_.size(); i++) {
198 Size idx = std::find(unsortedExp.begin(), unsortedExp.end(), expiries_[i]) - unsortedExp.begin();
199 string e = expiriesNoDuplicates_[idx];
200 dates.push_back(cal.advance(asof, expiries_[i]));
201 for (Size j = 0; j < deltaNames.size(); ++j) {
202 string qs = base + e + "/" + deltaNames[j];
203 QuantLib::ext::shared_ptr<MarketDatum> md = loader.get(qs, asof);
204 QuantLib::ext::shared_ptr<FXOptionQuote> q = QuantLib::ext::dynamic_pointer_cast<FXOptionQuote>(md);
205 QL_REQUIRE(q, "quote not found, " << qs);
206 blackVolMatrix[i][j] = q->quote()->value();
207 }
208 }
209 }
210
212 if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::Linear)
213 interp = QuantExt::InterpolatedSmileSection::InterpolationMethod::Linear;
214 else if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::Cubic)
215 interp = QuantExt::InterpolatedSmileSection::InterpolationMethod::CubicSpline;
216 else {
217 QL_FAIL("Delta FX vol surface: invalid interpolation, expected Linear, Cubic");
218 }
219
220 bool flatExtrapolation = true;
221 auto smileExtrapType = parseExtrapolation(config->smileExtrapolation());
222 if (smileExtrapType == Extrapolation::UseInterpolator) {
223 DLOG("Smile extrapolation switched to using interpolator.");
224 flatExtrapolation = false;
225 } else if (smileExtrapType == Extrapolation::None) {
226 DLOG("Smile extrapolation cannot be turned off on its own so defaulting to flat.");
227 } else if (smileExtrapType == Extrapolation::Flat) {
228 DLOG("Smile extrapolation has been set to flat.");
229 } else {
230 DLOG("Smile extrapolation " << smileExtrapType << " not expected so defaulting to flat.");
231 }
232
233 // daycounter used for interpolation in time.
234 // TODO: push into conventions or config
235 DayCounter dc = config->dayCounter();
236 std::vector<Real> putDeltasNum, callDeltasNum;
237 std::transform(putDeltas.begin(), putDeltas.end(), std::back_inserter(putDeltasNum),
238 [](const std::pair<Real, string>& x) { return x.first; });
239 std::transform(callDeltas.begin(), callDeltas.end(), std::back_inserter(callDeltasNum),
240 [](const std::pair<Real, string>& x) { return x.first; });
241 vol_ = QuantLib::ext::make_shared<QuantExt::BlackVolatilitySurfaceDelta>(
242 asof, dates, putDeltasNum, callDeltasNum, hasATM, blackVolMatrix, dc, cal, fxSpot_, domYts_, forYts_,
243 deltaType_, atmType_, boost::none, switchTenor_, longTermDeltaType_, longTermAtmType_, boost::none, interp,
244 flatExtrapolation);
245
246 vol_->enableExtrapolation();
247}
248
250 QuantLib::ext::shared_ptr<FXVolatilityCurveConfig> config, const FXTriangulation& fxSpots,
251 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves) {
252
253 // collect relevant market data and populate expiries (as per regex or configured list)
254
255 std::set<Period> expiriesTmp;
256
257 std::vector<QuantLib::ext::shared_ptr<FXOptionQuote>> data;
258 std::ostringstream ss;
259 ss << MarketDatum::InstrumentType::FX_OPTION << "/RATE_LNVOL/" << spec.unitCcy()<< "/"
260 << spec.ccy()<< "/*";
261 Wildcard w(ss.str());
262 for (const auto& md : loader.get(w, asof)) {
263 QuantLib::ext::shared_ptr<FXOptionQuote> q = QuantLib::ext::dynamic_pointer_cast<FXOptionQuote>(md);
264 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to FXOptionQuote");
265 QL_REQUIRE(q->unitCcy() == spec.unitCcy(),
266 "FXOptionQuote unit ccy '" << q->unitCcy() << "' <> FXVolatilityCurveSpec unit ccy '" << spec.unitCcy() << "'");
267 QL_REQUIRE(q->ccy() == spec.ccy(),
268 "FXOptionQuote ccy '" << q->ccy() << "' <> FXVolatilityCurveSpec ccy '" << spec.ccy() << "'");
269 Strike s = parseStrike(q->strike());
271 vector<string> tokens;
272 boost::split(tokens, md->name(), boost::is_any_of("/"));
273 QL_REQUIRE(tokens.size() == 6, "6 tokens expected in " << md->name());
274 if (expiriesWildcard_ && expiriesWildcard_->matches(tokens[4]))
275 expiriesTmp.insert(q->expiry());
276 data.push_back(q);
277 }
278 }
279
280 if (!expiriesWildcard_) {
281 auto tmp = parseVectorOfValues<Period>(expiriesNoDuplicates_, &parsePeriod);
282 expiriesTmp = std::set<Period>(tmp.begin(), tmp.end());
283 }
284
285 // populate quotes
286
287 std::vector<Size> smileDeltas = config->smileDelta();
288 std::sort(smileDeltas.begin(), smileDeltas.end());
289
290 std::vector<std::vector<Real>> bfQuotesTmp(expiriesTmp.size(), std::vector<Real>(smileDeltas.size(), Null<Real>()));
291 std::vector<std::vector<Real>> rrQuotesTmp(expiriesTmp.size(), std::vector<Real>(smileDeltas.size(), Null<Real>()));
292 std::vector<Real> atmQuotesTmp(expiriesTmp.size(), Null<Real>());
293
294 for (auto const& q : data) {
295 Size expiryIdx = std::distance(expiriesTmp.begin(), expiriesTmp.find(q->expiry()));
296 if (expiryIdx >= expiriesTmp.size())
297 continue;
298 Strike s = parseStrike(q->strike());
299 if (s.type == Strike::Type::ATM) {
300 atmQuotesTmp[expiryIdx] = q->quote()->value();
301 } else {
302 Size deltaIdx = std::distance(smileDeltas.begin(), std::find(smileDeltas.begin(), smileDeltas.end(),
303 static_cast<Size>(s.value + 0.5)));
304 if (deltaIdx >= smileDeltas.size())
305 continue;
306 if (s.type == Strike::Type::BF) {
307 bfQuotesTmp[expiryIdx][deltaIdx] = q->quote()->value();
308 } else if (s.type == Strike::Type::RR) {
309 rrQuotesTmp[expiryIdx][deltaIdx] = q->quote()->value();
310 }
311 }
312 }
313
314 // identify the rows with complete data
315
316 std::vector<bool> dataComplete(expiriesTmp.size(), true);
317
318 for (Size i = 0; i < expiriesTmp.size(); ++i) {
319 for (Size j = 0; j < smileDeltas.size(); ++j) {
320 if (bfQuotesTmp[i][j] == Null<Real>() || rrQuotesTmp[i][j] == Null<Real>() ||
321 atmQuotesTmp[i] == Null<Real>())
322 dataComplete[i] = false;
323 }
324 }
325
326 // if we have an explicitly configured expiry list, we require that the data is complete for all expiries
327
328 if (!expiriesWildcard_) {
329 Size i = 0;
330 for (auto const& e : expiriesTmp) {
331 QL_REQUIRE(dataComplete[i++], "BFRR FX vol surface: incomplete data for expiry " << e);
332 }
333 }
334
335 // build the final quotes for the expiries that have complete data
336
337 Size i = 0;
338 for (auto const& e : expiriesTmp) {
339 if (dataComplete[i++]) {
340 expiries_.push_back(e);
341 TLOG("adding expiry " << e << " with complete data");
342 } else {
343 TLOG("removing expiry " << e << ", because data is not complete");
344 }
345 }
346
347 std::vector<std::vector<Real>> bfQuotes(expiries_.size(), std::vector<Real>(smileDeltas.size()));
348 std::vector<std::vector<Real>> rrQuotes(expiries_.size(), std::vector<Real>(smileDeltas.size()));
349 std::vector<Real> atmQuotes(expiries_.size());
350
351 Size row = 0;
352 for (Size i = 0; i < expiriesTmp.size(); ++i) {
353 if (!dataComplete[i])
354 continue;
355 atmQuotes[row] = atmQuotesTmp[i];
356 for (Size j = 0; j < smileDeltas.size(); ++j) {
357 bfQuotes[row][j] = bfQuotesTmp[i][j];
358 rrQuotes[row][j] = rrQuotesTmp[i][j];
359 }
360 ++row;
361 }
362
363 // build BFRR surface
364
365 DLOG("build BFRR fx vol surface with " << expiries_.size() << " expiries and " << smileDeltas.size()
366 << " delta(s)");
367
369 if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::Linear)
370 interp = QuantExt::BlackVolatilitySurfaceBFRR::SmileInterpolation::Linear;
371 else if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::Cubic)
372 interp = QuantExt::BlackVolatilitySurfaceBFRR::SmileInterpolation::Cubic;
373 else {
374 QL_FAIL("BFRR FX vol surface: invalid interpolation, expected Linear, Cubic");
375 }
376
377 std::vector<Date> dates;
378 std::transform(expiries_.begin(), expiries_.end(), std::back_inserter(dates),
379 [&asof, &config](const Period& p) { return config->calendar().advance(asof, p); });
380
381 std::vector<Real> smileDeltasScaled;
382 std::transform(smileDeltas.begin(), smileDeltas.end(), std::back_inserter(smileDeltasScaled),
383 [](Size d) { return static_cast<Real>(d) / 100.0; });
384
385 vol_ = QuantLib::ext::make_shared<QuantExt::BlackVolatilitySurfaceBFRR>(
386 asof, dates, smileDeltasScaled, bfQuotes, rrQuotes, atmQuotes, config->dayCounter(), config->calendar(),
389
390 vol_->enableExtrapolation();
391}
392
394 QuantLib::ext::shared_ptr<FXVolatilityCurveConfig> config,
395 const FXTriangulation& fxSpots,
396 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves) {
397
398 bool isATM = config->dimension() == FXVolatilityCurveConfig::Dimension::ATM;
399 Natural smileDelta = 0;
400 std::string deltaRr;
401 std::string deltaBf;
402 if (!isATM) {
403 QL_REQUIRE(config->smileDelta().size() == 1,
404 "Exactly one SmileDelta required for VannaVolga Curve (got " << config->smileDelta().size() << ")");
405 smileDelta = config->smileDelta().front();
406 deltaRr = to_string(smileDelta) + "RR";
407 deltaBf = to_string(smileDelta) + "BF";
408 }
409 // We loop over all market data, looking for quotes that match the configuration
410 // every time we find a matching expiry we remove it from the list
411 // we replicate this for all 3 types of quotes were applicable.
412 Size n = isATM ? 1 : 3; // [0] = ATM, [1] = RR, [2] = BF
413 vector<vector<QuantLib::ext::shared_ptr<FXOptionQuote>>> quotes(n);
414
415 QL_REQUIRE(!expiriesWildcard_ || isATM, "wildcards only supported for ATM, Delta, BFRR FxVol Curves");
416
417 vector<Period> cExpiries;
418 vector<vector<Period>> expiries;
419 // Create the regular expression
420 if (!expiriesWildcard_) {
421 cExpiries = parseVectorOfValues<Period>(expiriesNoDuplicates_, &parsePeriod);
422 expiries = vector<vector<Period>>(n, cExpiries);
423 }
424
425 // Load the relevant quotes
426 std::vector<QuantLib::ext::shared_ptr<FXOptionQuote>> data;
427 std::ostringstream ss;
428 ss << MarketDatum::InstrumentType::FX_OPTION << "/RATE_LNVOL/" << spec.unitCcy()<< "/"
429 << spec.ccy()<< "/*";
430 Wildcard w(ss.str());
431 for (const auto& md : loader.get(w, asof)) {
432
433 QuantLib::ext::shared_ptr<FXOptionQuote> q = QuantLib::ext::dynamic_pointer_cast<FXOptionQuote>(md);
434 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to FXOptionQuote");
435 QL_REQUIRE(q->unitCcy() == spec.unitCcy(),
436 "FXOptionQuote unit ccy '" << q->unitCcy() << "' <> FXVolatilityCurveSpec unit ccy '" << spec.unitCcy() << "'");
437 QL_REQUIRE(q->ccy() == spec.ccy(),
438 "FXOptionQuote ccy '" << q->ccy() << "' <> FXVolatilityCurveSpec ccy '" << spec.ccy() << "'");
439
440
441 Size idx = 999999;
442 if (q->strike() == "ATM")
443 idx = 0;
444 else if (!isATM && q->strike() == deltaRr)
445 idx = 1;
446 else if (!isATM && q->strike() == deltaBf)
447 idx = 2;
448
449 // silently skip unknown strike strings
450 if ((isATM && idx == 0) || (!isATM && idx <= 2)) {
451 if (expiriesWildcard_) {
452 vector<string> tokens;
453 boost::split(tokens, md->name(), boost::is_any_of("/"));
454 QL_REQUIRE(tokens.size() == 6, "6 tokens expected in " << md->name());
455 if (expiriesWildcard_->matches(tokens[4])) {
456 quotes[idx].push_back(q);
457 }
458 } else {
459 auto it = std::find(expiries[idx].begin(), expiries[idx].end(), q->expiry());
460 if (it != expiries[idx].end()) {
461 // we have a hit
462 quotes[idx].push_back(q);
463 // remove it from the list
464 expiries[idx].erase(it);
465 }
466
467 // check if we are done
468 // for ATM we just check expiries[0], otherwise we check all 3
469 if (expiries[0].empty() && (isATM || (expiries[1].empty() && expiries[2].empty())))
470 break;
471 }
472 }
473 }
474
475 // Check ATM first
476 // Check that we have all the expiries we need
477 LOG("FXVolCurve: read " << quotes[0].size() << " ATM vols");
478 if (!expiriesWildcard_) {
479 QL_REQUIRE(expiries[0].size() == 0,
480 "No ATM quote found for spec " << spec << " with expiry " << expiries[0].front());
481 }
482
483 QL_REQUIRE(quotes[0].size() > 0, "No ATM quotes found for spec " << spec);
484 // No check the rest
485 if (!isATM) {
486 LOG("FXVolCurve: read " << quotes[1].size() << " RR and " << quotes[2].size() << " BF quotes");
487 QL_REQUIRE(expiries[1].size() == 0,
488 "No RR quote found for spec " << spec << " with expiry " << expiries[1].front());
489 QL_REQUIRE(expiries[2].size() == 0,
490 "No BF quote found for spec " << spec << " with expiry " << expiries[2].front());
491 }
492
493 // sort all quotes
494 for (Size i = 0; i < n; i++) {
495 std::sort(quotes[i].begin(), quotes[i].end(),
496 [](const QuantLib::ext::shared_ptr<FXOptionQuote>& a, const QuantLib::ext::shared_ptr<FXOptionQuote>& b) -> bool {
497 return a->expiry() < b->expiry();
498 });
499 }
500
501 // daycounter used for interpolation in time.
502 // TODO: push into conventions or config
503 DayCounter dc = config->dayCounter();
504 Calendar cal = config->calendar();
505
506 // build vol curve
507 if (isATM && quotes[0].size() == 1) {
508 vol_ = QuantLib::ext::shared_ptr<BlackVolTermStructure>(
509 new BlackConstantVol(asof, config->calendar(), quotes[0].front()->quote()->value(), dc));
510 expiries_ = {quotes[0].front()->expiry()};
511 } else {
512
513 Size numExpiries = quotes[0].size();
514 vector<Date> dates(numExpiries);
515 vector<vector<Volatility>> vols(n, vector<Volatility>(numExpiries)); // same as above: [0] = ATM, etc.
516
517 for (Size i = 0; i < numExpiries; i++) {
518 dates[i] = cal.advance(asof, quotes[0][i]->expiry());
519 expiries_.push_back(quotes[0][i]->expiry());
520 DLOG("Spec Tenor Vol Variance");
521 for (Size idx = 0; idx < n; idx++) {
522 vols[idx][i] = quotes[idx][i]->quote()->value();
523 Real variance = vols[idx][i] * vols[idx][i] * (dates[i] - asof) / 365.0; // approximate variance
524 DLOG(spec << " " << quotes[0][i]->expiry() << " " << vols[idx][i] << " " << variance);
525 }
526 }
527
528 if (isATM) {
529 // ATM
530 // Set forceMonotoneVariance to false - allowing decreasing variance
531 vol_ = QuantLib::ext::shared_ptr<BlackVolTermStructure>(new BlackVarianceCurve(asof, dates, vols[0], dc, false));
532 } else {
533 // Smile
534 bool vvFirstApprox = false; // default to VannaVolga second approximation
535 if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::VannaVolga1) {
536 vvFirstApprox = true;
537 }
538
539 vol_ = QuantLib::ext::make_shared<QuantExt::FxBlackVannaVolgaVolatilitySurface>(
540 asof, dates, vols[0], vols[1], vols[2], dc, cal, fxSpot_, domYts_, forYts_, false, vvFirstApprox,
542 }
543 }
544 vol_->enableExtrapolation();
545}
546
548 QuantLib::ext::shared_ptr<FXVolatilityCurveConfig> config, const FXTriangulation& fxSpots,
549 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves) {
550
551 // collect relevant market data and populate expiries (as per regex or configured list)
552 std::set<Period> expiriesTmp;
553
554 std::vector<QuantLib::ext::shared_ptr<FXOptionQuote>> data;
555 std::ostringstream ss;
556 ss << MarketDatum::InstrumentType::FX_OPTION << "/RATE_LNVOL/" << spec.unitCcy() << "/" << spec.ccy() << "/*";
557 Wildcard w(ss.str());
558 for (const auto& md : loader.get(w, asof)) {
559 QuantLib::ext::shared_ptr<FXOptionQuote> q = QuantLib::ext::dynamic_pointer_cast<FXOptionQuote>(md);
560 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to FXOptionQuote");
561 QL_REQUIRE(q->unitCcy() == spec.unitCcy(), "FXOptionQuote unit ccy '" << q->unitCcy()
562 << "' <> FXVolatilityCurveSpec unit ccy '"
563 << spec.unitCcy() << "'");
564 QL_REQUIRE(q->ccy() == spec.ccy(),
565 "FXOptionQuote ccy '" << q->ccy() << "' <> FXVolatilityCurveSpec ccy '" << spec.ccy() << "'");
566 Strike s = parseStrike(q->strike());
567 if (s.type == Strike::Type::Absolute) {
568 vector<string> tokens;
569 boost::split(tokens, md->name(), boost::is_any_of("/"));
570 QL_REQUIRE(tokens.size() == 6, "6 tokens expected in " << md->name());
571 if (expiriesWildcard_ && expiriesWildcard_->matches(tokens[4]))
572 expiriesTmp.insert(q->expiry());
573 data.push_back(q);
574 }
575 }
576
577 if (!expiriesWildcard_) {
578 auto tmp = parseVectorOfValues<Period>(expiriesNoDuplicates_, &parsePeriod);
579 expiriesTmp = std::set<Period>(tmp.begin(), tmp.end());
580 }
581
582 // populate quotes
583 std::vector<std::map<Real, Real>> strikeQuotesTmp(expiriesTmp.size());
584
585 for (auto const& q : data) {
586 Size expiryIdx = std::distance(expiriesTmp.begin(), expiriesTmp.find(q->expiry()));
587 if (expiryIdx >= expiriesTmp.size())
588 continue;
589 Strike s = parseStrike(q->strike());
590 // If the strike for expirtIdx does not exist, read in the quote
591 if (strikeQuotesTmp[expiryIdx].count(s.value) == 0)
592 strikeQuotesTmp[expiryIdx][s.value] = q->quote()->value();
593 }
594
595 // identify the expiries with at least one strike quote
596 std::vector<bool> dataComplete(expiriesTmp.size(), true);
597
598 for (Size i = 0; i < expiriesTmp.size(); ++i) {
599 if (strikeQuotesTmp[i].empty())
600 dataComplete[i] = false;
601 }
602
603 // if we have an explicitly configured expiry list, we require that there is at least one strike quote for all expiries
604
605 if (!expiriesWildcard_) {
606 Size i = 0;
607 for (auto const& e : expiriesTmp) {
608 QL_REQUIRE(dataComplete[i++], "Absolute FX vol surface: missing data for expiry " << e);
609 }
610 }
611
612 // build the final quotes for the expiries that have complete data
613 Size i = 0;
614 for (auto const& e : expiriesTmp) {
615 if (dataComplete[i++]) {
616 expiries_.push_back(e);
617 TLOG("adding expiry " << e << " with at least one strike quote");
618 } else {
619 TLOG("removing expiry " << e << ", no strike quote found");
620 }
621 }
622
623 std::vector<std::vector<Real>> strikeQuotes, strikes;
624 std::vector<Real> strikeQuote, strike;
625
626 for (Size i = 0; i < expiriesTmp.size(); ++i) {
627 if (!dataComplete[i])
628 continue;
629 for (auto const& quote : strikeQuotesTmp[i]) {
630 strike.push_back(quote.first);
631 strikeQuote.push_back(quote.second);
632 }
633 strikeQuotes.push_back(strikeQuote);
634 strikes.push_back(strike);
635 strikeQuote.clear();
636 strike.clear();
637 }
638
639 // build Absolute surface
640
641 DLOG("build Absolute fx vol surface with " << expiries_.size() << " expiries");
642
644 if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::Linear)
645 interp = QuantExt::BlackVolatilitySurfaceAbsolute::SmileInterpolation::Linear;
646 else if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::Cubic)
647 interp = QuantExt::BlackVolatilitySurfaceAbsolute::SmileInterpolation::Cubic;
648 else {
649 QL_FAIL("Absolute FX vol surface: invalid interpolation, expected Linear, Cubic");
650 }
651
652 std::vector<Date> dates;
653 std::transform(expiries_.begin(), expiries_.end(), std::back_inserter(dates),
654 [&asof, &config](const Period& p) { return config->calendar().advance(asof, p); });
655
656 vol_ = QuantLib::ext::make_shared<QuantExt::BlackVolatilitySurfaceAbsolute>(
657 asof, dates, strikes, strikeQuotes, config->dayCounter(), config->calendar(),
659 longTermAtmType_, interp);
660
661 vol_->enableExtrapolation();
662}
663
664Handle<QuantExt::CorrelationTermStructure>
665getCorrelationCurve(const std::string& index1, const std::string& index2,
666 const map<string, QuantLib::ext::shared_ptr<CorrelationCurve>>& correlationCurves) {
667 // straight pair
668 auto tmpCorr = correlationCurves.find("Correlation/" + index1 + "&" + index2);
669 if (tmpCorr != correlationCurves.end()) {
670 return Handle<QuantExt::CorrelationTermStructure>(tmpCorr->second->corrTermStructure());
671 }
672 // inverse pair
673 tmpCorr = correlationCurves.find("Correlation/" + index2 + "&" + index1);
674 if (tmpCorr != correlationCurves.end()) {
675 return Handle<QuantExt::CorrelationTermStructure>(tmpCorr->second->corrTermStructure());
676 }
677 // inverse fx index1
678 tmpCorr = correlationCurves.find("Correlation/" + inverseFxIndex(index1) + "&" + index2);
679 if (tmpCorr != correlationCurves.end()) {
680 Handle<QuantExt::CorrelationTermStructure> h =
681 Handle<QuantExt::CorrelationTermStructure>(tmpCorr->second->corrTermStructure());
682 return Handle<QuantExt::CorrelationTermStructure>(
683 QuantLib::ext::make_shared<QuantExt::NegativeCorrelationTermStructure>(h));
684 }
685 tmpCorr = correlationCurves.find("Correlation/" + index2 + "&" + inverseFxIndex(index1));
686 if (tmpCorr != correlationCurves.end()) {
687 Handle<QuantExt::CorrelationTermStructure> h =
688 Handle<QuantExt::CorrelationTermStructure>(tmpCorr->second->corrTermStructure());
689 return Handle<QuantExt::CorrelationTermStructure>(
690 QuantLib::ext::make_shared<QuantExt::NegativeCorrelationTermStructure>(h));
691 }
692 // inverse fx index2
693 tmpCorr = correlationCurves.find("Correlation/" + index1 + "&" + inverseFxIndex(index2));
694 if (tmpCorr != correlationCurves.end()) {
695 Handle<QuantExt::CorrelationTermStructure> h =
696 Handle<QuantExt::CorrelationTermStructure>(tmpCorr->second->corrTermStructure());
697 return Handle<QuantExt::CorrelationTermStructure>(
698 QuantLib::ext::make_shared<QuantExt::NegativeCorrelationTermStructure>(h));
699 }
700 tmpCorr = correlationCurves.find("Correlation/" + inverseFxIndex(index2) + "&" + index1);
701 if (tmpCorr != correlationCurves.end()) {
702 Handle<QuantExt::CorrelationTermStructure> h =
703 Handle<QuantExt::CorrelationTermStructure>(tmpCorr->second->corrTermStructure());
704 return Handle<QuantExt::CorrelationTermStructure>(
705 QuantLib::ext::make_shared<QuantExt::NegativeCorrelationTermStructure>(h));
706 }
707 // both fx indices inverted
708 tmpCorr = correlationCurves.find("Correlation/" + inverseFxIndex(index1) + "&" + inverseFxIndex(index2));
709 if (tmpCorr != correlationCurves.end()) {
710 return Handle<QuantExt::CorrelationTermStructure>(tmpCorr->second->corrTermStructure());
711 }
712 tmpCorr = correlationCurves.find("Correlation/" + inverseFxIndex(index2) + "&" + inverseFxIndex(index1));
713 if (tmpCorr != correlationCurves.end()) {
714 return Handle<QuantExt::CorrelationTermStructure>(tmpCorr->second->corrTermStructure());
715 }
716
717 QL_FAIL("no correlation curve found for " << index1 << ":" << index2);
718}
719
721 QuantLib::ext::shared_ptr<FXVolatilityCurveConfig> config, const FXTriangulation& fxSpots,
722 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves,
723 const map<string, QuantLib::ext::shared_ptr<FXVolCurve>>& fxVols,
724 const map<string, QuantLib::ext::shared_ptr<CorrelationCurve>>& correlationCurves) {
725
726 DLOG("Triangulating FxVol curve " << config->curveID() << " from baseVols " << config->baseVolatility1() << ":"
727 << config->baseVolatility2());
728
729 std::string baseCcy;
730 QL_REQUIRE(config->baseVolatility1().size() == 6, "invalid ccy pair length for baseVolatility1");
731 auto forBase1 = config->baseVolatility1().substr(0, 3);
732 auto domBase1 = config->baseVolatility1().substr(3);
733 std::string spec1 = "FXVolatility/" + forBase1 + "/" + domBase1 + "/" + config->baseVolatility1();
734
735 bool base1Inverted = false;
736 if (forBase1 != sourceCcy_ && forBase1 != targetCcy_) {
737 // we invert the pair
738 base1Inverted = true;
739 std::string tmp = forBase1;
740 forBase1 = targetCcy_;
741 domBase1 = tmp;
742
743 QL_REQUIRE(forBase1 == sourceCcy_ || forBase1 == targetCcy_,
744 "FxVol: mismatch in the baseVolatility1 " << config->baseVolatility1() << " and Target Pair "
745 << sourceCcy_ << targetCcy_);
746 }
747 baseCcy = domBase1;
748
749 QL_REQUIRE(config->baseVolatility2().size() == 6, "invalid ccy pair length for baseVolatility2");
750 auto forBase2 = config->baseVolatility2().substr(0, 3);
751 auto domBase2 = config->baseVolatility2().substr(3);
752 std::string spec2 = "FXVolatility/" + forBase2 + "/" + domBase2 + "/" + config->baseVolatility2();
753 bool base2Inverted = false;
754
755 QL_REQUIRE(forBase2 == baseCcy || domBase2 == baseCcy,
756 "baseVolatility2 must share a ccy code with the baseVolatility1");
757
758 if (forBase2 != sourceCcy_ && forBase2 != targetCcy_) {
759 // we invert the pair
760 std::string tmp = forBase2;
761 forBase2 = targetCcy_;
762 domBase2 = tmp;
763 base2Inverted = true;
764 }
765
766 auto tmp = fxVols.find(spec1);
767 QL_REQUIRE(tmp != fxVols.end(), "fx vol not found for " << config->baseVolatility1());
768 Handle<BlackVolTermStructure> forBaseVol;
769 if (base1Inverted) {
770 auto h = Handle<BlackVolTermStructure>(tmp->second->volTermStructure());
771 if (!h.empty())
772 forBaseVol = Handle<BlackVolTermStructure>(QuantLib::ext::make_shared<QuantExt::BlackInvertedVolTermStructure>(h));
773 } else {
774 forBaseVol = Handle<BlackVolTermStructure>(tmp->second->volTermStructure());
775 }
776 forBaseVol->enableExtrapolation();
777
778 tmp = fxVols.find(spec2);
779 QL_REQUIRE(tmp != fxVols.end(), "fx vol not found for " << config->baseVolatility2());
780 Handle<BlackVolTermStructure> domBaseVol;
781 if (base2Inverted) {
782 auto h = Handle<BlackVolTermStructure>(tmp->second->volTermStructure());
783 if (!h.empty())
784 domBaseVol = Handle<BlackVolTermStructure>(QuantLib::ext::make_shared<QuantExt::BlackInvertedVolTermStructure>(h));
785
786 } else {
787 domBaseVol = Handle<BlackVolTermStructure>(tmp->second->volTermStructure());
788 }
789 domBaseVol->enableExtrapolation();
790
791 string forIndex = "FX-" + config->fxIndexTag() + "-" + sourceCcy_ + "-" + baseCcy;
792 string domIndex = "FX-" + config->fxIndexTag() + "-" + targetCcy_ + "-" + baseCcy;
793
794 Handle<QuantExt::CorrelationTermStructure> rho = getCorrelationCurve(forIndex, domIndex, correlationCurves);
795
796 vol_ = QuantLib::ext::make_shared<QuantExt::BlackTriangulationATMVolTermStructure>(forBaseVol, domBaseVol, rho);
797 vol_->enableExtrapolation();
798}
799
800void FXVolCurve::init(Date asof, FXVolatilityCurveSpec spec, const Loader& loader,
801 const CurveConfigurations& curveConfigs, const FXTriangulation& fxSpots,
802 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves,
803 const std::map<string, QuantLib::ext::shared_ptr<FXVolCurve>>& fxVols,
804 const map<string, QuantLib::ext::shared_ptr<CorrelationCurve>>& correlationCurves,
805 const bool buildCalibrationInfo) {
806 try {
807
808 const QuantLib::ext::shared_ptr<FXVolatilityCurveConfig>& config = curveConfigs.fxVolCurveConfig(spec.curveConfigID());
809 QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();
810
811 QL_REQUIRE(config->dimension() == FXVolatilityCurveConfig::Dimension::ATM ||
814 config->dimension() == FXVolatilityCurveConfig::Dimension::SmileDelta ||
815 config->dimension() == FXVolatilityCurveConfig::Dimension::SmileBFRR ||
817 "Unknown FX curve building dimension");
818
819 expiriesWildcard_ = getUniqueWildcard(config->expiries());
820
821 // remove expiries that would lead to duplicate expiry dates (keep the later expiry in this case)
822
823 if (expiriesWildcard_) {
824 DLOG("expiry wildcard is used: " << expiriesWildcard_->pattern());
825 } else {
826 std::vector<std::tuple<std::string, Period, Date>> tmp;
827 for (auto const& e : config->expiries()) {
828 auto p = parsePeriod(e);
829 tmp.push_back(std::make_tuple(e, p, config->calendar().advance(asof, p)));
830 }
831 std::sort(tmp.begin(), tmp.end(),
832 [](const std::tuple<std::string, Period, Date>& a,
833 const std::tuple<std::string, Period, Date>& b) -> bool {
834 if (std::get<2>(a) == std::get<2>(b)) {
835 try {
836 // equal dates => compare periods (might throw!)
837 return std::get<1>(a) < std::get<1>(b);
838 } catch (...) {
839 // period comparison not possible => compare the strings
840 return std::get<0>(a) < std::get<0>(b);
841 }
842 }
843 // different dates => compare them
844 return std::get<2>(a) < std::get<2>(b);
845 });
846 Date lastDate = Date::maxDate();
847 for (Size i = tmp.size(); i > 0; --i) {
848 if (std::get<2>(tmp[i - 1]) == lastDate)
849 continue;
850 expiriesNoDuplicates_.insert(expiriesNoDuplicates_.begin(), std::get<0>(tmp[i - 1]));
851 lastDate = std::get<2>(tmp[i - 1]);
852 }
853
854 DLOG("expiries in configuration:")
855 for (auto const& e : config->expiries()) {
856 DLOG(e);
857 }
858
859 DLOG("expiries after removing duplicate expiry dates and sorting:");
860 for (auto const& e : expiriesNoDuplicates_) {
861 DLOG(e);
862 }
863 }
864
865 QL_REQUIRE(config->dimension() == FXVolatilityCurveConfig::Dimension::ATMTriangulated || expiriesWildcard_ ||
866 !expiriesNoDuplicates_.empty(),
867 "no expiries after removing duplicate expiry dates");
868
869 std::vector<std::string> tokens;
870 boost::split(tokens, config->fxSpotID(), boost::is_any_of("/"));
871 QL_REQUIRE(tokens.size() == 3, "Expected 3 tokens in fx spot id '" << config->fxSpotID() << "'");
872 sourceCcy_ = tokens[1];
873 targetCcy_ = tokens[2];
874
875 atmType_ = DeltaVolQuote::AtmType::AtmDeltaNeutral;
876 deltaType_ = DeltaVolQuote::DeltaType::Spot;
877 switchTenor_ = 2 * Years;
878 longTermAtmType_ = DeltaVolQuote::AtmType::AtmDeltaNeutral;
879 longTermDeltaType_ = DeltaVolQuote::DeltaType::Fwd;
880 riskReversalInFavorOf_ = Option::Call;
882 spotDays_ = 2;
883 string calTmp = sourceCcy_ + "," + targetCcy_;
885
886 if (config->conventionsID() == "") {
887 WLOG("no fx option conventions given in fxvol curve config for " << spec.curveConfigID()
888 << ", assuming defaults");
889 } else {
890 auto fxOptConv = QuantLib::ext::dynamic_pointer_cast<FxOptionConvention>(conventions->get(config->conventionsID()));
891 QL_REQUIRE(fxOptConv,
892 "unable to cast convention '" << config->conventionsID() << "' into FxOptionConvention");
893 QuantLib::ext::shared_ptr<FXConvention> fxConv;
894 if (!fxOptConv->fxConventionID().empty()) {
895 fxConv = QuantLib::ext::dynamic_pointer_cast<FXConvention>(conventions->get(fxOptConv->fxConventionID()));
896 QL_REQUIRE(fxConv, "unable to cast convention '" << fxOptConv->fxConventionID()
897 << "', from FxOptionConvention '"
898 << config->conventionsID() << "' into FxConvention");
899 }
900 atmType_ = fxOptConv->atmType();
901 deltaType_ = fxOptConv->deltaType();
902 longTermAtmType_ = fxOptConv->longTermAtmType();
903 longTermDeltaType_ = fxOptConv->longTermDeltaType();
904 switchTenor_ = fxOptConv->switchTenor();
905 riskReversalInFavorOf_ = fxOptConv->riskReversalInFavorOf();
906 butterflyIsBrokerStyle_ = fxOptConv->butterflyIsBrokerStyle();
907 if (fxConv) {
908 spotDays_ = fxConv->spotDays();
909 spotCalendar_ = fxConv->advanceCalendar();
910 }
911 }
912
913 auto spotSpec = QuantLib::ext::dynamic_pointer_cast<FXSpotSpec>(parseCurveSpec(config->fxSpotID()));
914 QL_REQUIRE(spotSpec != nullptr,
915 "could not parse '" << config->fxSpotID() << "' to FXSpotSpec, expected FX/CCY1/CCY2");
916 fxSpot_ = fxSpots.getQuote(spotSpec->unitCcy() + spotSpec->ccy());
917 if (!config->fxDomesticYieldCurveID().empty())
918 domYts_ = getHandle<YieldTermStructure>(config->fxDomesticYieldCurveID(), yieldCurves);
919 if (!config->fxForeignYieldCurveID().empty())
920 forYts_ = getHandle<YieldTermStructure>(config->fxForeignYieldCurveID(), yieldCurves);
921
922 if (config->dimension() == FXVolatilityCurveConfig::Dimension::SmileDelta) {
923 buildSmileDeltaCurve(asof, spec, loader, config, fxSpots, yieldCurves);
924 } else if (config->dimension() == FXVolatilityCurveConfig::Dimension::SmileBFRR) {
925 buildSmileBfRrCurve(asof, spec, loader, config, fxSpots, yieldCurves);
926 } else if (config->dimension() == FXVolatilityCurveConfig::Dimension::ATMTriangulated) {
927 buildATMTriangulated(asof, spec, loader, config, fxSpots, yieldCurves, fxVols, correlationCurves);
928 } else if (config->dimension() == FXVolatilityCurveConfig::Dimension::SmileAbsolute) {
929 buildSmileAbsoluteCurve(asof, spec, loader, config, fxSpots, yieldCurves);
930 } else {
931 buildVannaVolgaOrATMCurve(asof, spec, loader, config, fxSpots, yieldCurves);
932 }
933
934 // build calibration info
935
936 if (buildCalibrationInfo) {
937
938 DLOG("Building calibration info for fx vol surface");
939
940 if (domYts_.empty() || forYts_.empty()) {
941 WLOG("no domestic / foreign yield curves given in fx vol curve config for "
942 << spec.curveConfigID() << ", skip building calibration info");
943 return;
944 }
945
946 ReportConfig rc = effectiveReportConfig(curveConfigs.reportConfigFxVols(), config->reportConfig());
947
948 bool reportOnDeltaGrid = *rc.reportOnDeltaGrid();
949 bool reportOnMoneynessGrid = *rc.reportOnMoneynessGrid();
950 std::vector<Real> moneyness = *rc.moneyness();
951 std::vector<std::string> deltas = *rc.deltas();
952 std::vector<Period> expiries = *rc.expiries();
953
954 calibrationInfo_ = QuantLib::ext::make_shared<FxEqCommVolCalibrationInfo>();
955
956 calibrationInfo_->dayCounter = config->dayCounter().empty() ? "na" : config->dayCounter().name();
957 calibrationInfo_->calendar = config->calendar().empty() ? "na" : config->calendar().name();
963 calibrationInfo_->riskReversalInFavorOf = riskReversalInFavorOf_ == Option::Call ? "Call" : "Put";
964 calibrationInfo_->butterflyStyle = butterflyIsBrokerStyle_ ? "Broker" : "Smile";
965
966 std::vector<Real> times, forwards, domDisc, forDisc;
967 Date settl = spotCalendar_.advance(asof, spotDays_ * Days);
968 for (auto const& p : expiries) {
969 Date d = vol_->optionDateFromTenor(p);
970 Date settlFwd = spotCalendar_.advance(d, spotDays_ * Days);
971 calibrationInfo_->expiryDates.push_back(d);
972 times.push_back(vol_->dayCounter().empty() ? Actual365Fixed().yearFraction(asof, d)
973 : vol_->timeFromReference(d));
974 domDisc.push_back(domYts_->discount(settlFwd) / domYts_->discount(settl));
975 forDisc.push_back(forYts_->discount(settlFwd) / forYts_->discount(settl));
976 forwards.push_back(fxSpot_->value() / domDisc.back() * forDisc.back());
977 }
978
979 calibrationInfo_->times = times;
980 calibrationInfo_->forwards = forwards;
981
982 Date switchExpiry =
983 vol_->calendar().empty() ? asof + switchTenor_ : vol_->optionDateFromTenor(switchTenor_);
984 Real switchTime = vol_->dayCounter().empty() ? Actual365Fixed().yearFraction(asof, switchExpiry)
985 : vol_->timeFromReference(switchExpiry);
986 if (switchTenor_ == 0 * Days)
987 switchTime = QL_MAX_REAL;
988
989 std::vector<std::vector<Real>> callPricesDelta(times.size(), std::vector<Real>(deltas.size(), 0.0));
990 std::vector<std::vector<Real>> callPricesMoneyness(times.size(), std::vector<Real>(moneyness.size(), 0.0));
991
992 calibrationInfo_->isArbitrageFree = true;
993
994 if (reportOnDeltaGrid) {
995 calibrationInfo_->deltas = deltas;
996 calibrationInfo_->deltaCallPrices =
997 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
998 calibrationInfo_->deltaPutPrices =
999 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1000 calibrationInfo_->deltaGridStrikes =
1001 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1002 calibrationInfo_->deltaGridProb =
1003 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1004 calibrationInfo_->deltaGridImpliedVolatility =
1005 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1006 calibrationInfo_->deltaGridCallSpreadArbitrage =
1007 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(deltas.size(), true));
1008 calibrationInfo_->deltaGridButterflyArbitrage =
1009 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(deltas.size(), true));
1010 DeltaVolQuote::DeltaType dt;
1011 DeltaVolQuote::AtmType at;
1012 TLOG("Delta surface arbitrage analysis result (no calendar spread arbitrage included):");
1013 Real maxTime = QL_MAX_REAL;
1014 if (!expiries_.empty())
1015 maxTime = vol_->timeFromReference(vol_->optionDateFromTenor(expiries_.back()));
1016 for (Size i = 0; i < times.size(); ++i) {
1017 Real t = times[i];
1018 if (t <= switchTime || close_enough(t, switchTime)) {
1019 at = atmType_;
1020 dt = deltaType_;
1021 } else {
1022 at = longTermAtmType_;
1023 dt = longTermDeltaType_;
1024 }
1025 // for times after the last quoted expiry we use artificial conventions to avoid problems with
1026 // strike from delta conversions: we keep the pa feature, but use fwd delta always and ATM DNS
1027 if (t > maxTime) {
1028 at = DeltaVolQuote::AtmDeltaNeutral;
1029 dt = (deltaType_ == DeltaVolQuote::Spot || deltaType_ == DeltaVolQuote::Fwd)
1030 ? DeltaVolQuote::Fwd
1031 : DeltaVolQuote::PaFwd;
1032 }
1033 bool validSlice = true;
1034 for (Size j = 0; j < deltas.size(); ++j) {
1035 DeltaString d(deltas[j]);
1036 try {
1037 Real strike;
1038 if (d.isAtm()) {
1039 strike =
1040 QuantExt::getAtmStrike(dt, at, fxSpot_->value(), domDisc[i], forDisc[i], vol_, t);
1041 } else if (d.isCall()) {
1042 strike = QuantExt::getStrikeFromDelta(Option::Call, d.delta(), dt, fxSpot_->value(),
1043 domDisc[i], forDisc[i], vol_, t);
1044 } else {
1045 strike = QuantExt::getStrikeFromDelta(Option::Put, d.delta(), dt, fxSpot_->value(),
1046 domDisc[i], forDisc[i], vol_, t);
1047 }
1048 Real stddev = std::sqrt(vol_->blackVariance(t, strike));
1049 callPricesDelta[i][j] = blackFormula(Option::Call, strike, forwards[i], stddev);
1050
1051 if (d.isPut()) {
1052 calibrationInfo_->deltaPutPrices[i][j] = blackFormula(Option::Put, strike, forwards[i], stddev, domDisc[i]);
1053 } else {
1054 calibrationInfo_->deltaCallPrices[i][j] = blackFormula(Option::Call, strike, forwards[i], stddev, domDisc[i]);
1055 }
1056
1057 calibrationInfo_->deltaGridStrikes[i][j] = strike;
1058 calibrationInfo_->deltaGridImpliedVolatility[i][j] = stddev / std::sqrt(t);
1059 } catch (const std::exception& e) {
1060 validSlice = false;
1061 TLOG("error for time " << t << " delta " << deltas[j] << ": " << e.what());
1062 }
1063 }
1064 if (validSlice) {
1065 try {
1067 forwards[i], callPricesDelta[i]);
1068 calibrationInfo_->deltaGridCallSpreadArbitrage[i] = cm.callSpreadArbitrage();
1069 calibrationInfo_->deltaGridButterflyArbitrage[i] = cm.butterflyArbitrage();
1070 if (!cm.arbitrageFree())
1071 calibrationInfo_->isArbitrageFree = false;
1072 calibrationInfo_->deltaGridProb[i] = cm.density();
1074 } catch (const std::exception& e) {
1075 TLOG("error for time " << t << ": " << e.what());
1076 calibrationInfo_->isArbitrageFree = false;
1077 TLOGGERSTREAM("..(invalid slice)..");
1078 }
1079 } else {
1080 calibrationInfo_->isArbitrageFree = false;
1081 TLOGGERSTREAM("..(invalid slice)..");
1082 }
1083 }
1084 TLOG("Delta surface arbitrage analysis completed.");
1085 }
1086
1087 if (reportOnMoneynessGrid) {
1088 calibrationInfo_->moneyness = moneyness;
1089 calibrationInfo_->moneynessCallPrices =
1090 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1091 calibrationInfo_->moneynessPutPrices =
1092 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1093 calibrationInfo_->moneynessGridStrikes =
1094 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1095 calibrationInfo_->moneynessGridProb =
1096 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1097 calibrationInfo_->moneynessGridImpliedVolatility =
1098 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1099 calibrationInfo_->moneynessGridCallSpreadArbitrage =
1100 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(moneyness.size(), true));
1101 calibrationInfo_->moneynessGridButterflyArbitrage =
1102 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(moneyness.size(), true));
1103 calibrationInfo_->moneynessGridCalendarArbitrage =
1104 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(moneyness.size(), true));
1105 for (Size i = 0; i < times.size(); ++i) {
1106 Real t = times[i];
1107 for (Size j = 0; j < moneyness.size(); ++j) {
1108 try {
1109 Real strike = moneyness[j] * forwards[i];
1110 calibrationInfo_->moneynessGridStrikes[i][j] = strike;
1111 Real stddev = std::sqrt(vol_->blackVariance(t, strike));
1112 callPricesMoneyness[i][j] = blackFormula(Option::Call, strike, forwards[i], stddev);
1113 calibrationInfo_->moneynessGridImpliedVolatility[i][j] = stddev / std::sqrt(t);
1114 if (moneyness[j] >= 1) {
1115 calibrationInfo_->moneynessCallPrices[i][j] = blackFormula(Option::Call, strike, forwards[i], stddev, domDisc[i]);
1116 } else {
1117 calibrationInfo_->moneynessPutPrices[i][j] = blackFormula(Option::Put, strike, forwards[i], stddev, domDisc[i]);
1118 };
1119 } catch (const std::exception& e) {
1120 TLOG("error for time " << t << " moneyness " << moneyness[j] << ": " << e.what());
1121 }
1122 }
1123 }
1124 if (!times.empty() && !moneyness.empty()) {
1125 try {
1126 QuantExt::CarrMadanSurface cm(times, moneyness, fxSpot_->value(), forwards,
1127 callPricesMoneyness);
1128 for (Size i = 0; i < times.size(); ++i) {
1129 calibrationInfo_->moneynessGridProb[i] = cm.timeSlices()[i].density();
1130 }
1131 calibrationInfo_->moneynessGridCallSpreadArbitrage = cm.callSpreadArbitrage();
1132 calibrationInfo_->moneynessGridButterflyArbitrage = cm.butterflyArbitrage();
1133 calibrationInfo_->moneynessGridCalendarArbitrage = cm.calendarArbitrage();
1134 if (!cm.arbitrageFree())
1135 calibrationInfo_->isArbitrageFree = false;
1136 TLOG("Moneyness surface Arbitrage analysis result:");
1138 } catch (const std::exception& e) {
1139 TLOG("error: " << e.what());
1140 calibrationInfo_->isArbitrageFree = false;
1141 }
1142 TLOG("Moneyness surface Arbitrage analysis completed:");
1143 }
1144 }
1145
1146 // the bfrr surface provides info on smiles with error, which we report here
1147
1148 if (reportOnDeltaGrid || reportOnMoneynessGrid) {
1149 if (auto bfrr = QuantLib::ext::dynamic_pointer_cast<QuantExt::BlackVolatilitySurfaceBFRR>(vol_)) {
1150 if (bfrr->deltas().size() != bfrr->currentDeltas().size()) {
1151 calibrationInfo_->messages.push_back(
1152 "Warning: Used only " + std::to_string(bfrr->currentDeltas().size()) + " deltas of the " +
1153 std::to_string(bfrr->deltas().size()) +
1154 " deltas that were initially provided, because all smiles were invalid.");
1155 }
1156 for (Size i = 0; i < bfrr->dates().size(); ++i) {
1157 if (bfrr->smileHasError()[i]) {
1158 calibrationInfo_->messages.push_back("Ignore invalid smile at expiry " +
1159 ore::data::to_string(bfrr->dates()[i]) + ": " +
1160 bfrr->smileErrorMessage()[i]);
1161 }
1162 }
1163 }
1164 }
1165
1166 DLOG("Building calibration info for fx vol surface completed.");
1167 }
1168
1169 } catch (std::exception& e) {
1170 QL_FAIL("fx vol curve building failed: " << e.what());
1171 } catch (...) {
1172 QL_FAIL("fx vol curve building failed: unknown error");
1173 }
1174}
1175} // namespace data
1176} // namespace ore
const std::vector< bool > & butterflyArbitrage() const
const std::vector< bool > & callSpreadArbitrage() const
const std::vector< Real > & density() const
const std::vector< std::vector< bool > > & butterflyArbitrage() const
const std::vector< std::vector< bool > > & calendarArbitrage() const
const std::vector< std::vector< bool > > & callSpreadArbitrage() const
const std::vector< CarrMadanMarginalProbability > & timeSlices() const
Container class for all Curve Configurations.
const std::string & curveConfigID() const
Definition: curvespec.hpp:83
Utility class for handling delta strings ATM, 10P, 25C, ... used e.g. for FX Surfaces.
Definition: strike.hpp:81
bool isCall() const
Definition: strike.hpp:86
bool isAtm() const
Definition: strike.hpp:84
Real delta() const
Definition: strike.hpp:87
bool isPut() const
Definition: strike.hpp:85
QuantLib::Handle< QuantLib::Quote > getQuote(const std::string &pair) const
void buildVannaVolgaOrATMCurve(Date asof, FXVolatilityCurveSpec spec, const Loader &loader, QuantLib::ext::shared_ptr< FXVolatilityCurveConfig > config, const FXTriangulation &fxSpots, const map< string, QuantLib::ext::shared_ptr< YieldCurve > > &yieldCurves)
Definition: fxvolcurve.cpp:393
QuantLib::DeltaVolQuote::AtmType atmType_
Definition: fxvolcurve.hpp:79
void buildSmileDeltaCurve(Date asof, FXVolatilityCurveSpec spec, const Loader &loader, QuantLib::ext::shared_ptr< FXVolatilityCurveConfig > config, const FXTriangulation &fxSpots, const map< string, QuantLib::ext::shared_ptr< YieldCurve > > &yieldCurves)
Definition: fxvolcurve.cpp:72
boost::optional< Wildcard > expiriesWildcard_
Definition: fxvolcurve.hpp:75
QuantLib::Period switchTenor_
Definition: fxvolcurve.hpp:78
QuantLib::DeltaVolQuote::DeltaType longTermDeltaType_
Definition: fxvolcurve.hpp:82
QuantLib::ext::shared_ptr< FxEqCommVolCalibrationInfo > calibrationInfo_
Definition: fxvolcurve.hpp:86
FXVolCurve()
Default constructor.
Definition: fxvolcurve.hpp:51
QuantLib::DeltaVolQuote::AtmType longTermAtmType_
Definition: fxvolcurve.hpp:81
void buildSmileAbsoluteCurve(Date asof, FXVolatilityCurveSpec spec, const Loader &loader, QuantLib::ext::shared_ptr< FXVolatilityCurveConfig > config, const FXTriangulation &fxSpots, const map< string, QuantLib::ext::shared_ptr< YieldCurve > > &yieldCurves)
Definition: fxvolcurve.cpp:547
void buildATMTriangulated(Date asof, FXVolatilityCurveSpec spec, const Loader &loader, QuantLib::ext::shared_ptr< FXVolatilityCurveConfig > config, const FXTriangulation &fxSpots, const map< string, QuantLib::ext::shared_ptr< YieldCurve > > &yieldCurves, const std::map< std::string, QuantLib::ext::shared_ptr< FXVolCurve > > &fxVols, const map< string, QuantLib::ext::shared_ptr< CorrelationCurve > > &correlationCurves)
Definition: fxvolcurve.cpp:720
QuantLib::DeltaVolQuote::DeltaType deltaType_
Definition: fxvolcurve.hpp:80
QuantLib::ext::shared_ptr< BlackVolTermStructure > vol_
Definition: fxvolcurve.hpp:69
void init(Date asof, FXVolatilityCurveSpec spec, const Loader &loader, const CurveConfigurations &curveConfigs, const FXTriangulation &fxSpots, const map< string, QuantLib::ext::shared_ptr< YieldCurve > > &yieldCurves, const std::map< string, QuantLib::ext::shared_ptr< FXVolCurve > > &fxVols, const map< string, QuantLib::ext::shared_ptr< CorrelationCurve > > &correlationCurves, const bool buildCalibrationInfo)
Definition: fxvolcurve.cpp:800
Handle< Quote > fxSpot_
Definition: fxvolcurve.hpp:70
void buildSmileBfRrCurve(Date asof, FXVolatilityCurveSpec spec, const Loader &loader, QuantLib::ext::shared_ptr< FXVolatilityCurveConfig > config, const FXTriangulation &fxSpots, const map< string, QuantLib::ext::shared_ptr< YieldCurve > > &yieldCurves)
Definition: fxvolcurve.cpp:249
std::vector< string > expiriesNoDuplicates_
Definition: fxvolcurve.hpp:73
std::vector< Period > expiries_
Definition: fxvolcurve.hpp:74
Handle< YieldTermStructure > forYts_
Definition: fxvolcurve.hpp:71
Handle< YieldTermStructure > domYts_
Definition: fxvolcurve.hpp:71
const FXVolatilityCurveSpec & spec() const
Definition: fxvolcurve.hpp:63
QuantLib::Option::Type riskReversalInFavorOf_
Definition: fxvolcurve.hpp:83
FX Volatility curve description.
Definition: curvespec.hpp:288
const string & ccy() const
Definition: curvespec.hpp:303
const string & unitCcy() const
Definition: curvespec.hpp:302
Market data loader base class.
Definition: loader.hpp:47
virtual QuantLib::ext::shared_ptr< MarketDatum > get(const std::string &name, const QuantLib::Date &d) const
get quote by its unique name, throws if not existent, override in derived classes for performance
Definition: loader.cpp:24
const boost::optional< bool > reportOnDeltaGrid() const
const boost::optional< std::vector< std::string > > & deltas() const
const boost::optional< bool > reportOnMoneynessGrid() const
const boost::optional< std::vector< Period > > & expiries() const
const boost::optional< std::vector< Real > > & moneyness() const
CurveSpec parser.
Wrapper class for building FX volatility structures.
QuantLib::ext::shared_ptr< CurveSpec > parseCurveSpec(const string &s)
function to convert a string into a curve spec
Calendar parseCalendar(const string &s)
Convert text to QuantLib::Calendar.
Definition: parsers.cpp:157
Strike parseStrike(const std::string &s)
Convert text to Strike.
Definition: strike.cpp:30
Period parsePeriod(const string &s)
Convert text to QuantLib::Period.
Definition: parsers.cpp:171
Map text representations to QuantLib/QuantExt types.
Classes and functions for log message handling.
@ data
Definition: log.hpp:77
#define LOG(text)
Logging Macro (Level = Notice)
Definition: log.hpp:552
#define DLOG(text)
Logging Macro (Level = Debug)
Definition: log.hpp:554
#define TLOGGERSTREAM(text)
Definition: log.hpp:633
#define WLOG(text)
Logging Macro (Level = Warning)
Definition: log.hpp:550
#define TLOG(text)
Logging Macro (Level = Data)
Definition: log.hpp:556
Real getAtmStrike(DeltaVolQuote::DeltaType dt, DeltaVolQuote::AtmType at, Real spot, Real domDiscount, Real forDiscount, boost::shared_ptr< BlackVolTermStructure > vol, Real t, Real accuracy, Size maxIterations)
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
RandomVariable variance(const RandomVariable &r)
std::string arbitrageAsString(const CarrMadanMarginalProbabilityClass &cm)
Real getStrikeFromDelta(Option::Type optionType, Real delta, DeltaVolQuote::DeltaType dt, Real spot, Real domDiscount, Real forDiscount, boost::shared_ptr< BlackVolTermStructure > vol, Real t, Real accuracy, Size maxIterations)
ReportConfig effectiveReportConfig(const ReportConfig &globalConfig, const ReportConfig &localConfig)
Size size(const ValueType &v)
Definition: value.cpp:145
std::string to_string(const LocationInfo &l)
Definition: ast.cpp:28
Handle< QuantExt::CorrelationTermStructure > getCorrelationCurve(const std::string &index1, const std::string &index2, const map< string, QuantLib::ext::shared_ptr< CorrelationCurve > > &correlationCurves)
Definition: fxvolcurve.cpp:665
std::string inverseFxIndex(const std::string &indexName)
Extrapolation parseExtrapolation(const string &s)
Parse Extrapolation from string.
Definition: parsers.cpp:778
boost::optional< Wildcard > getUniqueWildcard(const C &c)
checks if at most one element in C has a wild card and returns it in this case
Definition: wildcard.hpp:65
Serializable Credit Default Swap.
Definition: namespaces.docs:23
std::size_t count
QuantLib::Real value
Definition: strike.hpp:47
Error for market data or curve.
vector< Real > strikes
vector< string > curveConfigs
string conversion utilities