Skip to content

Commit c80424e

Browse files
committed
Merge branch 'QPR-11658' into 'master'
Resolve QPR-11658 (absolute strike fx vol surface) Closes QPR-11658 See merge request qs/ore!43
2 parents 702c451 + 7edc42d commit c80424e

10 files changed

Lines changed: 416 additions & 4 deletions

File tree

OREData/ored/configuration/fxvolcurveconfig.cpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const vector<string>& FXVolatilityCurveConfig::quotes() {
6363
quotes_.push_back(base + e + "/" + to_string(d) + "RR");
6464
quotes_.push_back(base + e + "/" + to_string(d) + "BF");
6565
}
66-
} else if (dimension_ == Dimension::SmileDelta) {
66+
} else if (dimension_ == Dimension::SmileDelta || dimension_ == Dimension::SmileAbsolute) {
6767
for (auto d : deltas_) {
6868
quotes_.push_back(base + e + "/" + d);
6969
}
@@ -166,6 +166,15 @@ void FXVolatilityCurveConfig::fromXML(XMLNode* node) {
166166
smileDelta_ = {10, 25};
167167
else
168168
smileDelta_ = parseListOfValues<Size>(sDelta, &parseInteger);
169+
} else if (smileType == "Absolute") {
170+
dimension_ = Dimension::SmileAbsolute;
171+
if (smileInterp == "" || smileInterp == "Cubic") {
172+
smileInterpolation_ = SmileInterpolation::Cubic;
173+
} else if (smileInterp == "Linear") {
174+
smileInterpolation_ = SmileInterpolation::Linear;
175+
} else {
176+
QL_FAIL("SmileInterpolation " << smileInterp << " not supported");
177+
}
169178
} else {
170179
QL_FAIL("SmileType '" << smileType << "' not supported, expected VannaVolga, Delta, BFRR");
171180
}
@@ -240,6 +249,17 @@ XMLNode* FXVolatilityCurveConfig::toXML(XMLDocument& doc) {
240249
}
241250
XMLUtils::addGenericChildAsList(doc, node, "SmileDelta", smileDelta_);
242251
XMLUtils::addChild(doc, node, "Conventions", to_string(conventionsID_));
252+
} else if (dimension_ == Dimension::SmileAbsolute) {
253+
XMLUtils::addChild(doc, node, "Dimension", "Smile");
254+
XMLUtils::addChild(doc, node, "SmileType", "Absolute");
255+
if (smileInterpolation_ == SmileInterpolation::Linear) {
256+
XMLUtils::addChild(doc, node, "SmileInterpolation", "Linear");
257+
} else if (smileInterpolation_ == SmileInterpolation::Cubic) {
258+
XMLUtils::addChild(doc, node, "SmileInterpolation", "Cubic");
259+
} else {
260+
QL_FAIL("Unknown SmileInterpolation in FXVolatilityCurveConfig::toXML()");
261+
}
262+
XMLUtils::addChild(doc, node, "Conventions", to_string(conventionsID_));
243263
} else {
244264
QL_FAIL("Unknown Dimension in FXVolatilityCurveConfig::toXML()");
245265
}

OREData/ored/configuration/fxvolcurveconfig.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class FXVolatilityCurveConfig : public CurveConfig {
5252
* as per Castagna& Mercurio(2006), to use. The second approximation is more accurate
5353
* but can ask for the square root of a negative number under unusual circumstances.
5454
*/
55-
enum class Dimension { ATM, SmileVannaVolga, SmileDelta, SmileBFRR, ATMTriangulated };
55+
enum class Dimension { ATM, SmileVannaVolga, SmileDelta, SmileBFRR, SmileAbsolute, ATMTriangulated };
5656
enum class SmileInterpolation {
5757
VannaVolga1,
5858
VannaVolga2,

OREData/ored/marketdata/fxvolcurve.cpp

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <qle/termstructures/blackdeltautilities.hpp>
2929
#include <qle/termstructures/blackinvertedvoltermstructure.hpp>
3030
#include <qle/termstructures/blacktriangulationatmvol.hpp>
31+
#include <qle/termstructures/blackvolsurfaceabsolute.hpp>
3132
#include <qle/termstructures/blackvolsurfacebfrr.hpp>
3233
#include <qle/termstructures/blackvolsurfacedelta.hpp>
3334
#include <qle/termstructures/fxblackvolsurface.hpp>
@@ -519,6 +520,123 @@ void FXVolCurve::buildVannaVolgaOrATMCurve(Date asof, FXVolatilityCurveSpec spec
519520
vol_->enableExtrapolation();
520521
}
521522

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+
522640
Handle<QuantExt::CorrelationTermStructure>
523641
getCorrelationCurve(const std::string& index1, const std::string& index2,
524642
const map<string, boost::shared_ptr<CorrelationCurve>>& correlationCurves) {
@@ -670,7 +788,8 @@ void FXVolCurve::init(Date asof, FXVolatilityCurveSpec spec, const Loader& loade
670788
config->dimension() == FXVolatilityCurveConfig::Dimension::ATMTriangulated ||
671789
config->dimension() == FXVolatilityCurveConfig::Dimension::SmileVannaVolga ||
672790
config->dimension() == FXVolatilityCurveConfig::Dimension::SmileDelta ||
673-
config->dimension() == FXVolatilityCurveConfig::Dimension::SmileBFRR,
791+
config->dimension() == FXVolatilityCurveConfig::Dimension::SmileBFRR ||
792+
config->dimension() == FXVolatilityCurveConfig::Dimension::SmileAbsolute,
674793
"Unknown FX curve building dimension");
675794

676795
expiriesWildcard_ = getUniqueWildcard(config->expiries());
@@ -782,6 +901,8 @@ void FXVolCurve::init(Date asof, FXVolatilityCurveSpec spec, const Loader& loade
782901
buildSmileBfRrCurve(asof, spec, loader, config, fxSpots, yieldCurves);
783902
} else if (config->dimension() == FXVolatilityCurveConfig::Dimension::ATMTriangulated) {
784903
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);
785906
} else {
786907
buildVannaVolgaOrATMCurve(asof, spec, loader, config, fxSpots, yieldCurves);
787908
}

OREData/ored/marketdata/fxvolcurve.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ class FXVolCurve {
109109
boost::shared_ptr<FXVolatilityCurveConfig> config, const FXTriangulation& fxSpots,
110110
const map<string, boost::shared_ptr<YieldCurve>>& yieldCurves);
111111

112+
void buildSmileAbsoluteCurve(Date asof, FXVolatilityCurveSpec spec, const Loader& loader,
113+
boost::shared_ptr<FXVolatilityCurveConfig> config, const FXTriangulation& fxSpots,
114+
const map<string, boost::shared_ptr<YieldCurve>>& yieldCurves);
115+
112116
};
113117
} // namespace data
114118
} // namespace ore

OREData/ored/marketdata/marketdatum.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1188,7 +1188,7 @@ class FXOptionQuote : public MarketDatum {
11881188

11891189
Strike s = parseStrike(strike);
11901190
QL_REQUIRE(s.type == Strike::Type::DeltaCall || s.type == Strike::Type::DeltaPut ||
1191-
s.type == Strike::Type::ATM || s.type == Strike::Type::BF || s.type == Strike::Type::RR,
1191+
s.type == Strike::Type::ATM || s.type == Strike::Type::BF || s.type == Strike::Type::RR || s.type == Strike::Type::Absolute,
11921192
"Unsupported FXOptionQuote strike (" << strike << ")");
11931193
}
11941194

QuantExt/qle/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ termstructures/blackvariancesurfacemoneyness.cpp
271271
termstructures/blackvariancesurfacesparse.cpp
272272
termstructures/blackvariancesurfacestddevs.cpp
273273
termstructures/blackvolconstantspread.cpp
274+
termstructures/blackvolsurfaceabsolute.cpp
274275
termstructures/blackvolsurfacebfrr.cpp
275276
termstructures/blackvolsurfacedelta.cpp
276277
termstructures/blackvolsurfaceproxy.cpp
@@ -734,6 +735,7 @@ termstructures/blackvariancesurfacemoneyness.hpp
734735
termstructures/blackvariancesurfacesparse.hpp
735736
termstructures/blackvariancesurfacestddevs.hpp
736737
termstructures/blackvolconstantspread.hpp
738+
termstructures/blackvolsurfaceabsolute.hpp
737739
termstructures/blackvolsurfacebfrr.hpp
738740
termstructures/blackvolsurfacedelta.hpp
739741
termstructures/blackvolsurfaceproxy.hpp

QuantExt/qle/quantext.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@
391391
#include <qle/termstructures/blackvariancesurfacesparse.hpp>
392392
#include <qle/termstructures/blackvariancesurfacestddevs.hpp>
393393
#include <qle/termstructures/blackvolconstantspread.hpp>
394+
#include <qle/termstructures/blackvolsurfaceabsolute.hpp>
394395
#include <qle/termstructures/blackvolsurfacebfrr.hpp>
395396
#include <qle/termstructures/blackvolsurfacedelta.hpp>
396397
#include <qle/termstructures/blackvolsurfaceproxy.hpp>

0 commit comments

Comments
 (0)