Skip to content

Commit 22ecaa0

Browse files
pcaspersjenkins
authored andcommitted
QPR-11677 market vols are quoted as "efffective" vols, i.e. they include the vol decay adjustment
1 parent b0692e2 commit 22ecaa0

7 files changed

Lines changed: 147 additions & 35 deletions

File tree

OREAnalytics/orea/app/reportwriter.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,13 +384,19 @@ void ReportWriter::writeCashflow(ore::data::Report& report, const std::string& b
384384
volFixingDate = tmp->underlying()->fixingDates().front();
385385
qlIndexName = tmp->index()->name();
386386
usesCapVol = true;
387+
// for now we output the stripped caplet vol, not the effective one
388+
// capVolatility = tmp->effectiveCapletVolatility();
389+
// floorVolatility = tmp->effectiveFloorletVolatility();
387390
} else if (auto tmp =
388391
boost::dynamic_pointer_cast<CappedFlooredAverageONIndexedCoupon>(c)) {
389392
floorStrike = tmp->effectiveFloor();
390393
capStrike = tmp->effectiveCap();
391394
volFixingDate = tmp->underlying()->fixingDates().front();
392395
qlIndexName = tmp->index()->name();
393396
usesCapVol = true;
397+
// capVolatility = tmp->effectiveCapletVolatility();
398+
// for now we output the stripped caplet vol, not the effective one
399+
// floorVolatility = tmp->effectiveFloorletVolatility();
394400
}
395401

396402
// get market volaility for cap / floor
@@ -404,7 +410,7 @@ void ReportWriter::writeCashflow(ore::data::Report& report, const std::string& b
404410
->swaptionVol(IndexNameTranslator::instance().oreName(qlIndexName),
405411
configuration)
406412
->volatility(volFixingDate, swaptionTenor, floorStrike);
407-
} else if (usesCapVol) {
413+
} else if (usesCapVol && floorVolatility == Null<Real>()) {
408414
floorVolatility =
409415
market
410416
->capFloorVol(IndexNameTranslator::instance().oreName(qlIndexName),
@@ -419,7 +425,7 @@ void ReportWriter::writeCashflow(ore::data::Report& report, const std::string& b
419425
->swaptionVol(IndexNameTranslator::instance().oreName(qlIndexName),
420426
configuration)
421427
->volatility(volFixingDate, swaptionTenor, capStrike);
422-
} else if (usesCapVol) {
428+
} else if (usesCapVol && capVolatility == Null<Real>()) {
423429
capVolatility =
424430
market
425431
->capFloorVol(IndexNameTranslator::instance().oreName(qlIndexName),

QuantExt/qle/cashflows/averageonindexedcoupon.cpp

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ void CappedFlooredAverageONIndexedCoupon::performCalculations() const {
166166
if (cap_ != Null<Real>())
167167
capletRate = (nakedOption_ && floor_ == Null<Real>() ? -1.0 : 1.0) * pricer()->capletRate(effectiveCap());
168168
rate_ = swapletRate + floorletRate - capletRate;
169+
auto p = boost::dynamic_pointer_cast<CapFlooredAverageONIndexedCouponPricer>(pricer());
170+
QL_REQUIRE(p, "CapFlooredAverageONIndexedCoupon::performCalculations(): internal error, could not cast to "
171+
"CapFlooredAverageONIndexedCouponPricer");
172+
effectiveCapletVolatility_ = p->effectiveCapletVolatility();
173+
effectiveFloorletVolatility_ = p->effectiveFloorletVolatility();
169174
}
170175

171176
Rate CappedFlooredAverageONIndexedCoupon::cap() const { return gearing_ > 0.0 ? cap_ : floor_; }
@@ -229,6 +234,16 @@ Rate CappedFlooredAverageONIndexedCoupon::effectiveFloor() const {
229234
}
230235
}
231236

237+
Real CappedFlooredAverageONIndexedCoupon::effectiveCapletVolatility() const {
238+
calculate();
239+
return effectiveCapletVolatility_;
240+
}
241+
242+
Real CappedFlooredAverageONIndexedCoupon::effectiveFloorletVolatility() const {
243+
calculate();
244+
return effectiveFloorletVolatility_;
245+
}
246+
232247
void CappedFlooredAverageONIndexedCoupon::accept(AcyclicVisitor& v) {
233248
Visitor<CappedFlooredAverageONIndexedCoupon>* v1 = dynamic_cast<Visitor<CappedFlooredAverageONIndexedCoupon>*>(&v);
234249
if (v1 != 0)
@@ -257,11 +272,19 @@ CappedFlooredAverageONIndexedCoupon::CappedFlooredAverageONIndexedCoupon(
257272
// capped floored average on coupon pricer base class implementation
258273

259274
CapFlooredAverageONIndexedCouponPricer::CapFlooredAverageONIndexedCouponPricer(
260-
const Handle<OptionletVolatilityStructure>& v)
261-
: capletVol_(v) {
275+
const Handle<OptionletVolatilityStructure>& v, const bool effectiveVolatilityInput)
276+
: capletVol_(v), effectiveVolatilityInput_(effectiveVolatilityInput) {
262277
registerWith(capletVol_);
263278
}
264279

280+
bool CapFlooredAverageONIndexedCouponPricer::effectiveVolatilityInput() const { return effectiveVolatilityInput_; }
281+
282+
Real CapFlooredAverageONIndexedCouponPricer::effectiveCapletVolatility() const { return effectiveCapletVolatility_; }
283+
284+
Real CapFlooredAverageONIndexedCouponPricer::effectiveFloorletVolatility() const {
285+
return effectiveFloorletVolatility_;
286+
}
287+
265288
Handle<OptionletVolatilityStructure> CapFlooredAverageONIndexedCouponPricer::capletVolatility() const {
266289
return capletVol_;
267290
}

QuantExt/qle/cashflows/averageonindexedcoupon.hpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ class CappedFlooredAverageONIndexedCoupon : public FloatingRateCoupon {
129129
Rate effectiveCap() const;
130130
//! effective floor of fixing
131131
Rate effectiveFloor() const;
132+
//! effective caplet volatility
133+
Real effectiveCapletVolatility() const;
134+
//! effective floorlet volatility
135+
Real effectiveFloorletVolatility() const;
132136
//@}
133137
//! \name Visitability
134138
//@{
@@ -148,16 +152,25 @@ class CappedFlooredAverageONIndexedCoupon : public FloatingRateCoupon {
148152
bool nakedOption_;
149153
bool localCapFloor_;
150154
bool includeSpread_;
155+
mutable Real effectiveCapletVolatility_;
156+
mutable Real effectiveFloorletVolatility_;
151157
};
152158

153159
//! capped floored averaged indexed coupon pricer base class
154160
class CapFlooredAverageONIndexedCouponPricer : public FloatingRateCouponPricer {
155161
public:
156-
CapFlooredAverageONIndexedCouponPricer(const Handle<OptionletVolatilityStructure>& v);
162+
CapFlooredAverageONIndexedCouponPricer(const Handle<OptionletVolatilityStructure>& v,
163+
const bool effectiveVolatilityInput = false);
157164
Handle<OptionletVolatilityStructure> capletVolatility() const;
165+
bool effectiveVolatilityInput() const;
166+
Real effectiveCapletVolatility() const; // only available after capletRate() was called
167+
Real effectiveFloorletVolatility() const; // only available after floorletRate() was called
158168

159-
private:
169+
protected:
160170
Handle<OptionletVolatilityStructure> capletVol_;
171+
bool effectiveVolatilityInput_;
172+
mutable Real effectiveCapletVolatility_;
173+
mutable Real effectiveFloorletVolatility_;
161174
};
162175

163176
//! helper class building a sequence of overnight coupons

QuantExt/qle/cashflows/blackovernightindexedcouponpricer.cpp

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ void BlackOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& cou
3838
}
3939
swapletRate_ = coupon_->underlying()->rate();
4040
effectiveIndexFixing_ = coupon_->underlying()->effectiveIndexFixing();
41+
effectiveCapletVolatility_ = effectiveFloorletVolatility_ = Null<Real>();
4142
}
4243

4344
Real BlackOvernightIndexedCouponPricer::optionletRateGlobal(Option::Type optionType, Real effStrike) const {
@@ -55,23 +56,35 @@ Real BlackOvernightIndexedCouponPricer::optionletRateGlobal(Option::Type optionT
5556
return gearing_ * std::max(a - b, 0.0);
5657
} else {
5758
// not yet determined, use Black model
58-
// for the standard deviation see Lyashenko, Mercurio, Looking forward to backward looking rates, section 6.3.
59-
// the idea is to dampen the average volatility sigma between the fixing start and fixing end date by a
60-
// linear function going from (fixing start, 1) to (fixing end, 0)
6159
QL_REQUIRE(!capletVolatility().empty(), "BlackOvernightIndexedCouponPricer: missing optionlet volatility");
6260
std::vector<Date> fixingDates = coupon_->underlying()->fixingDates();
6361
QL_REQUIRE(!fixingDates.empty(), "BlackOvernightIndexedCouponPricer: empty fixing dates");
64-
Real fixingStartTime = capletVolatility()->timeFromReference(fixingDates.front());
65-
Real fixingEndTime = capletVolatility()->timeFromReference(fixingDates.back());
66-
Real sigma = capletVolatility()->volatility(
67-
std::max(fixingDates.front(), capletVolatility()->referenceDate() + 1), effStrike);
68-
Real T = std::max(fixingStartTime, 0.0);
69-
if (!close_enough(fixingEndTime, T))
70-
T += std::pow(fixingEndTime - T, 3.0) / std::pow(fixingEndTime - fixingStartTime, 2.0) / 3.0;
71-
Real stdDev = sigma * std::sqrt(T);
72-
Real shift = capletVolatility()->displacement();
7362
bool shiftedLn = capletVolatility()->volatilityType() == ShiftedLognormal;
74-
Rate fixing = shiftedLn ? blackFormula(optionType, effStrike, effectiveIndexFixing_, stdDev, 1.0, shift)
63+
Real shift = capletVolatility()->displacement();
64+
Real stdDev;
65+
Real effectiveTime = capletVolatility()->timeFromReference(fixingDates.back());
66+
if (effectiveVolatilityInput()) {
67+
// vol input is effective, i.e. we use a plain black model
68+
stdDev = capletVolatility()->volatility(fixingDates.back(), effStrike) * std::sqrt(effectiveTime);
69+
} else {
70+
// vol input is not effective:
71+
// for the standard deviation see Lyashenko, Mercurio, Looking forward to backward looking rates,
72+
// section 6.3. the idea is to dampen the average volatility sigma between the fixing start and fixing end
73+
// date by a linear function going from (fixing start, 1) to (fixing end, 0)
74+
Real fixingStartTime = capletVolatility()->timeFromReference(fixingDates.front());
75+
Real fixingEndTime = capletVolatility()->timeFromReference(fixingDates.back());
76+
Real sigma = capletVolatility()->volatility(
77+
std::max(fixingDates.front(), capletVolatility()->referenceDate() + 1), effStrike);
78+
Real T = std::max(fixingStartTime, 0.0);
79+
if (!close_enough(fixingEndTime, T))
80+
T += std::pow(fixingEndTime - T, 3.0) / std::pow(fixingEndTime - fixingStartTime, 2.0) / 3.0;
81+
stdDev = sigma * std::sqrt(T);
82+
}
83+
if (optionType == Option::Type::Call)
84+
effectiveCapletVolatility_ = stdDev / std::sqrt(effectiveTime);
85+
else
86+
effectiveFloorletVolatility_ = stdDev / std::sqrt(effectiveTime);
87+
Real fixing = shiftedLn ? blackFormula(optionType, effStrike, effectiveIndexFixing_, stdDev, 1.0, shift)
7588
: bachelierBlackFormula(optionType, effStrike, effectiveIndexFixing_, stdDev, 1.0);
7689
return gearing_ * fixing;
7790
}
@@ -88,6 +101,10 @@ Real cappedFlooredRate(Real r, Option::Type optionType, Real k) {
88101
} // namespace
89102

90103
Real BlackOvernightIndexedCouponPricer::optionletRateLocal(Option::Type optionType, Real effStrike) const {
104+
105+
QL_REQUIRE(!effectiveVolatilityInput(),
106+
"BlackAverageONIndexedCouponPricer::optionletRateLocal() does not support effective volatility input.");
107+
91108
// We compute a rate and a rawRate such that
92109
// rate * tau * nominal is the amount of the coupon with locally (i.e. daily) capped / floored rates
93110
// rawRate * tau * nominal is the amount of the coupon without capping / flooring the rate
@@ -262,6 +279,7 @@ void BlackAverageONIndexedCouponPricer::initialize(const FloatingRateCoupon& cou
262279
}
263280
swapletRate_ = coupon_->underlying()->rate();
264281
forwardRate_ = (swapletRate_ - coupon_->underlying()->spread()) / coupon_->underlying()->gearing();
282+
effectiveCapletVolatility_ = effectiveFloorletVolatility_ = Null<Real>();
265283
}
266284

267285
Real BlackAverageONIndexedCouponPricer::optionletRateGlobal(Option::Type optionType, Real effStrike) const {
@@ -285,24 +303,40 @@ Real BlackAverageONIndexedCouponPricer::optionletRateGlobal(Option::Type optionT
285303
QL_REQUIRE(!capletVolatility().empty(), "BlackAverageONIndexedCouponPricer: missing optionlet volatility");
286304
std::vector<Date> fixingDates = coupon_->underlying()->fixingDates();
287305
QL_REQUIRE(!fixingDates.empty(), "BlackAverageONIndexedCouponPricer: empty fixing dates");
288-
Real fixingStartTime = capletVolatility()->timeFromReference(fixingDates.front());
289-
Real fixingEndTime = capletVolatility()->timeFromReference(fixingDates.back());
290-
QL_REQUIRE(!close_enough(fixingEndTime, fixingStartTime),
291-
"BlackAverageONIndexedCouponPricer: fixingStartTime = fixingEndTime = " << fixingStartTime);
292-
Real sigma = capletVolatility()->volatility(
293-
std::max(fixingDates.front(), capletVolatility()->referenceDate() + 1), effStrike);
294-
Real stdDev = sigma * std::sqrt(std::max(fixingStartTime, 0.0) +
295-
std::pow(fixingEndTime - std::max(fixingStartTime, 0.0), 3.0) /
296-
std::pow(fixingEndTime - fixingStartTime, 2.0) / 3.0);
297-
Real shift = capletVolatility()->displacement();
298306
bool shiftedLn = capletVolatility()->volatilityType() == ShiftedLognormal;
299-
Rate fixing = shiftedLn ? blackFormula(optionType, effStrike, forwardRate_, stdDev, 1.0, shift)
307+
Real shift = capletVolatility()->displacement();
308+
Real stdDev;
309+
Real effectiveTime = capletVolatility()->timeFromReference(fixingDates.back());
310+
if (effectiveVolatilityInput()) {
311+
// vol input is effective, i.e. we use a plain black model
312+
stdDev = capletVolatility()->volatility(fixingDates.back(), effStrike) * std::sqrt(effectiveTime);
313+
} else {
314+
// vol input is not effective:
315+
Real fixingStartTime = capletVolatility()->timeFromReference(fixingDates.front());
316+
Real fixingEndTime = capletVolatility()->timeFromReference(fixingDates.back());
317+
QL_REQUIRE(!close_enough(fixingEndTime, fixingStartTime),
318+
"BlackAverageONIndexedCouponPricer: fixingStartTime = fixingEndTime = " << fixingStartTime);
319+
Real sigma = capletVolatility()->volatility(
320+
std::max(fixingDates.front(), capletVolatility()->referenceDate() + 1), effStrike);
321+
stdDev = sigma * std::sqrt(std::max(fixingStartTime, 0.0) +
322+
std::pow(fixingEndTime - std::max(fixingStartTime, 0.0), 3.0) /
323+
std::pow(fixingEndTime - fixingStartTime, 2.0) / 3.0);
324+
}
325+
if (optionType == Option::Type::Call)
326+
effectiveCapletVolatility_ = stdDev / std::sqrt(effectiveTime);
327+
else
328+
effectiveFloorletVolatility_ = stdDev / std::sqrt(effectiveTime);
329+
Real fixing = shiftedLn ? blackFormula(optionType, effStrike, forwardRate_, stdDev, 1.0, shift)
300330
: bachelierBlackFormula(optionType, effStrike, forwardRate_, stdDev, 1.0);
301331
return gearing_ * fixing;
302332
}
303333
}
304334

305335
Real BlackAverageONIndexedCouponPricer::optionletRateLocal(Option::Type optionType, Real effStrike) const {
336+
337+
QL_REQUIRE(!effectiveVolatilityInput(),
338+
"BlackAverageONIndexedCouponPricer::optionletRateLocal() does not support effective volatility input.");
339+
306340
// We compute a rate and a rawRate such that
307341
// rate * tau * nominal is the amount of the coupon with locally (i.e. daily) capped / floored rates
308342
// rawRate * tau * nominal is the amount of the coupon without capping / flooring the rate

QuantExt/qle/cashflows/overnightindexedcoupon.cpp

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,11 @@ void CappedFlooredOvernightIndexedCoupon::performCalculations() const {
357357
if (cap_ != Null<Real>())
358358
capletRate = (nakedOption_ && floor_ == Null<Real>() ? -1.0 : 1.0) * pricer()->capletRate(effectiveCap());
359359
rate_ = swapletRate + floorletRate - capletRate;
360+
auto p = boost::dynamic_pointer_cast<CappedFlooredOvernightIndexedCouponPricer>(pricer());
361+
QL_REQUIRE(p, "CappedFlooredOvernightIndexedCoupon::performCalculations(): internal error, could not cast to "
362+
"CappedFlooredOvernightIndexedCouponPricer");
363+
effectiveCapletVolatility_ = p->effectiveCapletVolatility();
364+
effectiveFloorletVolatility_ = p->effectiveFloorletVolatility();
360365
}
361366

362367
Rate CappedFlooredOvernightIndexedCoupon::cap() const { return gearing_ > 0.0 ? cap_ : floor_; }
@@ -420,6 +425,16 @@ Rate CappedFlooredOvernightIndexedCoupon::effectiveFloor() const {
420425
}
421426
}
422427

428+
Real CappedFlooredOvernightIndexedCoupon::effectiveCapletVolatility() const {
429+
calculate();
430+
return effectiveCapletVolatility_;
431+
}
432+
433+
Real CappedFlooredOvernightIndexedCoupon::effectiveFloorletVolatility() const {
434+
calculate();
435+
return effectiveFloorletVolatility_;
436+
}
437+
423438
void CappedFlooredOvernightIndexedCoupon::accept(AcyclicVisitor& v) {
424439
Visitor<CappedFlooredOvernightIndexedCoupon>* v1 = dynamic_cast<Visitor<CappedFlooredOvernightIndexedCoupon>*>(&v);
425440
if (v1 != 0)
@@ -431,11 +446,19 @@ void CappedFlooredOvernightIndexedCoupon::accept(AcyclicVisitor& v) {
431446
// CappedFlooredOvernightIndexedCouponPricer implementation (this is the base class only)
432447

433448
CappedFlooredOvernightIndexedCouponPricer::CappedFlooredOvernightIndexedCouponPricer(
434-
const Handle<OptionletVolatilityStructure>& v)
435-
: capletVol_(v) {
449+
const Handle<OptionletVolatilityStructure>& v, const bool effectiveVolatilityInput)
450+
: capletVol_(v), effectiveVolatilityInput_(effectiveVolatilityInput) {
436451
registerWith(capletVol_);
437452
}
438453

454+
bool CappedFlooredOvernightIndexedCouponPricer::effectiveVolatilityInput() const { return effectiveVolatilityInput_; }
455+
456+
Real CappedFlooredOvernightIndexedCouponPricer::effectiveCapletVolatility() const { return effectiveCapletVolatility_; }
457+
458+
Real CappedFlooredOvernightIndexedCouponPricer::effectiveFloorletVolatility() const {
459+
return effectiveFloorletVolatility_;
460+
}
461+
439462
Handle<OptionletVolatilityStructure> CappedFlooredOvernightIndexedCouponPricer::capletVolatility() const {
440463
return capletVol_;
441464
}

QuantExt/qle/cashflows/overnightindexedcoupon.hpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ class CappedFlooredOvernightIndexedCoupon : public FloatingRateCoupon {
183183
Rate effectiveCap() const;
184184
//! effective floor of fixing
185185
Rate effectiveFloor() const;
186+
//! effective caplet volatility
187+
Real effectiveCapletVolatility() const;
188+
//! effective floorlet volatility
189+
Real effectiveFloorletVolatility() const;
186190
//@}
187191
//! \name Visitability
188192
//@{
@@ -200,16 +204,25 @@ class CappedFlooredOvernightIndexedCoupon : public FloatingRateCoupon {
200204
Rate cap_, floor_;
201205
bool nakedOption_;
202206
bool localCapFloor_;
207+
mutable Real effectiveCapletVolatility_;
208+
mutable Real effectiveFloorletVolatility_;
203209
};
204210

205211
//! capped floored overnight indexed coupon pricer base class
206212
class CappedFlooredOvernightIndexedCouponPricer : public FloatingRateCouponPricer {
207213
public:
208-
CappedFlooredOvernightIndexedCouponPricer(const Handle<OptionletVolatilityStructure>& v);
214+
CappedFlooredOvernightIndexedCouponPricer(const Handle<OptionletVolatilityStructure>& v,
215+
const bool effectiveVolatilityInput = false);
209216
Handle<OptionletVolatilityStructure> capletVolatility() const;
217+
bool effectiveVolatilityInput() const;
218+
Real effectiveCapletVolatility() const; // only available after capletRate() was called
219+
Real effectiveFloorletVolatility() const; // only available after floorletRate() was called
210220

211-
private:
221+
protected:
212222
Handle<OptionletVolatilityStructure> capletVol_;
223+
bool effectiveVolatilityInput_;
224+
mutable Real effectiveCapletVolatility_;
225+
mutable Real effectiveFloorletVolatility_;
213226
};
214227

215228
//! helper class building a sequence of overnight coupons

0 commit comments

Comments
 (0)