• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lballabio / QuantLib / 30783683586

03 Aug 2026 04:11AM UTC coverage: 74.96% (+0.04%) from 74.923%
30783683586

Pull #2705

github

web-flow
Merge 10275255b into 60be62969
Pull Request #2705: Rfr caplet vol bootstrapping

153 of 156 new or added lines in 3 files covered. (98.08%)

9 existing lines in 2 files now uncovered.

60217 of 80332 relevant lines covered (74.96%)

8533134.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.29
/ql/termstructures/volatility/optionlet/optionletstripper2.cpp
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2

3
/*
4
 Copyright (C) 2007 Giorgio Facchinetti
5
 Copyright (C) 2010 Ferdinando Ametrano
6
 Copyright (C) 2026 Kyrylo Protsenko
7

8
 This file is part of QuantLib, a free-software/open-source library
9
 for financial quantitative analysts and developers - http://quantlib.org/
10

11
 QuantLib is free software: you can redistribute it and/or modify it
12
 under the terms of the QuantLib license.  You should have received a
13
 copy of the license along with this program; if not, please email
14
 <quantlib-dev@lists.sf.net>. The license is also available online at
15
 <https://www.quantlib.org/license.shtml>.
16

17
 This program is distributed in the hope that it will be useful, but WITHOUT
18
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19
 FOR A PARTICULAR PURPOSE.  See the license for more details.
20
*/
21

22
#include <ql/cashflows/cashflows.hpp>
23
#include <ql/cashflows/floatingratecoupon.hpp>
24
#include <ql/indexes/iborindex.hpp>
25
#include <ql/instruments/capfloor.hpp>
26
#include <ql/math/solvers1d/brent.hpp>
27
#include <ql/quotes/simplequote.hpp>
28
#include <ql/termstructures/volatility/capfloor/capfloortermvolcurve.hpp>
29
#include <ql/termstructures/volatility/optionlet/optionletstripper1.hpp>
30
#include <ql/termstructures/volatility/optionlet/optionletstripper2.hpp>
31
#include <ql/termstructures/volatility/optionlet/spreadedoptionletvol.hpp>
32
#include <ql/termstructures/volatility/optionlet/strippedoptionletadapter.hpp>
33
#include <utility>
34

35

36
namespace QuantLib {
37

38
    OptionletStripper2::OptionletStripper2(
10✔
39
        const ext::shared_ptr<OptionletStripper1>& optionletStripper1,
40
        const Handle<CapFloorTermVolCurve>& atmCapFloorTermVolCurve)
10✔
41
    : OptionletStripper(optionletStripper1->termVolSurface(),
20✔
42
                        optionletStripper1->iborIndex(),
20✔
43
                        Handle<YieldTermStructure>(),
10✔
44
                        optionletStripper1->volatilityType(),
10✔
45
                        optionletStripper1->displacement(),
10✔
46
                        optionletStripper1->optionletFrequency(),
10✔
47
                        optionletStripper1->paymentLag()),
10✔
48
      stripper1_(optionletStripper1), atmCapFloorTermVolCurve_(atmCapFloorTermVolCurve),
49
      dc_(stripper1_->termVolSurface()->dayCounter()),
10✔
50
      nOptionExpiries_(atmCapFloorTermVolCurve->optionTenors().size()),
10✔
51
      atmCapFloorStrikes_(nOptionExpiries_), atmCapFloorPrices_(nOptionExpiries_),
10✔
52
      spreadsVolImplied_(nOptionExpiries_), caps_(nOptionExpiries_) {
70✔
53
        registerWith(stripper1_);
20✔
54
        registerWith(atmCapFloorTermVolCurve_);
10✔
55

56
        QL_REQUIRE(dc_ == atmCapFloorTermVolCurve->dayCounter(),
20✔
57
                   "different day counters provided");
58
    }
10✔
59

60
    void OptionletStripper2::performCalculations() const {
10✔
61

62
        //// optionletStripper data
63
        optionletDates_ = stripper1_->optionletFixingDates();
10✔
64
        optionletPaymentDates_ = stripper1_->optionletPaymentDates();
10✔
65
        optionletAccrualPeriods_ = stripper1_->optionletAccrualPeriods();
10✔
66
        optionletTimes_ = stripper1_->optionletFixingTimes();
10✔
67
        atmOptionletRate_ = stripper1_->atmOptionletRates();
10✔
68
        for (Size i=0; i<optionletTimes_.size(); ++i) {
303✔
69
            optionletStrikes_[i] = stripper1_->optionletStrikes(i);
293✔
70
            optionletVolatilities_[i] = stripper1_->optionletVolatilities(i);
293✔
71
        }
72

73
        // atmCapFloorTermVolCurve data
74
        const std::vector<Period>& optionExpiriesTenors =
75
                                    atmCapFloorTermVolCurve_->optionTenors();
10✔
76
        const std::vector<Time>& optionExpiriesTimes =
77
                                    atmCapFloorTermVolCurve_->optionTimes();
10✔
78
        std::vector<std::vector<Size>> capOptionletIndices(nOptionExpiries_);
10✔
79

80
        for (Size j=0; j<nOptionExpiries_; ++j) {
98✔
81
            Volatility atmOptionVol = atmCapFloorTermVolCurve_->volatility(
89✔
82
                optionExpiriesTimes[j], 33.3333); // dummy strike
89✔
83
            Handle<Quote> atmOptionVolHandle(
84
                ext::make_shared<SimpleQuote>(atmOptionVol));
89✔
85
            auto engine = makeCapFloorPricingEngine(
86
                iborIndex_->forwardingTermStructure(), atmOptionVolHandle);
268✔
87

88
            Leg leg = makeCapFloorLeg(optionExpiriesTenors[j]);
89✔
89
            if (isOvernightIndex()) {
88✔
90
                for (const auto& cashflow : leg) {
690✔
91
                    auto coupon = ext::dynamic_pointer_cast<FloatingRateCoupon>(cashflow);
660✔
92
                    QL_REQUIRE(coupon, "non-floating-rate coupon in cap/floor leg");
660✔
93
                    auto optionlet = std::find(
660✔
94
                        optionletDates_.begin(), optionletDates_.end(), coupon->fixingDate());
660✔
95
                    QL_REQUIRE(optionlet != optionletDates_.end(),
660✔
96
                               "ATM cap/floor fixing date " << coupon->fixingDate()
97
                                                             << " is not represented in the source "
98
                                                                "optionlet surface");
99
                    capOptionletIndices[j].push_back(optionlet - optionletDates_.begin());
660✔
100
                }
101
            } else {
102
                // Historical rule: the adjustment covers the optionlets up to
103
                // the cap's leg size, inclusive.
104
                for (Size i = 0; i < optionletVolatilities_.size(); ++i)
1,800✔
105
                    if (i <= leg.size())
1,742✔
106
                        capOptionletIndices[j].push_back(i);
756✔
107
            }
108
            atmCapFloorStrikes_[j] = CashFlows::atmRate(
176✔
109
                leg, **iborIndex_->forwardingTermStructure(), false,
264✔
110
                iborIndex_->forwardingTermStructure()->referenceDate());
264✔
111
            caps_[j] = ext::make_shared<CapFloor>(
176✔
112
                CapFloor::Cap, leg, std::vector<Rate>(1, atmCapFloorStrikes_[j]));
176✔
113
            caps_[j]->setPricingEngine(engine);
88✔
114
            atmCapFloorPrices_[j] = caps_[j]->NPV();
88✔
115
        }
88✔
116

117
        Handle<OptionletVolatilityStructure> adapter(
118
            ext::make_shared<StrippedOptionletAdapter>(stripper1_));
9✔
119
        adapter->enableExtrapolation();
9✔
120
        spreadsVolImplied_ = spreadsVolImplied(adapter);
9✔
121

122
        Volatility unadjustedVol, adjustedVol;
123
        for (Size j=0; j<nOptionExpiries_; ++j) {
97✔
124
            for (Size i : capOptionletIndices[j]) {
1,504✔
125
                unadjustedVol = adapter->volatility(optionletTimes_[i],
1,416✔
126
                                                    atmCapFloorStrikes_[j]);
127
                adjustedVol = unadjustedVol + spreadsVolImplied_[j];
1,416✔
128

129
                // insert adjusted volatility
130
                auto previous =
131
                    std::lower_bound(optionletStrikes_[i].begin(),
1,416✔
132
                                     optionletStrikes_[i].end(),
133
                                     atmCapFloorStrikes_[j]);
134
                Size insertIndex = previous - optionletStrikes_[i].begin();
135

136
                optionletStrikes_[i].insert(
1,416✔
137
                            optionletStrikes_[i].begin() + insertIndex,
138
                            atmCapFloorStrikes_[j]);
139
                optionletVolatilities_[i].insert(
1,416✔
140
                            optionletVolatilities_[i].begin() + insertIndex,
141
                            adjustedVol);
142
            }
143
        }
144
    }
10✔
145

146
    std::vector<Volatility> OptionletStripper2::spreadsVolImplied(
9✔
147
        const Handle<OptionletVolatilityStructure>& baseVolatility) const {
148

149
        Brent solver;
150
        std::vector<Volatility> result(nOptionExpiries_);
9✔
151
        const Volatility maxSpread = 0.1;
152
        for (Size j=0; j<nOptionExpiries_; ++j) {
97✔
153
            Volatility minBaseVol = QL_MAX_REAL;
88✔
154
            for (const auto& cashflow : caps_[j]->floatingLeg()) {
1,451✔
155
                auto coupon = ext::dynamic_pointer_cast<FloatingRateCoupon>(cashflow);
1,363✔
156
                QL_REQUIRE(coupon, "non-floating-rate coupon in cap/floor leg");
1,363✔
157
                minBaseVol = std::min(
1,363✔
158
                    minBaseVol,
159
                    baseVolatility->volatility(
2,726✔
160
                        coupon->fixingDate(), atmCapFloorStrikes_[j], true));
2,726✔
161
            }
162
            QL_REQUIRE(minBaseVol != QL_MAX_REAL,
88✔
163
                       "empty cap/floor leg for option expiry "
164
                           << atmCapFloorTermVolCurve_->optionTenors()[j]);
165
            QL_REQUIRE(minBaseVol >= 0.0,
88✔
166
                       "negative optionlet volatility at ATM strike "
167
                           << atmCapFloorStrikes_[j]);
168

169
            auto spreadQuote = ext::make_shared<SimpleQuote>(-1.0);
88✔
170
            Handle<OptionletVolatilityStructure> spreadedVolatility(
171
                ext::make_shared<SpreadedOptionletVolatility>(
88✔
172
                    baseVolatility, Handle<Quote>(spreadQuote)));
264✔
173
            caps_[j]->setPricingEngine(makeCapFloorPricingEngine(
176✔
174
                iborIndex_->forwardingTermStructure(), spreadedVolatility));
176✔
175

176
            ObjectiveFunction f(spreadQuote, caps_[j], atmCapFloorPrices_[j]);
264✔
177
            solver.setMaxEvaluations(maxEvaluations_);
88✔
178
            Real valueAtZero = f(0.0);
88✔
179
            if (close(valueAtZero, 0.0)) {
88✔
NEW
180
                result[j] = 0.0;
×
181
            } else if (valueAtZero > 0.0) {
88✔
182
                QL_REQUIRE(minBaseVol > 0.0,
50✔
183
                           "ATM cap/floor price for option expiry "
184
                               << atmCapFloorTermVolCurve_->optionTenors()[j]
185
                               << " is below the minimum attainable price");
186
                Volatility minSpread = -minBaseVol;
50✔
187
                Real valueAtMinSpread = f(minSpread);
50✔
188
                QL_REQUIRE(valueAtMinSpread <= 0.0,
50✔
189
                           "ATM cap/floor price for option expiry "
190
                               << atmCapFloorTermVolCurve_->optionTenors()[j]
191
                               << " is below the minimum attainable price");
192
                result[j] = solver.solve(
50✔
193
                    f, accuracy_, 0.5 * minSpread, minSpread, 0.0);
50✔
194
            } else {
195
                Real valueAtMaxSpread = f(maxSpread);
38✔
196
                QL_REQUIRE(valueAtMaxSpread >= 0.0,
38✔
197
                           "ATM cap/floor price for option expiry "
198
                               << atmCapFloorTermVolCurve_->optionTenors()[j]
199
                               << " requires a volatility spread greater than "
200
                               << maxSpread);
201
                result[j] = solver.solve(
38✔
202
                    f, accuracy_, 0.5 * maxSpread, 0.0, maxSpread);
38✔
203
            }
204
        }
88✔
205
        return result;
9✔
206
    }
×
207

208
    std::vector<Volatility> OptionletStripper2::spreadsVol() const {
3✔
209
        calculate();
3✔
210
        return spreadsVolImplied_;
3✔
211
    }
212

213
    std::vector<Rate> OptionletStripper2::atmCapFloorStrikes() const{
4✔
214
        calculate();
4✔
215
        return atmCapFloorStrikes_;
4✔
216
    }
217

218
    std::vector<Real> OptionletStripper2::atmCapFloorPrices() const {
7✔
219
        calculate();
7✔
220
        return atmCapFloorPrices_;
6✔
221
    }
222

223
//==========================================================================//
224
//                 OptionletStripper2::ObjectiveFunction                    //
225
//==========================================================================//
226

227
    OptionletStripper2::ObjectiveFunction::ObjectiveFunction(
88✔
228
        ext::shared_ptr<SimpleQuote> spreadQuote,
229
        ext::shared_ptr<CapFloor> cap,
230
        Real targetValue)
88✔
231
    : spreadQuote_(std::move(spreadQuote)), cap_(std::move(cap)),
232
      targetValue_(targetValue) {}
88✔
233

234
    Real OptionletStripper2::ObjectiveFunction::operator()(Volatility s) const
690✔
235
    {
236
        if (s!=spreadQuote_->value())
690✔
237
            spreadQuote_->setValue(s);
638✔
238
        return cap_->NPV()-targetValue_;
690✔
239
    }
240
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc