Skip to content

Commit 52a6c32

Browse files
QPR-12825 Add StickySABR sensitivity calculation for swaption, yield and capfloor vol
1 parent 1d43708 commit 52a6c32

10 files changed

Lines changed: 347 additions & 33 deletions

File tree

Docs/UserGuide/parameterisation/simulation.tex

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -665,15 +665,22 @@ \subsubsection{Market}\label{sec:sim_market}
665665
\item Extrapolation: This can be FlatFwd or FlatZero. If not given, the value defaults to FlatFwd.
666666
\end{itemize}
667667
668-
For swap, yield, interest cap-floor, yoy inflation cap-floor, zc inflation cap-floor, cds, fx, equity, commodity
669-
volatilities the smile dynamics can be specified as shown in listing \ref{lst:smile_dynamics_configuration} for swap
668+
For swaption, yield, interest rate cap-floor, yoy inflation cap-floor, zc inflation cap-floor, cds, fx, equity, commodity
669+
volatilities the smile dynamics can be specified as shown in listing \ref{lst:smile_dynamics_configuration} for swaption
670670
vols. The empty key serves as a default configuration for all keys for which no own smile dynamics node is present. The
671-
allowed smile dynamics values are StickyStrike and StickyMoneyness. If not given, the smile dynamics defaults to
672-
StickyStrike.
671+
allowed smile dynamics values are StickyStrike, StickyMoneyness and StickySABR.
672+
If not given, the smile dynamics defaults to StickyStrike.
673+
674+
Note that StickySABR is only available for swaption volatilities, yield volatilities and interest rate cap-floor volatilities,
675+
and can only be used if the corresponding T0 surface has been calibrated using a SABR model.
676+
The SABR volatility surface in the simulation market is recalibrated to the expiry/term/strike grid specified in the simulation
677+
configuration, using the initial SABR configuration from the T0 surface. Any subsequent recalibration will keep all SABR parameters
678+
except for $\alpha$ fixed.
673679
\begin{listing}
674680
\begin{minted}[fontsize=\footnotesize]{xml}
675681
<SmileDynamics key="">StickyStrike</SmileDynamics>
676682
<SmileDynamics key="EUR-ESTER">StickyMoneyness</SmileDynamics>
683+
<SmileDynamics key="USD-SOFR-3M">StickySABR</SmileDynamics>
677684
\end{minted}
678685
\caption{Smile Configuration Node}
679686
\label{lst:smile_dynamics_configuration}

OREAnalytics/orea/scenario/scenariosimmarket.cpp

Lines changed: 203 additions & 15 deletions
Large diffs are not rendered by default.

QuantExt/qle/termstructures/proxyoptionletvolatility.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ class ProxyOptionletVolatility : public QuantLib::OptionletVolatilityStructure {
4444
Real displacement() const override { return baseVol_->displacement(); }
4545
Calendar calendar() const override { return baseVol_->calendar(); }
4646

47+
const QuantLib::Handle<QuantLib::OptionletVolatilityStructure>& baseVol() const { return baseVol_; }
48+
const QuantLib::ext::shared_ptr<QuantLib::IborIndex>& baseIndex() const { return baseIndex_; }
49+
const QuantLib::ext::shared_ptr<QuantLib::IborIndex>& targetIndex() const { return targetIndex_; }
50+
const QuantLib::Period& baseRateComputationPeriod() const { return baseRateComputationPeriod_; }
51+
const QuantLib::Period& targetRateComputationPeriod() const { return targetRateComputationPeriod_; }
52+
double scalingFactor() const { return scalingFactor_; }
53+
4754
private:
4855
QuantLib::ext::shared_ptr<QuantLib::SmileSection> smileSectionImpl(const QuantLib::Date& optionDate) const override;
4956
QuantLib::ext::shared_ptr<QuantLib::SmileSection> smileSectionImpl(QuantLib::Time optionTime) const override;

QuantExt/qle/termstructures/proxyswaptionvolatility.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ ProxySwaptionVolatility::ProxySwaptionVolatility(const QuantLib::Handle<Swaption
3232
: SwaptionVolatilityStructure(baseVol->businessDayConvention(), baseVol->dayCounter()), baseVol_(baseVol),
3333
baseSwapIndexBase_(baseSwapIndexBase), baseShortSwapIndexBase_(baseShortSwapIndexBase),
3434
targetSwapIndexBase_(targetSwapIndexBase), targetShortSwapIndexBase_(targetShortSwapIndexBase) {
35+
registerWith(baseVol_);
36+
registerWith(baseSwapIndexBase_);
37+
registerWith(baseShortSwapIndexBase_);
38+
registerWith(targetSwapIndexBase_);
39+
registerWith(targetShortSwapIndexBase_);
3540
enableExtrapolation(baseVol->allowsExtrapolation());
3641
}
3742

QuantExt/qle/termstructures/proxyswaptionvolatility.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class ProxySwaptionVolatility : public QuantLib::SwaptionVolatilityStructure {
4545

4646
const QuantLib::Period& maxSwapTenor() const override { return baseVol_->maxSwapTenor(); }
4747

48+
QuantLib::Handle<QuantLib::SwaptionVolatilityStructure> baseVol() const { return baseVol_; }
49+
QuantLib::ext::shared_ptr<QuantLib::SwapIndex> baseSwapIndexBase() const { return baseSwapIndexBase_; }
50+
QuantLib::ext::shared_ptr<QuantLib::SwapIndex> baseShortSwapIndexBase() const { return baseShortSwapIndexBase_; }
51+
4852
private:
4953
QuantLib::ext::shared_ptr<QuantLib::SmileSection> smileSectionImpl(const QuantLib::Date& optionDate,
5054
const QuantLib::Period& swapTenor) const override;

QuantExt/qle/termstructures/sabrparametricvolatility.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,4 +759,34 @@ Real SabrParametricVolatility::evaluate(const Real timeToExpiry, const Real unde
759759
outputOptionType);
760760
}
761761

762+
QuantLib::ext::shared_ptr<SabrParametricVolatility> SabrParametricVolatility::clone(
763+
const std::vector<ParametricVolatility::MarketSmile>& marketSmiles,
764+
const std::vector<ParameterCalibration>& calibrationTypes) const {
765+
766+
auto modelParameters = modelParameters_;
767+
768+
if (calibrationTypes.size() > 0) {
769+
// If calibration types are provided, we need to adjust the model parameters accordingly
770+
for (Size i = 0; i < timeToExpiries_.size(); ++i) {
771+
const auto& tte = timeToExpiries_[i];
772+
for (Size j = 0; j < underlyingLengths_.size(); ++j) {
773+
const auto& ul = underlyingLengths_[j];
774+
auto key = std::make_pair(tte, ul);
775+
QL_REQUIRE(calibrationTypes.size() == modelParameters[key].size(),
776+
"SabrParametricVolatility::clone(): number of calibration types ("
777+
<< calibrationTypes.size() << ") does not match number of model parameters ("
778+
<< modelParameters[key].size() << ") for ("
779+
<< tte << ", " << ul << ").");
780+
for (Size k = 0; k < modelParameters[key].size(); ++k) {
781+
modelParameters[key][k].second = calibrationTypes[k];
782+
}
783+
}
784+
}
785+
}
786+
787+
return QuantLib::ext::make_shared<SabrParametricVolatility>(
788+
modelVariant_, marketSmiles, marketModelType_, inputMarketQuoteType_, discountCurve_, modelParameters,
789+
modelShifts_, maxCalibrationAttempts_, exitEarlyErrorThreshold_, maxAcceptableError_);
790+
}
791+
762792
} // namespace QuantExt

QuantExt/qle/termstructures/sabrparametricvolatility.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ class SabrParametricVolatility final : public ParametricVolatility {
8989

9090
const std::vector<CalibrationResult>& calibrationResults() const { return calibrationResults_; }
9191

92+
QuantLib::ext::shared_ptr<SabrParametricVolatility> clone(
93+
const std::vector<ParametricVolatility::MarketSmile>& marketSmiles,
94+
const std::vector<ParameterCalibration>& calibrationTypes) const;
95+
9296
private:
9397
static constexpr double eps1 = .0000001;
9498
static constexpr double eps2 = .9999;

QuantExt/qle/termstructures/sabrstrippedoptionletadapter.hpp

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class SabrStrippedOptionletAdapter : public QuantLib::OptionletVolatilityStructu
5555
const std::vector<std::vector<std::pair<Real, ParametricVolatility::ParameterCalibration>>>&
5656
initialModelParameters = {},
5757
const QuantLib::Size maxCalibrationAttempts = 10, const QuantLib::Real exitEarlyErrorThreshold = 0.005,
58-
const QuantLib::Real maxAcceptableError = 0.05);
58+
const QuantLib::Real maxAcceptableError = 0.05, bool stickySabr = false);
5959

6060
/*! Constructor taking an explicit \p referenceDate and the term structure will therefore be not \e moving.
6161
*/
@@ -105,6 +105,13 @@ class SabrStrippedOptionletAdapter : public QuantLib::OptionletVolatilityStructu
105105
calculate();
106106
return parametricVolatility_;
107107
}
108+
QuantExt::SabrParametricVolatility::ModelVariant modelVariant() const { return modelVariant_; }
109+
QuantLib::Real modelDisplacement() const { return modelDisplacement_; }
110+
const std::vector<std::vector<std::pair<Real, ParametricVolatility::ParameterCalibration>>>&
111+
initialModelParameters() const { return initialModelParameters_; }
112+
QuantLib::Size maxCalibrationAttempts() const { return maxCalibrationAttempts_; }
113+
QuantLib::Real exitEarlyErrorThreshold() const { return exitEarlyErrorThreshold_; }
114+
QuantLib::Real maxAcceptableError() const { return maxAcceptableError_; }
108115
//@}
109116

110117
protected:
@@ -130,6 +137,7 @@ class SabrStrippedOptionletAdapter : public QuantLib::OptionletVolatilityStructu
130137
QuantLib::Size maxCalibrationAttempts_;
131138
QuantLib::Real exitEarlyErrorThreshold_;
132139
QuantLib::Real maxAcceptableError_;
140+
bool stickySabr_;
133141

134142
//! State
135143
mutable std::map<Real, QuantLib::ext::shared_ptr<ParametricVolatilitySmileSection>> cache_;
@@ -145,13 +153,13 @@ SabrStrippedOptionletAdapter<TimeInterpolator>::SabrStrippedOptionletAdapter(
145153
const QuantLib::Real modelDisplacement,
146154
const std::vector<std::vector<std::pair<Real, ParametricVolatility::ParameterCalibration>>>& initialModelParameters,
147155
const QuantLib::Size maxCalibrationAttempts, const QuantLib::Real exitEarlyErrorThreshold,
148-
const QuantLib::Real maxAcceptableError)
156+
const QuantLib::Real maxAcceptableError, bool stickySabr)
149157
: OptionletVolatilityStructure(sob->settlementDays(), sob->calendar(), sob->businessDayConvention(),
150158
sob->dayCounter()),
151159
optionletBase_(sob), ti_(ti), modelVariant_(modelVariant), outputVolatilityType_(outputVolatilityType),
152160
outputDisplacement_(outputDisplacement), initialModelParameters_(initialModelParameters),
153161
maxCalibrationAttempts_(maxCalibrationAttempts), exitEarlyErrorThreshold_(exitEarlyErrorThreshold),
154-
maxAcceptableError_(maxAcceptableError) {
162+
maxAcceptableError_(maxAcceptableError), stickySabr_(stickySabr) {
155163
registerWith(optionletBase_);
156164
}
157165

@@ -238,6 +246,14 @@ inline void SabrStrippedOptionletAdapter<TimeInterpolator>::performCalculations(
238246
}
239247
}
240248

249+
// For sticky SABR, we only need to re-imply the alpha parameter after initial calibration
250+
if (stickySabr_) {
251+
if (auto sabr = QuantLib::ext::dynamic_pointer_cast<SabrParametricVolatility>(parametricVolatility_)) {
252+
parametricVolatility_ = sabr->clone(marketSmiles, {});
253+
return;
254+
}
255+
}
256+
241257
std::map<Real, Real> modelShift;
242258
if (modelDisplacement_ != Null<Real>()) {
243259
modelShift[Null<Real>()] = modelDisplacement_;
@@ -250,6 +266,18 @@ inline void SabrStrippedOptionletAdapter<TimeInterpolator>::performCalculations(
250266
: ParametricVolatility::MarketQuoteType::ShiftedLognormalVolatility,
251267
Handle<YieldTermStructure>(), modelParameters, modelShift, maxCalibrationAttempts_, exitEarlyErrorThreshold_,
252268
maxAcceptableError_);
269+
270+
// for sticky SABR, after initial calibration, we re-create parametric volatility with only alpha to be implied
271+
// this ensures that basis between the two parametric volatilities is eliminated
272+
if (stickySabr_) {
273+
if (auto sabr = QuantLib::ext::dynamic_pointer_cast<SabrParametricVolatility>(parametricVolatility_)) {
274+
parametricVolatility_ = sabr->clone(marketSmiles,
275+
{ ParametricVolatility::ParameterCalibration::Implied,
276+
ParametricVolatility::ParameterCalibration::Fixed,
277+
ParametricVolatility::ParameterCalibration::Fixed,
278+
ParametricVolatility::ParameterCalibration::Fixed });
279+
}
280+
}
253281
}
254282

255283
template <class TimeInterpolator> inline void SabrStrippedOptionletAdapter<TimeInterpolator>::deepUpdate() {

QuantExt/qle/termstructures/swaptionsabrcube.cpp

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,27 @@ SwaptionSabrCube::SwaptionSabrCube(
3636
initialModelParameters,
3737
const std::vector<Real>& outputShift, const std::vector<Real>& modelShift,
3838
const QuantLib::Size maxCalibrationAttempts, const QuantLib::Real exitEarlyErrorThreshold,
39-
const QuantLib::Real maxAcceptableError)
39+
const QuantLib::Real maxAcceptableError, bool stickySabr)
4040
: SwaptionVolatilityCube(atmVolStructure, optionTenors, swapTenors, strikeSpreads, volSpreads, swapIndexBase,
4141
shortSwapIndexBase, false),
4242
atmOptionTenors_(atmOptionTenors), atmSwapTenors_(atmSwapTenors), modelVariant_(modelVariant),
4343
outputVolatilityType_(outputVolatilityType), initialModelParameters_(initialModelParameters),
4444
outputShift_(outputShift), modelShift_(modelShift), maxCalibrationAttempts_(maxCalibrationAttempts),
45-
exitEarlyErrorThreshold_(exitEarlyErrorThreshold), maxAcceptableError_(maxAcceptableError) {
45+
exitEarlyErrorThreshold_(exitEarlyErrorThreshold), maxAcceptableError_(maxAcceptableError), stickySabr_(stickySabr) {
4646

4747
registerWith(atmVolStructure);
48-
49-
for (auto const& v : volSpreads)
50-
for (auto const& s : v)
51-
registerWith(s);
48+
registerWith(swapIndexBase);
49+
registerWith(shortSwapIndexBase);
50+
51+
if (stickySabr) {
52+
for (auto const& v : volSpreads)
53+
for (auto const& s : v)
54+
unregisterWith(s);
55+
} else {
56+
for (auto const& v : volSpreads)
57+
for (auto const& s : v)
58+
registerWith(s);
59+
}
5260
}
5361

5462
namespace {
@@ -147,6 +155,14 @@ void SwaptionSabrCube::performCalculations() const {
147155
}
148156
}
149157

158+
// For sticky SABR, we only need to re-imply the alpha parameter after initial calibration
159+
if (stickySabr_) {
160+
if (auto sabr = QuantLib::ext::dynamic_pointer_cast<SabrParametricVolatility>(parametricVolatility_)) {
161+
parametricVolatility_ = sabr->clone(marketSmiles, {});
162+
return;
163+
}
164+
}
165+
150166
std::map<Real, Real> modelShift;
151167
if (!modelShift_.empty()) {
152168
QL_REQUIRE(modelShift_.size() == allSwapTenors.size(),
@@ -164,6 +180,18 @@ void SwaptionSabrCube::performCalculations() const {
164180
: ParametricVolatility::MarketQuoteType::ShiftedLognormalVolatility,
165181
Handle<YieldTermStructure>(), modelParameters, modelShift, maxCalibrationAttempts_, exitEarlyErrorThreshold_,
166182
maxAcceptableError_);
183+
184+
// for sticky SABR, after initial calibration, we re-create parametric volatility with only alpha to be implied
185+
// this ensures that basis between the two parametric volatilities is eliminated
186+
if (stickySabr_) {
187+
if (auto sabr = QuantLib::ext::dynamic_pointer_cast<SabrParametricVolatility>(parametricVolatility_)) {
188+
parametricVolatility_ = sabr->clone(marketSmiles,
189+
{ ParametricVolatility::ParameterCalibration::Implied,
190+
ParametricVolatility::ParameterCalibration::Fixed,
191+
ParametricVolatility::ParameterCalibration::Fixed,
192+
ParametricVolatility::ParameterCalibration::Fixed });
193+
}
194+
}
167195
}
168196

169197
VolatilityType SwaptionSabrCube::volatilityType() const {
@@ -191,7 +219,7 @@ QuantLib::ext::shared_ptr<SmileSection> SwaptionSabrCube::smileSectionImpl(Time
191219
return tmp;
192220
}
193221

194-
QuantLib::ext::shared_ptr<ParametricVolatility> SwaptionSabrCube::parametricVolatility() const {
222+
const QuantLib::ext::shared_ptr<ParametricVolatility>& SwaptionSabrCube::parametricVolatility() const {
195223
calculate();
196224
return parametricVolatility_;
197225
}

QuantExt/qle/termstructures/swaptionsabrcube.hpp

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,26 @@ class SwaptionSabrCube : public SwaptionVolatilityCube {
5050
const std::vector<Real>& outputShift = {}, const std::vector<Real>& modelShift = {},
5151
const QuantLib::Size maxCalibrationAttempts = 10,
5252
const QuantLib::Real exitEarlyErrorThreshold = 0.005,
53-
const QuantLib::Real maxAcceptableError = 0.05);
53+
const QuantLib::Real maxAcceptableError = 0.05, bool stickySabr = false);
54+
// LazyObject interface
5455
void performCalculations() const override;
55-
QuantLib::ext::shared_ptr<SmileSection> smileSectionImpl(Time optionTime, Time swapLength) const override;
56-
57-
QuantLib::ext::shared_ptr<ParametricVolatility> parametricVolatility() const;
5856

57+
// SwaptionVolatilityStructure interface
58+
QuantLib::ext::shared_ptr<SmileSection> smileSectionImpl(Time optionTime, Time swapLength) const override;
5959
VolatilityType volatilityType() const override;
6060

61+
// inspectors
62+
const QuantLib::ext::shared_ptr<ParametricVolatility>& parametricVolatility() const;
63+
QuantExt::SabrParametricVolatility::ModelVariant modelVariant() const { return modelVariant_; }
64+
const std::map<std::pair<QuantLib::Period, QuantLib::Period>,
65+
std::vector<std::pair<Real, ParametricVolatility::ParameterCalibration>>>&
66+
initialModelParameters() const { return initialModelParameters_; }
67+
const std::vector<Real>& outputShift() const { return outputShift_; }
68+
const std::vector<Real>& modelShift() const { return modelShift_; }
69+
QuantLib::Size maxCalibrationAttempts() const { return maxCalibrationAttempts_; }
70+
QuantLib::Real exitEarlyErrorThreshold() const { return exitEarlyErrorThreshold_; }
71+
QuantLib::Real maxAcceptableError() const { return maxAcceptableError_; }
72+
6173
private:
6274
Real shiftImpl(Time optionTime, Time swapLength) const override;
6375

@@ -76,6 +88,7 @@ class SwaptionSabrCube : public SwaptionVolatilityCube {
7688
QuantLib::Size maxCalibrationAttempts_;
7789
QuantLib::Real exitEarlyErrorThreshold_;
7890
QuantLib::Real maxAcceptableError_;
91+
bool stickySabr_;
7992
};
8093

8194
} // namespace QuantExt

0 commit comments

Comments
 (0)