Skip to content

Commit 6d906cd

Browse files
QPR-12825 Add StickySABR support for UseSpreadedTermStructures use case
1 parent 52a6c32 commit 6d906cd

10 files changed

Lines changed: 498 additions & 164 deletions

OREAnalytics/orea/scenario/scenariosimmarket.cpp

Lines changed: 238 additions & 150 deletions
Large diffs are not rendered by default.

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());

OREAnalytics/orea/scenario/sensitivityscenariogenerator.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1361,7 +1361,7 @@ void SensitivityScenarioGenerator::generateCapFloorVolScenarios(bool up) {
13611361
vector<Real> shiftStrikes = data.shiftStrikes;
13621362
// Has an ATM shift been configured?
13631363
bool sensiIsAtm = false;
1364-
if (shiftStrikes.size() == 1 && shiftStrikes[0] == 0.0 && data.isRelative) {
1364+
if (shiftStrikes.size() == 1 && shiftStrikes[0] == 0.0) {
13651365
sensiIsAtm = true;
13661366
}
13671367

QuantExt/qle/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ termstructures/spreadedsmilesection2.cpp
424424
termstructures/spreadedsurvivalprobabilitytermstructure.cpp
425425
termstructures/spreadedswaptionvolatility.cpp
426426
termstructures/spreadedyoyvolsurface.cpp
427+
termstructures/strippedoptionlet.cpp
427428
termstructures/strippedoptionletadapter2.cpp
428429
termstructures/strippedyoyinflationoptionletvol.cpp
429430
termstructures/subperiodsswaphelper.cpp
@@ -1043,6 +1044,7 @@ termstructures/spreadedswaptionvolatility.hpp
10431044
termstructures/spreadedyoyvolsurface.hpp
10441045
termstructures/staticallycorrectedyieldtermstructure.hpp
10451046
termstructures/strippedcpivolatilitystructure.hpp
1047+
termstructures/strippedoptionlet.hpp
10461048
termstructures/strippedoptionletadapter.hpp
10471049
termstructures/strippedoptionletadapter2.hpp
10481050
termstructures/strippedyoyinflationoptionletvol.hpp

QuantExt/qle/quantext.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@
599599
#include <qle/termstructures/spreadedyoyvolsurface.hpp>
600600
#include <qle/termstructures/staticallycorrectedyieldtermstructure.hpp>
601601
#include <qle/termstructures/strippedcpivolatilitystructure.hpp>
602+
#include <qle/termstructures/strippedoptionlet.hpp>
602603
#include <qle/termstructures/strippedoptionletadapter.hpp>
603604
#include <qle/termstructures/strippedoptionletadapter2.hpp>
604605
#include <qle/termstructures/strippedyoyinflationoptionletvol.hpp>

QuantExt/qle/termstructures/sabrstrippedoptionletadapter.hpp

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ 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, bool stickySabr = false);
58+
const QuantLib::Real maxAcceptableError = 0.05,
59+
const std::vector<std::vector<Real>>& strikes = {},
60+
const std::vector<std::vector<Handle<Quote>>>& volSpreads = {},
61+
bool stickySabr = false);
5962

6063
/*! Constructor taking an explicit \p referenceDate and the term structure will therefore be not \e moving.
6164
*/
@@ -68,7 +71,10 @@ class SabrStrippedOptionletAdapter : public QuantLib::OptionletVolatilityStructu
6871
const std::vector<std::vector<std::pair<Real, ParametricVolatility::ParameterCalibration>>>&
6972
initialModelParameters = {},
7073
const QuantLib::Size maxCalibrationAttempts = 10, const QuantLib::Real exitEarlyErrorThreshold = 0.005,
71-
const QuantLib::Real maxAcceptableError = 0.05);
74+
const QuantLib::Real maxAcceptableError = 0.05,
75+
const std::vector<std::vector<Real>>& strikes = {},
76+
const std::vector<std::vector<Handle<Quote>>>& volSpreads = {},
77+
bool stickySabr = false);
7278

7379
//! \name TermStructure interface
7480
//@{
@@ -111,6 +117,7 @@ class SabrStrippedOptionletAdapter : public QuantLib::OptionletVolatilityStructu
111117
initialModelParameters() const { return initialModelParameters_; }
112118
QuantLib::Size maxCalibrationAttempts() const { return maxCalibrationAttempts_; }
113119
QuantLib::Real exitEarlyErrorThreshold() const { return exitEarlyErrorThreshold_; }
120+
const std::vector<std::vector<Real>>& strikes() const { return strikes_; }
114121
QuantLib::Real maxAcceptableError() const { return maxAcceptableError_; }
115122
//@}
116123

@@ -122,6 +129,8 @@ class SabrStrippedOptionletAdapter : public QuantLib::OptionletVolatilityStructu
122129
//@}
123130

124131
private:
132+
void init();
133+
125134
//! Base optionlet object that provides the stripped optionlet volatilities
126135
QuantLib::ext::shared_ptr<QuantLib::StrippedOptionletBase> optionletBase_;
127136

@@ -137,6 +146,8 @@ class SabrStrippedOptionletAdapter : public QuantLib::OptionletVolatilityStructu
137146
QuantLib::Size maxCalibrationAttempts_;
138147
QuantLib::Real exitEarlyErrorThreshold_;
139148
QuantLib::Real maxAcceptableError_;
149+
std::vector<std::vector<Real>> strikes_;
150+
std::vector<std::vector<Handle<Quote>>> volSpreads_;
140151
bool stickySabr_;
141152

142153
//! State
@@ -145,6 +156,61 @@ class SabrStrippedOptionletAdapter : public QuantLib::OptionletVolatilityStructu
145156
mutable std::unique_ptr<FlatExtrapolation> atmInterpolation_;
146157
};
147158

159+
template <class TimeInterpolator>
160+
inline void SabrStrippedOptionletAdapter<TimeInterpolator>::init() {
161+
registerWith(optionletBase_);
162+
Size nFixingDates = optionletBase_->optionletFixingDates().size();
163+
164+
// The dimension of input StrippedOptionletBase can be either
165+
//
166+
// - ATM only (only 1 optionlet strike for every fixing date)
167+
// SABR cube will be calibrated to the skew defined by strikes_ and volSpreads_
168+
//
169+
// or,
170+
//
171+
// - Smile (more than 1 optionlet strike for at least 1 fixing date)
172+
// SABR cube will be calibrated to the skew defined in the input StrippedOptionletBase
173+
174+
bool isAtm = true;
175+
for (Size i = 0; i < nFixingDates; ++i)
176+
if (optionletBase_->optionletStrikes(i).size() > 1)
177+
isAtm = isAtm && false;
178+
179+
if (!isAtm) {
180+
QL_REQUIRE(strikes_.empty(),
181+
"When StrippedOptionletBase contains smiles, strikes "
182+
"inputs to SabrStrippedOptionletAdapter must be empty");
183+
strikes_.resize(nFixingDates);
184+
for (Size i = 0; i < nFixingDates; ++i) {
185+
strikes_[i] = optionletBase_->optionletStrikes(i);
186+
}
187+
if (volSpreads_.empty()) {
188+
volSpreads_.resize(nFixingDates);
189+
for (Size i = 0; i < nFixingDates; ++i) {
190+
volSpreads_[i] = std::vector<Handle<Quote>>(
191+
strikes_[i].size(), Handle<Quote>(QuantLib::ext::make_shared<SimpleQuote>(0.0)));
192+
}
193+
} else { /* do nothing, volSpreads_ will be validated below */ }
194+
}
195+
196+
QL_REQUIRE(nFixingDates == volSpreads_.size(),
197+
"mismatch between number of fixing dates (" <<
198+
nFixingDates << ") and number of rows (" <<
199+
volSpreads_.size() << ")");
200+
for (Size i = 0; i < volSpreads_.size(); i++) {
201+
Size nStrikes = strikes_[i].size();
202+
QL_REQUIRE(nStrikes == volSpreads_[i].size(),
203+
"mismatch between number of strikes (" << nStrikes <<
204+
") and number of columns (" << volSpreads_[i].size() <<
205+
") in the " << io::ordinal(i+1) << " row");
206+
}
207+
if (!stickySabr_) {
208+
for (auto const& v : volSpreads_)
209+
for (auto const& s : v)
210+
registerWith(s);
211+
}
212+
}
213+
148214
template <class TimeInterpolator>
149215
SabrStrippedOptionletAdapter<TimeInterpolator>::SabrStrippedOptionletAdapter(
150216
const QuantLib::ext::shared_ptr<QuantLib::StrippedOptionletBase>& sob,
@@ -153,14 +219,15 @@ SabrStrippedOptionletAdapter<TimeInterpolator>::SabrStrippedOptionletAdapter(
153219
const QuantLib::Real modelDisplacement,
154220
const std::vector<std::vector<std::pair<Real, ParametricVolatility::ParameterCalibration>>>& initialModelParameters,
155221
const QuantLib::Size maxCalibrationAttempts, const QuantLib::Real exitEarlyErrorThreshold,
156-
const QuantLib::Real maxAcceptableError, bool stickySabr)
222+
const QuantLib::Real maxAcceptableError, const std::vector<std::vector<Real>>& strikes,
223+
const std::vector<std::vector<Handle<Quote>>>& volSpreads, bool stickySabr)
157224
: OptionletVolatilityStructure(sob->settlementDays(), sob->calendar(), sob->businessDayConvention(),
158225
sob->dayCounter()),
159226
optionletBase_(sob), ti_(ti), modelVariant_(modelVariant), outputVolatilityType_(outputVolatilityType),
160227
outputDisplacement_(outputDisplacement), initialModelParameters_(initialModelParameters),
161228
maxCalibrationAttempts_(maxCalibrationAttempts), exitEarlyErrorThreshold_(exitEarlyErrorThreshold),
162-
maxAcceptableError_(maxAcceptableError), stickySabr_(stickySabr) {
163-
registerWith(optionletBase_);
229+
maxAcceptableError_(maxAcceptableError), strikes_(strikes), volSpreads_(volSpreads), stickySabr_(stickySabr) {
230+
init();
164231
}
165232

166233
template <class TimeInterpolator>
@@ -171,13 +238,14 @@ SabrStrippedOptionletAdapter<TimeInterpolator>::SabrStrippedOptionletAdapter(
171238
const QuantLib::Real modelDiscplacement,
172239
const std::vector<std::vector<std::pair<Real, ParametricVolatility::ParameterCalibration>>>& initialModelParameters,
173240
const QuantLib::Size maxCalibrationAttempts, const QuantLib::Real exitEarlyErrorThreshold,
174-
const QuantLib::Real maxAcceptableError)
241+
const QuantLib::Real maxAcceptableError, const std::vector<std::vector<Real>>& strikes,
242+
const std::vector<std::vector<Handle<Quote>>>& volSpreads, bool stickySabr)
175243
: OptionletVolatilityStructure(referenceDate, sob->calendar(), sob->businessDayConvention(), sob->dayCounter()),
176244
optionletBase_(sob), ti_(ti), modelVariant_(modelVariant), outputVolatilityType_(outputVolatilityType),
177245
outputDisplacement_(outputDisplacement), initialModelParameters_(initialModelParameters),
178246
maxCalibrationAttempts_(maxCalibrationAttempts), exitEarlyErrorThreshold_(exitEarlyErrorThreshold),
179-
maxAcceptableError_(maxAcceptableError) {
180-
registerWith(optionletBase_);
247+
maxAcceptableError_(maxAcceptableError), strikes_(strikes), volSpreads_(volSpreads), stickySabr_(stickySabr) {
248+
init();
181249
}
182250

183251
template <class TimeInterpolator>
@@ -233,13 +301,26 @@ inline void SabrStrippedOptionletAdapter<TimeInterpolator>::performCalculations(
233301
<< this->optionletBase()->optionletFixingTimes().size() << ")");
234302
for (Size i = 0; i < this->optionletBase()->optionletFixingTimes().size(); ++i) {
235303
Real forward = atmInterpolation_->operator()(this->optionletBase()->optionletFixingTimes()[i]);
304+
auto optionletStrikes = strikes_.empty() ? this->optionletBase()->optionletStrikes(i) : strikes_[i];
305+
QL_REQUIRE(!optionletStrikes.empty(),
306+
"SabrStrippedOptionletAdapter: no optionlet strikes for optionlet fixing time "
307+
<< this->optionletBase()->optionletFixingTimes()[i]);
308+
auto optionletVolatilities = this->optionletBase()->optionletVolatilities(i);
309+
QL_REQUIRE(!optionletVolatilities.empty(),
310+
"SabrStrippedOptionletAdapter: no optionlet volatilities for optionlet fixing time "
311+
<< this->optionletBase()->optionletFixingTimes()[i]);
312+
if (optionletVolatilities.size() == 1 && optionletStrikes.size() > 1)
313+
optionletVolatilities = std::vector<Real>(optionletStrikes.size(), optionletVolatilities[0]);
314+
for (Size j = 0; j < optionletVolatilities.size(); ++j) {
315+
optionletVolatilities[j] += volSpreads_[i][j]->value();
316+
}
236317
marketSmiles.push_back(ParametricVolatility::MarketSmile{this->optionletBase()->optionletFixingTimes()[i],
237318
Null<Real>(),
238319
forward,
239320
optionletBase_->displacement(),
240321
{},
241-
this->optionletBase()->optionletStrikes(i),
242-
this->optionletBase()->optionletVolatilities(i)});
322+
optionletStrikes,
323+
optionletVolatilities});
243324
if (!initialModelParameters_.empty()) {
244325
modelParameters[std::make_pair(this->optionletBase()->optionletFixingTimes()[i], Null<Real>())] =
245326
initialModelParameters_.size() == 1 ? initialModelParameters_.front() : initialModelParameters_[i];
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright (C) 2025 AcadiaSoft Inc.
3+
All rights reserved.
4+
5+
This file is part of ORE, a free-software/open-source library
6+
for transparent pricing and risk analysis - http://opensourcerisk.org
7+
8+
ORE is free software: you can redistribute it and/or modify it
9+
under the terms of the Modified BSD License. You should have received a
10+
copy of the license along with this program.
11+
The license is also available online at <http://opensourcerisk.org>
12+
13+
This program is distributed on the basis that it will form a useful
14+
contribution to risk analytics and model standardisation, but WITHOUT
15+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16+
FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17+
*/
18+
19+
#include <qle/termstructures/strippedoptionlet.hpp>
20+
#include <ql/quotes/simplequote.hpp>
21+
22+
using std::vector;
23+
24+
namespace QuantExt {
25+
26+
StrippedOptionlet::StrippedOptionlet(Natural settlementDays,
27+
const Calendar& calendar,
28+
BusinessDayConvention bdc,
29+
ext::shared_ptr<IborIndex> iborIndex,
30+
const vector<Date>& optionletDates,
31+
const vector<Rate>& strikes,
32+
const Handle<OptionletVolatilityStructure>& baseVol,
33+
vector<vector<Handle<Quote>>> quotes,
34+
DayCounter dc,
35+
VolatilityType type,
36+
Real displacement,
37+
const vector<Real>& atmOptionletRates)
38+
: StrippedOptionlet(settlementDays,
39+
calendar,
40+
bdc,
41+
std::move(iborIndex),
42+
optionletDates,
43+
vector<vector<Rate>>(optionletDates.size(), strikes),
44+
baseVol,
45+
quotes,
46+
std::move(dc),
47+
type,
48+
displacement,
49+
atmOptionletRates) {}
50+
51+
StrippedOptionlet::StrippedOptionlet(Natural settlementDays,
52+
const Calendar& calendar,
53+
BusinessDayConvention bdc,
54+
ext::shared_ptr<IborIndex> iborIndex,
55+
const vector<Date>& optionletDates,
56+
const vector<vector<Rate>>& strikes,
57+
const Handle<OptionletVolatilityStructure>& baseVol,
58+
vector<vector<Handle<Quote>>> quotes,
59+
DayCounter dc,
60+
VolatilityType type,
61+
Real displacement,
62+
const vector<Real>& atmOptionletRates)
63+
: QuantLib::StrippedOptionlet(settlementDays, calendar, bdc, iborIndex, optionletDates,
64+
strikes,
65+
quotes,
66+
dc, type, displacement, atmOptionletRates),
67+
baseVol_(baseVol), quotes_(quotes) {
68+
registerWith(baseVol);
69+
for (auto x : quotes_)
70+
for (auto y : x)
71+
unregisterWith(y);
72+
}
73+
74+
void StrippedOptionlet::performCalculations() const {
75+
// fill the quotes matrix from the base vol structure
76+
for (Size i = 0; i < optionletFixingDates().size(); ++i) {
77+
Date d = optionletFixingDates()[i];
78+
for (Size j = 0; j < optionletStrikes(i).size(); ++j) {
79+
Rate k = optionletStrikes(i)[j];
80+
Volatility vol = baseVol_->volatility(d, k, true);
81+
auto sq = ext::dynamic_pointer_cast<SimpleQuote>(*quotes_[i][j]);
82+
QL_REQUIRE(sq, "StrippedOptionlet::performCalculations(): null SimpleQuote");
83+
sq->setValue(vol);
84+
}
85+
}
86+
QuantLib::StrippedOptionlet::performCalculations();
87+
}
88+
89+
} // namespace QuantExt
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright (C) 2025 AcadiaSoft Inc.
3+
All rights reserved.
4+
5+
This file is part of ORE, a free-software/open-source library
6+
for transparent pricing and risk analysis - http://opensourcerisk.org
7+
8+
ORE is free software: you can redistribute it and/or modify it
9+
under the terms of the Modified BSD License. You should have received a
10+
copy of the license along with this program.
11+
The license is also available online at <http://opensourcerisk.org>
12+
13+
This program is distributed on the basis that it will form a useful
14+
contribution to risk analytics and model standardisation, but WITHOUT
15+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16+
FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17+
*/
18+
19+
/*! \file strippedoptionlet.hpp
20+
*/
21+
22+
#pragma once
23+
24+
#include <ql/termstructures/volatility/optionlet/optionletvolatilitystructure.hpp>
25+
#include <ql/termstructures/volatility/optionlet/strippedoptionlet.hpp>
26+
27+
namespace QuantExt {
28+
29+
using namespace QuantLib;
30+
using std::vector;
31+
32+
/*! Helper class to wrap in a StrippedOptionlet object a OptionletVolatilityStructure
33+
*/
34+
class StrippedOptionlet : public QuantLib::StrippedOptionlet {
35+
public:
36+
StrippedOptionlet(Natural settlementDays,
37+
const Calendar& calendar,
38+
BusinessDayConvention bdc,
39+
ext::shared_ptr<IborIndex> iborIndex,
40+
const vector<Date>& optionletDates,
41+
const vector<Rate>& strikes,
42+
const Handle<OptionletVolatilityStructure>& baseVol,
43+
vector<vector<Handle<Quote>>> quotes,
44+
DayCounter dc,
45+
VolatilityType type = ShiftedLognormal,
46+
Real displacement = 0.0,
47+
const vector<Real>& atmOptionletRates = {});
48+
StrippedOptionlet(Natural settlementDays,
49+
const Calendar& calendar,
50+
BusinessDayConvention bdc,
51+
ext::shared_ptr<IborIndex> iborIndex,
52+
const vector<Date>& optionletDates,
53+
const vector<vector<Rate>>& strikes,
54+
const Handle<OptionletVolatilityStructure>& baseVol,
55+
vector<vector<Handle<Quote>>> quotes,
56+
DayCounter dc,
57+
VolatilityType type = ShiftedLognormal,
58+
Real displacement = 0.0,
59+
const vector<Real>& atmOptionletRates = {});
60+
61+
private:
62+
vector<vector<Handle<Quote>>> createEmptyQuotesMatrix(const vector<Date>& optionletDates,
63+
const vector<vector<Rate>>& strikes);
64+
void performCalculations() const override;
65+
66+
Handle<OptionletVolatilityStructure> baseVol_;
67+
vector<vector<Handle<Quote>>> quotes_;
68+
69+
};
70+
71+
} // namespace QuantExt

0 commit comments

Comments
 (0)