|
28 | 28 | #include <qle/termstructures/blackdeltautilities.hpp> |
29 | 29 | #include <qle/termstructures/blackinvertedvoltermstructure.hpp> |
30 | 30 | #include <qle/termstructures/blacktriangulationatmvol.hpp> |
| 31 | +#include <qle/termstructures/blackvolsurfaceabsolute.hpp> |
31 | 32 | #include <qle/termstructures/blackvolsurfacebfrr.hpp> |
32 | 33 | #include <qle/termstructures/blackvolsurfacedelta.hpp> |
33 | 34 | #include <qle/termstructures/fxblackvolsurface.hpp> |
@@ -519,6 +520,123 @@ void FXVolCurve::buildVannaVolgaOrATMCurve(Date asof, FXVolatilityCurveSpec spec |
519 | 520 | vol_->enableExtrapolation(); |
520 | 521 | } |
521 | 522 |
|
| 523 | +void FXVolCurve::buildSmileAbsoluteCurve(Date asof, FXVolatilityCurveSpec spec, const Loader& loader, |
| 524 | + boost::shared_ptr<FXVolatilityCurveConfig> config, const FXTriangulation& fxSpots, |
| 525 | + const map<string, boost::shared_ptr<YieldCurve>>& yieldCurves) { |
| 526 | + |
| 527 | + // collect relevant market data and populate expiries (as per regex or configured list) |
| 528 | + std::set<Period> expiriesTmp; |
| 529 | + |
| 530 | + std::vector<boost::shared_ptr<FXOptionQuote>> data; |
| 531 | + std::ostringstream ss; |
| 532 | + ss << MarketDatum::InstrumentType::FX_OPTION << "/RATE_LNVOL/" << spec.unitCcy() << "/" << spec.ccy() << "/*"; |
| 533 | + Wildcard w(ss.str()); |
| 534 | + for (const auto& md : loader.get(w, asof)) { |
| 535 | + boost::shared_ptr<FXOptionQuote> q = boost::dynamic_pointer_cast<FXOptionQuote>(md); |
| 536 | + QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to FXOptionQuote"); |
| 537 | + QL_REQUIRE(q->unitCcy() == spec.unitCcy(), "FXOptionQuote unit ccy '" << q->unitCcy() |
| 538 | + << "' <> FXVolatilityCurveSpec unit ccy '" |
| 539 | + << spec.unitCcy() << "'"); |
| 540 | + QL_REQUIRE(q->ccy() == spec.ccy(), |
| 541 | + "FXOptionQuote ccy '" << q->ccy() << "' <> FXVolatilityCurveSpec ccy '" << spec.ccy() << "'"); |
| 542 | + Strike s = parseStrike(q->strike()); |
| 543 | + if (s.type == Strike::Type::Absolute) { |
| 544 | + vector<string> tokens; |
| 545 | + boost::split(tokens, md->name(), boost::is_any_of("/")); |
| 546 | + QL_REQUIRE(tokens.size() == 6, "6 tokens expected in " << md->name()); |
| 547 | + if (expiriesWildcard_ && expiriesWildcard_->matches(tokens[4])) |
| 548 | + expiriesTmp.insert(q->expiry()); |
| 549 | + data.push_back(q); |
| 550 | + } |
| 551 | + } |
| 552 | + |
| 553 | + if (!expiriesWildcard_) { |
| 554 | + auto tmp = parseVectorOfValues<Period>(expiriesNoDuplicates_, &parsePeriod); |
| 555 | + expiriesTmp = std::set<Period>(tmp.begin(), tmp.end()); |
| 556 | + } |
| 557 | + |
| 558 | + // populate quotes |
| 559 | + std::vector<std::map<Real, Real>> strikeQuotesTmp(expiriesTmp.size()); |
| 560 | + |
| 561 | + for (auto const& q : data) { |
| 562 | + Size expiryIdx = std::distance(expiriesTmp.begin(), expiriesTmp.find(q->expiry())); |
| 563 | + if (expiryIdx >= expiriesTmp.size()) |
| 564 | + continue; |
| 565 | + Strike s = parseStrike(q->strike()); |
| 566 | + // If the strike for expirtIdx does not exist, read in the quote |
| 567 | + if (strikeQuotesTmp[expiryIdx].count(s.value) == 0) |
| 568 | + strikeQuotesTmp[expiryIdx][s.value] = q->quote()->value(); |
| 569 | + } |
| 570 | + |
| 571 | + // identify the expiries with at least one strike quote |
| 572 | + std::vector<bool> dataComplete(expiriesTmp.size(), true); |
| 573 | + |
| 574 | + for (Size i = 0; i < expiriesTmp.size(); ++i) { |
| 575 | + if (strikeQuotesTmp[i].empty()) |
| 576 | + dataComplete[i] = false; |
| 577 | + } |
| 578 | + |
| 579 | + // if we have an explicitly configured expiry list, we require that there is at least one strike quote for all expiries |
| 580 | + |
| 581 | + if (!expiriesWildcard_) { |
| 582 | + Size i = 0; |
| 583 | + for (auto const& e : expiriesTmp) { |
| 584 | + QL_REQUIRE(dataComplete[i++], "Absolute FX vol surface: missing data for expiry " << e); |
| 585 | + } |
| 586 | + } |
| 587 | + |
| 588 | + // build the final quotes for the expiries that have complete data |
| 589 | + Size i = 0; |
| 590 | + for (auto const& e : expiriesTmp) { |
| 591 | + if (dataComplete[i++]) { |
| 592 | + expiries_.push_back(e); |
| 593 | + TLOG("adding expiry " << e << " with at least one strike quote"); |
| 594 | + } else { |
| 595 | + TLOG("removing expiry " << e << ", no strike quote found"); |
| 596 | + } |
| 597 | + } |
| 598 | + |
| 599 | + std::vector<std::vector<Real>> strikeQuotes, strikes; |
| 600 | + std::vector<Real> strikeQuote, strike; |
| 601 | + |
| 602 | + for (Size i = 0; i < expiriesTmp.size(); ++i) { |
| 603 | + if (!dataComplete[i]) |
| 604 | + continue; |
| 605 | + for (auto const& quote : strikeQuotesTmp[i]) { |
| 606 | + strike.push_back(quote.first); |
| 607 | + strikeQuote.push_back(quote.second); |
| 608 | + } |
| 609 | + strikeQuotes.push_back(strikeQuote); |
| 610 | + strikes.push_back(strike); |
| 611 | + strikeQuote.clear(); |
| 612 | + strike.clear(); |
| 613 | + } |
| 614 | + |
| 615 | + // build Absolute surface |
| 616 | + |
| 617 | + DLOG("build Absolute fx vol surface with " << expiries_.size() << " expiries"); |
| 618 | + |
| 619 | + QuantExt::BlackVolatilitySurfaceAbsolute::SmileInterpolation interp; |
| 620 | + if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::Linear) |
| 621 | + interp = QuantExt::BlackVolatilitySurfaceAbsolute::SmileInterpolation::Linear; |
| 622 | + else if (config->smileInterpolation() == FXVolatilityCurveConfig::SmileInterpolation::Cubic) |
| 623 | + interp = QuantExt::BlackVolatilitySurfaceAbsolute::SmileInterpolation::Cubic; |
| 624 | + else { |
| 625 | + QL_FAIL("Absolute FX vol surface: invalid interpolation, expected Linear, Cubic"); |
| 626 | + } |
| 627 | + |
| 628 | + std::vector<Date> dates; |
| 629 | + std::transform(expiries_.begin(), expiries_.end(), std::back_inserter(dates), |
| 630 | + [&asof, &config](const Period& p) { return config->calendar().advance(asof, p); }); |
| 631 | + |
| 632 | + vol_ = boost::make_shared<QuantExt::BlackVolatilitySurfaceAbsolute>( |
| 633 | + asof, dates, strikes, strikeQuotes, config->dayCounter(), config->calendar(), |
| 634 | + fxSpot_, spotDays_, spotCalendar_, domYts_, forYts_, deltaType_, atmType_, switchTenor_, longTermDeltaType_, |
| 635 | + longTermAtmType_, interp); |
| 636 | + |
| 637 | + vol_->enableExtrapolation(); |
| 638 | +} |
| 639 | + |
522 | 640 | Handle<QuantExt::CorrelationTermStructure> |
523 | 641 | getCorrelationCurve(const std::string& index1, const std::string& index2, |
524 | 642 | const map<string, boost::shared_ptr<CorrelationCurve>>& correlationCurves) { |
@@ -670,7 +788,8 @@ void FXVolCurve::init(Date asof, FXVolatilityCurveSpec spec, const Loader& loade |
670 | 788 | config->dimension() == FXVolatilityCurveConfig::Dimension::ATMTriangulated || |
671 | 789 | config->dimension() == FXVolatilityCurveConfig::Dimension::SmileVannaVolga || |
672 | 790 | config->dimension() == FXVolatilityCurveConfig::Dimension::SmileDelta || |
673 | | - config->dimension() == FXVolatilityCurveConfig::Dimension::SmileBFRR, |
| 791 | + config->dimension() == FXVolatilityCurveConfig::Dimension::SmileBFRR || |
| 792 | + config->dimension() == FXVolatilityCurveConfig::Dimension::SmileAbsolute, |
674 | 793 | "Unknown FX curve building dimension"); |
675 | 794 |
|
676 | 795 | expiriesWildcard_ = getUniqueWildcard(config->expiries()); |
@@ -782,6 +901,8 @@ void FXVolCurve::init(Date asof, FXVolatilityCurveSpec spec, const Loader& loade |
782 | 901 | buildSmileBfRrCurve(asof, spec, loader, config, fxSpots, yieldCurves); |
783 | 902 | } else if (config->dimension() == FXVolatilityCurveConfig::Dimension::ATMTriangulated) { |
784 | 903 | buildATMTriangulated(asof, spec, loader, config, fxSpots, yieldCurves, fxVols, correlationCurves); |
| 904 | + } else if (config->dimension() == FXVolatilityCurveConfig::Dimension::SmileAbsolute) { |
| 905 | + buildSmileAbsoluteCurve(asof, spec, loader, config, fxSpots, yieldCurves); |
785 | 906 | } else { |
786 | 907 | buildVannaVolgaOrATMCurve(asof, spec, loader, config, fxSpots, yieldCurves); |
787 | 908 | } |
|
0 commit comments