Coverage Report

Created: 2026-02-03 07:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/experimental/coupons/lognormalcmsspreadpricer.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
/*
3
  Copyright (C) 2014, 2015, 2018 Peter Caspers
4
5
  This file is part of QuantLib, a free-software/open-source library
6
  for financial quantitative analysts and developers - http://quantlib.org/
7
8
  QuantLib is free software: you can redistribute it and/or modify it
9
  under the terms of the QuantLib license.  You should have received a
10
  copy of the license along with this program; if not, please email
11
  <quantlib-dev@lists.sf.net>. The license is also available online at
12
  <https://www.quantlib.org/license.shtml>.
13
14
15
  This program is distributed in the hope that it will be useful, but
16
  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17
  or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
18
*/
19
20
/*! \file lognormalcmsspreadpricer.cpp
21
*/
22
23
#include <ql/experimental/coupons/cmsspreadcoupon.hpp>
24
#include <ql/experimental/coupons/lognormalcmsspreadpricer.hpp>
25
#include <ql/math/integrals/kronrodintegral.hpp>
26
#include <ql/pricingengines/blackformula.hpp>
27
#include <ql/termstructures/volatility/swaption/swaptionvolcube.hpp>
28
#include <ql/optional.hpp>
29
#include <utility>
30
31
using std::sqrt;
32
33
namespace QuantLib {
34
35
    class LognormalCmsSpreadPricer::integrand_f {
36
        const LognormalCmsSpreadPricer* pricer;
37
      public:
38
        explicit integrand_f(const LognormalCmsSpreadPricer* pricer)
39
0
        : pricer(pricer) {}
40
0
        Real operator()(Real x) const {
41
0
            return pricer->integrand(x);
42
0
        }
43
    };
44
45
    LognormalCmsSpreadPricer::LognormalCmsSpreadPricer(
46
        const ext::shared_ptr<CmsCouponPricer>& cmsPricer,
47
        const Handle<Quote>& correlation,
48
        Handle<YieldTermStructure> couponDiscountCurve,
49
        const Size integrationPoints,
50
        const ext::optional<VolatilityType>& volatilityType,
51
        const Real shift1,
52
        const Real shift2)
53
0
    : CmsSpreadCouponPricer(correlation), cmsPricer_(cmsPricer),
54
0
      couponDiscountCurve_(std::move(couponDiscountCurve)) {
55
56
0
        registerWith(correlation);
57
0
        if (!couponDiscountCurve_.empty())
58
0
            registerWith(couponDiscountCurve_);
59
0
        registerWith(cmsPricer_);
60
61
0
        QL_REQUIRE(integrationPoints >= 4,
62
0
                   "at least 4 integration points should be used ("
63
0
                       << integrationPoints << ")");
64
0
        integrator_ =
65
0
            ext::make_shared<GaussHermiteIntegration>(integrationPoints);
66
67
0
        cnd_ = ext::make_shared<CumulativeNormalDistribution>(0.0, 1.0);
68
69
0
        if (!volatilityType) {
70
0
            QL_REQUIRE(shift1 == Null<Real>() && shift2 == Null<Real>(),
71
0
                       "if volatility type is inherited, no shifts should be "
72
0
                       "specified");
73
0
            inheritedVolatilityType_ = true;
74
0
            volType_ = cmsPricer->swaptionVolatility()->volatilityType();
75
0
        } else {
76
0
            shift1_ = shift1 == Null<Real>() ? 0.0 : shift1;
77
0
            shift2_ = shift2 == Null<Real>() ? 0.0 : shift2;
78
0
            inheritedVolatilityType_ = false;
79
0
            volType_ = *volatilityType;
80
0
        }
81
0
    }
Unexecuted instantiation: QuantLib::LognormalCmsSpreadPricer::LognormalCmsSpreadPricer(boost::shared_ptr<QuantLib::CmsCouponPricer> const&, QuantLib::Handle<QuantLib::Quote> const&, QuantLib::Handle<QuantLib::YieldTermStructure>, unsigned long, std::__1::optional<QuantLib::VolatilityType> const&, double, double)
Unexecuted instantiation: QuantLib::LognormalCmsSpreadPricer::LognormalCmsSpreadPricer(boost::shared_ptr<QuantLib::CmsCouponPricer> const&, QuantLib::Handle<QuantLib::Quote> const&, QuantLib::Handle<QuantLib::YieldTermStructure>, unsigned long, std::__1::optional<QuantLib::VolatilityType> const&, double, double)
82
83
0
    Real LognormalCmsSpreadPricer::integrand(const Real x) const {
84
85
        // this is Brigo, 13.16.2 with x = v / sqrt(2)
86
87
0
        Real v = M_SQRT2 * x;
88
0
        Real h =
89
0
            k_ - b_ * s2_ * std::exp((m2_ - 0.5 * v2_ * v2_) * fixingTime_ +
90
0
                                     v2_ * std::sqrt(fixingTime_) * v);
91
0
        Real phi1, phi2;
92
0
        phi1 = (*cnd_)(
93
0
            phi_ * (std::log(a_ * s1_ / h) +
94
0
                    (m1_ + (0.5 - rho_ * rho_) * v1_ * v1_) * fixingTime_ +
95
0
                    rho_ * v1_ * std::sqrt(fixingTime_) * v) /
96
0
            (v1_ * std::sqrt(fixingTime_ * (1.0 - rho_ * rho_))));
97
0
        phi2 = (*cnd_)(
98
0
            phi_ * (std::log(a_ * s1_ / h) +
99
0
                    (m1_ - 0.5 * v1_ * v1_) * fixingTime_ +
100
0
                    rho_ * v1_ * std::sqrt(fixingTime_) * v) /
101
0
            (v1_ * std::sqrt(fixingTime_ * (1.0 - rho_ * rho_))));
102
0
        Real f = a_ * phi_ * s1_ *
103
0
                     std::exp(m1_ * fixingTime_ -
104
0
                              0.5 * rho_ * rho_ * v1_ * v1_ * fixingTime_ +
105
0
                              rho_ * v1_ * std::sqrt(fixingTime_) * v) *
106
0
                     phi1 -
107
0
                 phi_ * h * phi2;
108
0
        return std::exp(-x * x) * f;
109
0
    }
110
111
0
    Real LognormalCmsSpreadPricer::integrand_normal(const Real x) const {
112
113
        // this is http://ssrn.com/abstract=2686998, 3.20 with x = s / sqrt(2)
114
115
0
        Real s = M_SQRT2 * x;
116
117
0
        Real beta =
118
0
            phi_ *
119
0
            (gearing1_ * adjustedRate1_ + gearing2_ * adjustedRate2_ - k_ +
120
0
             std::sqrt(fixingTime_) *
121
0
                 (rho_ * gearing1_ * vol1_ + gearing2_ * vol2_) * s);
122
0
        Real f =
123
0
            close_enough(alpha_, 0.0)
124
0
                ? Real(std::max(beta, 0.0))
125
0
                : psi_ * alpha_ / (M_SQRTPI * M_SQRT2) *
126
0
                          std::exp(-beta * beta / (2.0 * alpha_ * alpha_)) +
127
0
                      beta * (1.0 - (*cnd_)(-psi_ * beta / alpha_));
128
0
        return std::exp(-x * x) * f;
129
0
    }
130
131
    void
132
0
    LognormalCmsSpreadPricer::initialize(const FloatingRateCoupon &coupon) {
133
134
0
        coupon_ = dynamic_cast<const CmsSpreadCoupon *>(&coupon);
135
0
        QL_REQUIRE(coupon_, "CMS spread coupon needed");
136
0
        index_ = coupon_->swapSpreadIndex();
137
0
        gearing_ = coupon_->gearing();
138
0
        spread_ = coupon_->spread();
139
140
0
        fixingDate_ = coupon_->fixingDate();
141
0
        paymentDate_ = coupon_->date();
142
143
        // if no coupon discount curve is given just use the discounting curve
144
        // from the _first_ swap index.
145
        // for rate calculation this curve cancels out in the computation, so
146
        // e.g. the discounting
147
        // swap engine will produce correct results, even if the
148
        // couponDiscountCurve is not set here.
149
        // only the price member function in this class will be dependent on the
150
        // coupon discount curve.
151
152
0
        today_ = QuantLib::Settings::instance().evaluationDate();
153
154
0
        if (couponDiscountCurve_.empty())
155
0
            couponDiscountCurve_ =
156
0
                index_->swapIndex1()->exogenousDiscount()
157
0
                    ? index_->swapIndex1()->discountingTermStructure()
158
0
                    : index_->swapIndex1()->forwardingTermStructure();
159
160
0
        discount_ = paymentDate_ > couponDiscountCurve_->referenceDate()
161
0
                        ? couponDiscountCurve_->discount(paymentDate_)
162
0
                        : 1.0;
163
164
0
        spreadLegValue_ = spread_ * coupon_->accrualPeriod() * discount_;
165
166
0
        gearing1_ = index_->gearing1();
167
0
        gearing2_ = index_->gearing2();
168
169
0
        QL_REQUIRE(gearing1_ > 0.0 && gearing2_ < 0.0,
170
0
                   "gearing1 (" << gearing1_
171
0
                                << ") should be positive while gearing2 ("
172
0
                                << gearing2_ << ") should be negative");
173
174
0
        c1_ = ext::make_shared<CmsCoupon>(
175
0
            coupon_->date(), coupon_->nominal(), coupon_->accrualStartDate(),
176
0
            coupon_->accrualEndDate(), coupon_->fixingDays(),
177
0
            index_->swapIndex1(), 1.0, 0.0, coupon_->referencePeriodStart(),
178
0
            coupon_->referencePeriodEnd(), coupon_->dayCounter(),
179
0
            coupon_->isInArrears());
180
181
0
        c2_ = ext::make_shared<CmsCoupon>(
182
0
            coupon_->date(), coupon_->nominal(), coupon_->accrualStartDate(),
183
0
            coupon_->accrualEndDate(), coupon_->fixingDays(),
184
0
            index_->swapIndex2(), 1.0, 0.0, coupon_->referencePeriodStart(),
185
0
            coupon_->referencePeriodEnd(), coupon_->dayCounter(),
186
0
            coupon_->isInArrears());
187
188
0
        c1_->setPricer(cmsPricer_);
189
0
        c2_->setPricer(cmsPricer_);
190
191
0
        if (fixingDate_ > today_) {
192
193
0
            fixingTime_ = cmsPricer_->swaptionVolatility()->timeFromReference(
194
0
                fixingDate_);
195
196
0
            swapRate1_ = c1_->indexFixing();
197
0
            swapRate2_ = c2_->indexFixing();
198
199
0
            adjustedRate1_ = c1_->adjustedFixing();
200
0
            adjustedRate2_ = c2_->adjustedFixing();
201
202
0
            ext::shared_ptr<SwaptionVolatilityStructure> swvol =
203
0
                *cmsPricer_->swaptionVolatility();
204
0
            ext::shared_ptr<SwaptionVolatilityCube> swcub =
205
0
                ext::dynamic_pointer_cast<SwaptionVolatilityCube>(swvol);
206
207
0
            if(inheritedVolatilityType_ && volType_ == ShiftedLognormal) {
208
0
                shift1_ =
209
0
                    swvol->shift(fixingDate_, index_->swapIndex1()->tenor());
210
0
                shift2_ =
211
0
                    swvol->shift(fixingDate_, index_->swapIndex2()->tenor());
212
0
            }
213
214
0
            if (swcub == nullptr) {
215
                // not a cube, just an atm surface given, so we can
216
                // not easily convert volatilities and just forbid it
217
0
                QL_REQUIRE(inheritedVolatilityType_,
218
0
                           "if only an atm surface is given, the volatility "
219
0
                           "type must be inherited");
220
0
                vol1_ = swvol->volatility(
221
0
                    fixingDate_, index_->swapIndex1()->tenor(), swapRate1_);
222
0
                vol2_ = swvol->volatility(
223
0
                    fixingDate_, index_->swapIndex2()->tenor(), swapRate2_);
224
0
            } else {
225
0
                vol1_ = swcub->smileSection(fixingDate_,
226
0
                                            index_->swapIndex1()->tenor())
227
0
                            ->volatility(swapRate1_, volType_, shift1_);
228
0
                vol2_ = swcub->smileSection(fixingDate_,
229
0
                                            index_->swapIndex2()->tenor())
230
0
                            ->volatility(swapRate2_, volType_, shift2_);
231
0
            }
232
233
0
            if(volType_ == ShiftedLognormal) {
234
0
                mu1_ = 1.0 / fixingTime_ * std::log((adjustedRate1_ + shift1_) /
235
0
                                                    (swapRate1_ + shift1_));
236
0
                mu2_ = 1.0 / fixingTime_ * std::log((adjustedRate2_ + shift2_) /
237
0
                                                    (swapRate2_ + shift2_));
238
0
            }
239
            // for the normal volatility case we do not need the drifts
240
            // but rather use adjusted rates directly in the integrand
241
242
0
            rho_ = std::max(std::min(correlation()->value(), 0.9999),
243
0
                            -0.9999); // avoid division by zero in integrand
244
0
        } else {
245
            // fixing is in the past or today
246
0
            adjustedRate1_ = c1_->indexFixing();
247
0
            adjustedRate2_ = c2_->indexFixing();
248
0
        }
249
0
    }
250
251
    Real LognormalCmsSpreadPricer::optionletPrice(Option::Type optionType,
252
0
                                                  Real strike) const {
253
        // this method is only called for future fixings
254
0
        optionType_ = optionType;
255
0
        phi_ = optionType == Option::Call ? 1.0 : -1.0;
256
0
        Real res = 0.0;
257
0
        if (volType_ == ShiftedLognormal) {
258
            // (shifted) lognormal volatility
259
0
            if (strike >= 0.0) {
260
0
                a_ = gearing1_;
261
0
                b_ = gearing2_;
262
0
                s1_ = swapRate1_ + shift1_;
263
0
                s2_ = swapRate2_ + shift2_;
264
0
                m1_ = mu1_;
265
0
                m2_ = mu2_;
266
0
                v1_ = vol1_;
267
0
                v2_ = vol2_;
268
0
                k_ = strike + gearing1_ * shift1_ + gearing2_ * shift2_;
269
0
            } else {
270
0
                a_ = -gearing2_;
271
0
                b_ = -gearing1_;
272
0
                s1_ = swapRate2_ + shift1_;
273
0
                s2_ = swapRate1_ + shift2_;
274
0
                m1_ = mu2_;
275
0
                m2_ = mu1_;
276
0
                v1_ = vol2_;
277
0
                v2_ = vol1_;
278
0
                k_ = -strike - gearing1_ * shift1_ - gearing2_ * shift2_;
279
0
                res += phi_ * (gearing1_ * adjustedRate1_ +
280
0
                               gearing2_ * adjustedRate2_ - strike);
281
0
            }
282
0
            res +=
283
0
                1.0 / M_SQRTPI * (*integrator_)(integrand_f(this));
284
0
        } else {
285
            // normal volatility
286
0
            Real forward = gearing1_ * adjustedRate1_ +
287
0
                gearing2_ * adjustedRate2_;
288
0
            Real stddev =
289
0
                std::sqrt(fixingTime_ *
290
0
                          (gearing1_ * gearing1_ * vol1_ * vol1_ +
291
0
                           gearing2_ * gearing2_ * vol2_ * vol2_ +
292
0
                           2.0 * gearing1_ * gearing2_ * rho_ * vol1_ * vol2_));
293
0
            res =
294
0
                bachelierBlackFormula(optionType_, strike, forward, stddev, 1.0);
295
0
        }
296
0
        return res * discount_ * coupon_->accrualPeriod();
297
0
    }
298
299
0
    Rate LognormalCmsSpreadPricer::swapletRate() const {
300
0
        return swapletPrice() / (coupon_->accrualPeriod() * discount_);
301
0
    }
302
303
0
    Real LognormalCmsSpreadPricer::capletPrice(Rate effectiveCap) const {
304
        // caplet is equivalent to call option on fixing
305
0
        if (fixingDate_ <= today_) {
306
            // the fixing is determined
307
0
            const Rate Rs = std::max(
308
0
                coupon_->index()->fixing(fixingDate_) - effectiveCap, 0.);
309
0
            Rate price = gearing_ * Rs * coupon_->accrualPeriod() * discount_;
310
0
            return price;
311
0
        } else {
312
0
            Real capletPrice = optionletPrice(Option::Call, effectiveCap);
313
0
            return gearing_ * capletPrice;
314
0
        }
315
0
    }
316
317
0
    Rate LognormalCmsSpreadPricer::capletRate(Rate effectiveCap) const {
318
0
        return capletPrice(effectiveCap) /
319
0
               (coupon_->accrualPeriod() * discount_);
320
0
    }
321
322
0
    Real LognormalCmsSpreadPricer::floorletPrice(Rate effectiveFloor) const {
323
        // floorlet is equivalent to put option on fixing
324
0
        if (fixingDate_ <= today_) {
325
            // the fixing is determined
326
0
            const Rate Rs = std::max(
327
0
                effectiveFloor - coupon_->index()->fixing(fixingDate_), 0.);
328
0
            Rate price = gearing_ * Rs * coupon_->accrualPeriod() * discount_;
329
0
            return price;
330
0
        } else {
331
0
            Real floorletPrice = optionletPrice(Option::Put, effectiveFloor);
332
0
            return gearing_ * floorletPrice;
333
0
        }
334
0
    }
335
336
0
    Rate LognormalCmsSpreadPricer::floorletRate(Rate effectiveFloor) const {
337
0
        return floorletPrice(effectiveFloor) /
338
0
               (coupon_->accrualPeriod() * discount_);
339
0
    }
340
341
0
    Real LognormalCmsSpreadPricer::swapletPrice() const {
342
0
        return gearing_ * coupon_->accrualPeriod() * discount_ *
343
0
                   (gearing1_ * adjustedRate1_ + gearing2_ * adjustedRate2_) +
344
0
               spreadLegValue_;
345
0
    }
346
}