Skip to content

Commit 306727f

Browse files
QPR-12825 Fix bug in optionelet sensis when the underlying surface is a proxy
1 parent 6d906cd commit 306727f

7 files changed

Lines changed: 196 additions & 41 deletions

File tree

OREAnalytics/orea/engine/parsensitivityinstrumentbuilder.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ void ParSensitivityInstrumentBuilder::createParInstruments(
434434
Size n_expiries = data.shiftExpiries.size();
435435

436436
// Determine if the cap floor is ATM
437-
bool isAtm = data.shiftStrikes.size() == 1 && data.shiftStrikes[0] == 0.0 && data.isRelative;
437+
bool isAtm = data.shiftStrikes.size() == 1 && data.shiftStrikes[0] == 0.0;
438438

439439
for (Size j = 0; j < n_strikes; ++j) {
440440
Real strike = data.shiftStrikes[j];

OREAnalytics/orea/scenario/scenariosimmarket.cpp

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,9 @@ ScenarioSimMarket::ScenarioSimMarket(
798798
}
799799
DLOG("Initial market " << name << " yield volatility type = " << wrapper->volatilityType());
800800

801-
auto proxy = QuantLib::ext::dynamic_pointer_cast<ProxySwaptionVolatility>(*wrapper);
801+
bool stickySabr = smileDynamics == "StickySABR";
802+
auto proxy = stickySabr || !useSpreadedTermStructures_ ?
803+
QuantLib::ext::dynamic_pointer_cast<ProxySwaptionVolatility>(*wrapper) : nullptr;
802804
if (proxy) {
803805
DLOG("Detected ProxySwaptionVolatility for " << name);
804806
wrapper.linkTo(*proxy->baseVol());
@@ -815,7 +817,6 @@ ScenarioSimMarket::ScenarioSimMarket(
815817
if (param.second.first) {
816818
DLOG("Simulating yield vols for ccy " << name);
817819
DLOG("YieldVol simulate atm only : " << (simulateAtmOnly ? "True" : "False"));
818-
bool stickySabr = smileDynamics == "StickySABR";
819820
bool stickyStrike = smileDynamics == "StickyStrike";
820821

821822
if (simulateAtmOnly) {
@@ -1163,15 +1164,20 @@ ScenarioSimMarket::ScenarioSimMarket(
11631164
try {
11641165
LOG("building " << name << " cap/floor volatility curve...");
11651166
RelinkableHandle<OptionletVolatilityStructure> wrapper;
1167+
1168+
bool stickySabr = parameters->capFloorVolSmileDynamics(name) == "StickySABR";
11661169
QuantLib::ext::shared_ptr<ProxyOptionletVolatility> proxy;
1167-
proxy = QuantLib::ext::dynamic_pointer_cast<ProxyOptionletVolatility>(
1168-
*initMarket->capFloorVol(name, configuration));
1170+
proxy = stickySabr || !useSpreadedTermStructures_ ?
1171+
QuantLib::ext::dynamic_pointer_cast<ProxyOptionletVolatility>(
1172+
*initMarket->capFloorVol(name, configuration))
1173+
: nullptr;
11691174
if (proxy) {
11701175
DLOG("Detected ProxyOptionletVolatility for " << name);
11711176
wrapper.linkTo(*proxy->baseVol());
11721177
} else {
11731178
wrapper.linkTo(*initMarket->capFloorVol(name, configuration));
11741179
}
1180+
11751181
auto [iborIndexName, rateComputationPeriod] =
11761182
initMarket->capFloorVolIndexBase(name, configuration);
11771183
QuantLib::ext::shared_ptr<IborIndex> iborIndex =
@@ -1217,7 +1223,6 @@ ScenarioSimMarket::ScenarioSimMarket(
12171223
vector<Period> optionTenors = parameters->capFloorVolExpiries(name);
12181224
vector<Date> optionDates(optionTenors.size());
12191225

1220-
bool stickySabr = parameters->capFloorVolSmileDynamics(name) == "StickySABR";
12211226
vector<vector<Real>> strikesSabr;
12221227
vector<vector<Handle<Quote>>> volSpreadsSabr;
12231228

@@ -1255,15 +1260,17 @@ ScenarioSimMarket::ScenarioSimMarket(
12551260
isAtm = true;
12561261
}
12571262

1263+
vector<vector<Real>> strikesProxyAdjusted(optionTenors.size(), strikes);
12581264
vector<vector<Handle<Quote>>> quotes(
12591265
optionTenors.size(), vector<Handle<Quote>>(strikes.size(), Handle<Quote>()));
12601266

12611267
DLOG("cap floor use adjusted option pillars = " << std::boolalpha << parameters_->capFloorVolAdjustOptionletPillars());
12621268
DLOG("have ibor index = " << std::boolalpha << (iborIndex != nullptr));
12631269

12641270
vector<Rate> atmStrikes(optionTenors.size(), Null<Rate>());
1271+
auto atmStrikesProxyAdjusted = atmStrikes;
12651272
vector<Rate> atmVols(optionTenors.size(), Null<Rate>());
1266-
for (Size i = 0; i < optionTenors.size(); ++i) {
1273+
for (Size i = 0, index = 0; i < optionTenors.size(); ++i) {
12671274

12681275
if (parameters_->capFloorVolAdjustOptionletPillars() && iborIndex) {
12691276
// If we ask for cap pillars at tenors t_i for i = 1,...,N, we should attempt to
@@ -1355,9 +1362,33 @@ ScenarioSimMarket::ScenarioSimMarket(
13551362
}
13561363
}
13571364
}
1365+
1366+
Real proxyAdjustment = 0.0;
1367+
if (proxy) {
1368+
Real baseAtmLevel = proxy->getAtmLevel(optionDates[i], proxy->baseIndex(),
1369+
proxy->baseRateComputationPeriod());
1370+
DLOG("Base ATM level from proxy for option tenor " << optionTenors[i]
1371+
<< " is " << baseAtmLevel);
1372+
Real targetAtmLevel = proxy->getAtmLevel(optionDates[i], proxy->targetIndex(),
1373+
proxy->targetRateComputationPeriod());
1374+
DLOG("Target ATM level from proxy for option tenor " << optionTenors[i]
1375+
<< " is " << targetAtmLevel);
1376+
proxyAdjustment = -(targetAtmLevel - baseAtmLevel);
1377+
DLOG("Adjusted strikes for option tenor " << optionTenors[i]
1378+
<< " by proxy adjustment of "
1379+
<< proxyAdjustment);
1380+
}
1381+
for (Size j = 0; j < strikesProxyAdjusted[i].size(); ++j) {
1382+
strikesProxyAdjusted[i][j] = strikes[j] + proxyAdjustment;
1383+
if (!close_enough(proxyAdjustment, 0.0))
1384+
DLOG(" adjusted strike from " << strikes[j] << " to " << strikesProxyAdjusted[i][j]);
1385+
}
1386+
atmStrikesProxyAdjusted[i] = atmStrikes[i] + proxyAdjustment;
1387+
if (!close_enough(proxyAdjustment, 0.0))
1388+
DLOG(" adjusted ATM strike from " << atmStrikes[i] << " to " << atmStrikesProxyAdjusted[i]);
13581389

1359-
for (Size j = 0; j < strikes.size(); ++j) {
1360-
Real strike = isAtm ? atmStrikes[i] : strikes[j];
1390+
for (Size j = 0; j < strikesProxyAdjusted[i].size(); ++j, ++index) {
1391+
Real strike = isAtm ? atmStrikesProxyAdjusted[i] : strikesProxyAdjusted[i][j];
13611392
Real vol =
13621393
wrapper->volatility(optionDates[i], strike, true);
13631394
if (isAtm)
@@ -1367,7 +1398,7 @@ ScenarioSimMarket::ScenarioSimMarket(
13671398
<< std::setprecision(12) << vol);
13681399
QuantLib::ext::shared_ptr<SimpleQuote> q =
13691400
QuantLib::ext::make_shared<SimpleQuote>(useSpreadedTermStructures_ ? 0.0 : vol);
1370-
Size index = i * strikes.size() + j;
1401+
13711402
simDataTmp.emplace(std::piecewise_construct,
13721403
std::forward_as_tuple(param.first, name, index),
13731404
std::forward_as_tuple(q));
@@ -1378,7 +1409,7 @@ ScenarioSimMarket::ScenarioSimMarket(
13781409
}
13791410
quotes[i][j] = Handle<Quote>(q);
13801411
}
1381-
if (i < strikesSabr.size()) {
1412+
if (!strikesSabr.empty()) {
13821413
for (Size j = 0; j < strikesSabr[i].size(); ++j) {
13831414
QL_REQUIRE(quotes[i].size() == 1,
13841415
"SSM internal error: expected quotes size 1 for stickySabr");
@@ -1415,14 +1446,31 @@ ScenarioSimMarket::ScenarioSimMarket(
14151446
QuantLib::ext::shared_ptr<QuantLib::StrippedOptionlet> optionlet;
14161447

14171448
if (useSpreadedTermStructures_) {
1418-
hCapletVol = Handle<OptionletVolatilityStructure>(
1419-
QuantLib::ext::make_shared<QuantExt::SpreadedOptionletVolatility2>(wrapper, optionDates,
1420-
strikes, quotes));
1449+
1450+
if (proxy) {
1451+
// Use AtmAdjustedSpreadedOptionletVolatility2 which adjusts strike level in the volSpread matrix
1452+
// according to difference in ATM levels when a smileSection is queried
1453+
hCapletVol = Handle<OptionletVolatilityStructure>(
1454+
QuantLib::ext::make_shared<AtmAdjustedSpreadedOptionletVolatility2>(wrapper,
1455+
optionDates,
1456+
strikes,
1457+
quotes,
1458+
proxy->baseIndex(),
1459+
proxy->targetIndex(),
1460+
proxy->baseRateComputationPeriod(),
1461+
proxy->targetRateComputationPeriod(),
1462+
proxy->scalingFactor()));
1463+
} else {
1464+
hCapletVol = Handle<OptionletVolatilityStructure>(
1465+
QuantLib::ext::make_shared<QuantExt::SpreadedOptionletVolatility2>(wrapper, optionDates,
1466+
strikes, quotes));
1467+
}
1468+
14211469
if (stickySabr) {
14221470
auto strikeVec = vector<vector<Real>>(optionDates.size());
14231471
auto optionletQuotes = vector<vector<Handle<Quote>>>(optionDates.size());
14241472
for (Size i = 0; i < optionDates.size(); ++i) {
1425-
strikeVec[i].push_back(atmStrikes[i]);
1473+
strikeVec[i].push_back(atmStrikesProxyAdjusted[i]);
14261474
optionletQuotes[i] = vector<Handle<Quote>>(1);
14271475
optionletQuotes[i][0] = Handle<Quote>(ext::make_shared<SimpleQuote>(0.0));
14281476
}
@@ -1447,7 +1495,7 @@ ScenarioSimMarket::ScenarioSimMarket(
14471495
}
14481496
optionlet = QuantLib::ext::make_shared<QuantLib::StrippedOptionlet>(
14491497
settleDays, wrapper->calendar(), wrapper->businessDayConvention(), iborIndex,
1450-
optionDates, strikes, quotes, dc, wrapper->volatilityType(),
1498+
optionDates, strikesProxyAdjusted, quotes, dc, wrapper->volatilityType(),
14511499
wrapper->displacement());
14521500
if (!stickySabr) {
14531501
hCapletVol = Handle<OptionletVolatilityStructure>(

OREAnalytics/orea/scenario/scenariosimmarketparameters.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,7 @@ void ScenarioSimMarketParameters::fromXML(XMLNode* root) {
887887
if (atmOnlyNode)
888888
swapVolSimulateATMOnly_ = XMLUtils::getChildValueAsBool(nodeChild, "SimulateATMOnly", true);
889889

890-
if (!swapVolSimulateATMOnly_) {
890+
if (!swapVolSimulateATMOnly_) {
891891
vector<XMLNode*> spreadNodes = XMLUtils::getChildrenNodes(nodeChild, "StrikeSpreads");
892892
if (spreadNodes.size() > 0) {
893893
keysCheck = set<string>(keys.begin(), keys.end());

QuantExt/qle/termstructures/proxyoptionletvolatility.cpp

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,8 @@ QuantLib::ext::shared_ptr<SmileSection> ProxyOptionletVolatility::smileSectionIm
7878

7979
// compute the base and target forward rate levels
8080

81-
Real baseAtmLevel;
82-
if (isOis(baseIndex_))
83-
baseAtmLevel = getOisAtmLevel(QuantLib::ext::dynamic_pointer_cast<OvernightIndex>(baseIndex_),
84-
baseIndex_->fixingCalendar().adjust(fixingDate), baseRateComputationPeriod_);
85-
else if (isBMA(baseIndex_))
86-
baseAtmLevel = getBMAAtmLevel(QuantLib::ext::dynamic_pointer_cast<BMAIndexWrapper>(baseIndex_)->bma(),
87-
baseIndex_->fixingCalendar().adjust(fixingDate), baseRateComputationPeriod_);
88-
else
89-
baseAtmLevel = baseIndex_->fixing(baseIndex_->fixingCalendar().adjust(fixingDate));
90-
91-
Real targetAtmLevel;
92-
if (isOis(targetIndex_))
93-
targetAtmLevel =
94-
getOisAtmLevel(QuantLib::ext::dynamic_pointer_cast<OvernightIndex>(targetIndex_),
95-
targetIndex_->fixingCalendar().adjust(fixingDate), targetRateComputationPeriod_);
96-
else if (isBMA(targetIndex_))
97-
targetAtmLevel =
98-
getBMAAtmLevel(QuantLib::ext::dynamic_pointer_cast<BMAIndexWrapper>(targetIndex_)->bma(),
99-
targetIndex_->fixingCalendar().adjust(fixingDate), targetRateComputationPeriod_);
100-
else
101-
targetAtmLevel = targetIndex_->fixing(targetIndex_->fixingCalendar().adjust(fixingDate));
81+
Real baseAtmLevel = ProxyOptionletVolatility::getAtmLevel(fixingDate, baseIndex_, baseRateComputationPeriod_);
82+
Real targetAtmLevel = ProxyOptionletVolatility::getAtmLevel(fixingDate, targetIndex_, targetRateComputationPeriod_);
10283

10384
// build the atm-adjusted smile section and return it
10485

@@ -111,4 +92,18 @@ Volatility ProxyOptionletVolatility::volatilityImpl(Time optionTime, Rate strike
11192
return smileSection(optionTime)->volatility(strike) * scalingFactor_;
11293
}
11394

95+
Real ProxyOptionletVolatility::getAtmLevel(
96+
const QuantLib::Date& fixingDate,
97+
const QuantLib::ext::shared_ptr<QuantLib::IborIndex>& index,
98+
const QuantLib::Period& rateComputationPeriod) {
99+
if (isOis(index))
100+
return getOisAtmLevel(QuantLib::ext::dynamic_pointer_cast<OvernightIndex>(index),
101+
index->fixingCalendar().adjust(fixingDate), rateComputationPeriod);
102+
else if (isBMA(index))
103+
return getBMAAtmLevel(QuantLib::ext::dynamic_pointer_cast<BMAIndexWrapper>(index)->bma(),
104+
index->fixingCalendar().adjust(fixingDate), rateComputationPeriod);
105+
else
106+
return index->fixing(index->fixingCalendar().adjust(fixingDate));
107+
}
108+
114109
} // namespace QuantExt

QuantExt/qle/termstructures/proxyoptionletvolatility.hpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#pragma once
2525

26+
#include <ql/indexes/iborindex.hpp>
2627
#include <ql/termstructures/volatility/optionlet/optionletvolatilitystructure.hpp>
2728

2829
namespace QuantExt {
@@ -40,9 +41,9 @@ class ProxyOptionletVolatility : public QuantLib::OptionletVolatilityStructure {
4041
QuantLib::Rate maxStrike() const override { return baseVol_->maxStrike(); }
4142
QuantLib::Date maxDate() const override { return baseVol_->maxDate(); }
4243
const QuantLib::Date& referenceDate() const override { return baseVol_->referenceDate(); }
43-
VolatilityType volatilityType() const override { return baseVol_->volatilityType(); }
44-
Real displacement() const override { return baseVol_->displacement(); }
45-
Calendar calendar() const override { return baseVol_->calendar(); }
44+
QuantLib::VolatilityType volatilityType() const override { return baseVol_->volatilityType(); }
45+
QuantLib::Real displacement() const override { return baseVol_->displacement(); }
46+
QuantLib::Calendar calendar() const override { return baseVol_->calendar(); }
4647

4748
const QuantLib::Handle<QuantLib::OptionletVolatilityStructure>& baseVol() const { return baseVol_; }
4849
const QuantLib::ext::shared_ptr<QuantLib::IborIndex>& baseIndex() const { return baseIndex_; }
@@ -51,6 +52,10 @@ class ProxyOptionletVolatility : public QuantLib::OptionletVolatilityStructure {
5152
const QuantLib::Period& targetRateComputationPeriod() const { return targetRateComputationPeriod_; }
5253
double scalingFactor() const { return scalingFactor_; }
5354

55+
static QuantLib::Real getAtmLevel(const QuantLib::Date& fixingDate,
56+
const QuantLib::ext::shared_ptr<QuantLib::IborIndex>& index,
57+
const QuantLib::Period& rateComputationPeriod);
58+
5459
private:
5560
QuantLib::ext::shared_ptr<QuantLib::SmileSection> smileSectionImpl(const QuantLib::Date& optionDate) const override;
5661
QuantLib::ext::shared_ptr<QuantLib::SmileSection> smileSectionImpl(QuantLib::Time optionTime) const override;

0 commit comments

Comments
 (0)