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

lballabio / QuantLib / 22123883734

18 Feb 2026 02:19AM UTC coverage: 74.224% (+0.02%) from 74.201%
22123883734

Pull #2443

github

web-flow
Merge 5d7eca8ff into 1cd1ce4a2
Pull Request #2443: Add BlackVarianceSurfaceFromSmileSections for ragged vol grids

39 of 45 new or added lines in 2 files covered. (86.67%)

108 existing lines in 11 files now uncovered.

57540 of 77522 relevant lines covered (74.22%)

8745985.79 hits per line

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

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

3
/*
4
 Copyright (C) 2013, 2015, 2018 Peter Caspers
5

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

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

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

20
#include <ql/termstructures/volatility/smilesectionutils.hpp>
21
#include <ql/math/comparison.hpp>
22
#include <algorithm>
23

24
namespace QuantLib {
25

26
    SmileSectionUtils::SmileSectionUtils(const SmileSection &section,
246✔
27
                                         const std::vector<Real> &moneynessGrid,
28
                                         const Real atm,
29
                                         const bool deleteArbitragePoints) {
246✔
30

31
        if (!moneynessGrid.empty()) {
246✔
32
            QL_REQUIRE(
218✔
33
                section.volatilityType() == Normal || moneynessGrid[0] >= 0.0,
34
                "moneyness grid should only contain non negative values ("
35
                    << moneynessGrid[0] << ")");
36
            for (Size i = 0; i < moneynessGrid.size() - 1; i++) {
1,970✔
37
                QL_REQUIRE(moneynessGrid[i] < moneynessGrid[i + 1],
1,752✔
38
                           "moneyness grid should contain strictly increasing "
39
                           "values ("
40
                               << moneynessGrid[i] << ","
41
                               << moneynessGrid[i + 1] << " at indices " << i
42
                               << ", " << i + 1 << ")");
43
            }
44
        }
45

46
        if (atm == Null<Real>()) {
246✔
47
            f_ = section.atmLevel();
×
48
            QL_REQUIRE(f_ != Null<Real>(),
×
49
                       "atm level must be provided by source section or given "
50
                       "in the constructor");
51
        } else {
52
            f_ = atm;
246✔
53
        }
54

55
        std::vector<Real> tmp;
56

57
        static const Real defaultMoney[] = { 0.0,  0.01, 0.05, 0.10, 0.25, 0.40,
58
                                             0.50, 0.60, 0.70, 0.80, 0.90, 1.0,
59
                                             1.25, 1.5,  1.75, 2.0,  5.0,  7.5,
60
                                             10.0, 15.0, 20.0 };
61
        static const Real defaultMoneyNormal[] = {
62
            -0.20,  -0.15,  -0.10,  -0.075,  -0.05,   -0.04,   -0.03,
63
            -0.02,  -0.015, -0.01,  -0.0075, -0.0050, -0.0025, 0.0,
64
            0.0025, 0.0050, 0.0075, 0.01,    0.015,   0.02,    0.03,
65
            0.04,   0.05,   0.075,  0.10,    0.15,    0.20
66
        };
67

68
        if (moneynessGrid.empty()) {
246✔
69
            tmp = section.volatilityType() == Normal
28✔
70
                      ? std::vector<Real>(defaultMoneyNormal,
56✔
71
                                          defaultMoneyNormal + 27)
72
                      : std::vector<Real>(defaultMoney, defaultMoney + 21);
73
        }
74
        else
75
            tmp = std::vector<Real>(moneynessGrid);
436✔
76

77
        Real shift = section.shift();
246✔
78

79
        if (section.volatilityType() == ShiftedLognormal && tmp[0] > QL_EPSILON) {
246✔
80
            m_.push_back(0.0);
218✔
81
            k_.push_back(-shift);
218✔
82
        }
83

84
        bool minStrikeAdded = false, maxStrikeAdded = false;
85
        for (Real& i : tmp) {
2,804✔
86
            Real k = section.volatilityType() == Normal ? Real(f_ + i) : Real(i * (f_ + shift) - shift);
2,558✔
87
            if ((section.volatilityType() == ShiftedLognormal && i <= QL_EPSILON) ||
5,088✔
88
                (k >= section.minStrike() && k <= section.maxStrike())) {
5,024✔
89
                if (!minStrikeAdded || !close(k, section.minStrike())) {
2,516✔
90
                    m_.push_back(i);
2,516✔
91
                    k_.push_back(k);
2,516✔
92
                }
93
                if (close(k, section.maxStrike()))
2,516✔
94
                    maxStrikeAdded = true;
95
            } else { // if the section provides a limited strike range
96
                     // we put the respective endpoint in our grid
97
                     // in order to not loose too much information
98
                if (k < section.minStrike() && !minStrikeAdded) {
42✔
99
                    m_.push_back(section.volatilityType() == Normal
20✔
100
                                     ? Real(section.minStrike() - f_)
40✔
101
                                     : Real((section.minStrike() + shift) / f_));
20✔
102
                    k_.push_back(section.minStrike());
20✔
103
                    minStrikeAdded = true;
104
                }
105
                if (k > section.maxStrike() && !maxStrikeAdded) {
42✔
106
                    m_.push_back(section.volatilityType() == Normal
6✔
107
                                     ? Real(section.maxStrike() - f_)
12✔
108
                                     : Real((section.maxStrike() + shift) / f_));
6✔
109
                    k_.push_back(section.maxStrike());
6✔
110
                    maxStrikeAdded = true;
111
                }
112
            }
113
        }
114

115
        // only known for shifted lognormal vols, otherwise we include
116
        // the lower strike in the loop below
117
        if(section.volatilityType() == ShiftedLognormal)
246✔
118
            c_.push_back(f_ + shift);
246✔
119

120
        for (Size i = (section.volatilityType() == Normal ? 0 : 1);
246✔
121
             i < k_.size(); i++) {
2,760✔
122
            c_.push_back(section.optionPrice(k_[i], Option::Call, 1.0));
2,514✔
123
        }
124

125
        Size centralIndex =
126
            std::upper_bound(m_.begin(), m_.end(),
246✔
127
                             (section.volatilityType() == Normal ? 0.0 : 1.0) -
492✔
128
                                 QL_EPSILON) -
129
            m_.begin();
246✔
130
        QL_REQUIRE(centralIndex < k_.size() - 1 && centralIndex > 1,
246✔
131
                   "Atm point in moneyness grid ("
132
                       << centralIndex << ") too close to boundary.");
133

134
        // shift central index to the right if necessary
135
        // (sometimes even the atm point lies in an arbitrageable area)
136

137
        while (centralIndex < k_.size() - 1 &&
246✔
138
               !af(centralIndex, centralIndex, centralIndex + 1))
246✔
139
            centralIndex++;
140

141
        QL_REQUIRE(centralIndex < k_.size(),
246✔
142
                   "central index is at right boundary");
143

144
        leftIndex_ = centralIndex;
246✔
145
        rightIndex_ = centralIndex;
246✔
146

147
        bool done = false;
148
        while (!done) {
493✔
149

150
            bool isAf = true;
151
            done = true;
152

153
            while (isAf && rightIndex_ < k_.size() - 1) {
1,374✔
154
                rightIndex_++;
1,129✔
155
                isAf = af(leftIndex_, rightIndex_, rightIndex_) &&
1,129✔
156
                       af(leftIndex_, rightIndex_ - 1, rightIndex_);
1,128✔
157
            }
158
            if (!isAf)
248✔
159
                rightIndex_--;
3✔
160

161
            isAf = true;
162
            while (isAf && leftIndex_ > 1) {
1,271✔
163
                leftIndex_--;
1,093✔
164
                isAf = af(leftIndex_, leftIndex_, rightIndex_) &&
1,093✔
165
                       af(leftIndex_, leftIndex_ + 1, rightIndex_);
1,024✔
166
            }
167
            if (!isAf)
248✔
168
                leftIndex_++;
70✔
169

170
            if (rightIndex_ < leftIndex_)
248✔
171
                rightIndex_ = leftIndex_;
×
172

173
            if (deleteArbitragePoints && leftIndex_ > 1) {
248✔
174
                m_.erase(m_.begin() + leftIndex_ - 1);
175
                k_.erase(k_.begin() + leftIndex_ - 1);
1✔
176
                c_.erase(c_.begin() + leftIndex_ - 1);
1✔
177
                leftIndex_--;
1✔
178
                if (rightIndex_ > 0)
1✔
179
                    rightIndex_--;
1✔
180
                done = false;
181
            }
182
            if (deleteArbitragePoints && rightIndex_ < k_.size() - 1) {
248✔
183
                m_.erase(m_.begin() + rightIndex_ + 1);
184
                k_.erase(k_.begin() + rightIndex_ + 1);
1✔
185
                c_.erase(c_.begin() + rightIndex_ + 1);
1✔
186
                if (rightIndex_ > 0)
1✔
187
                    rightIndex_--;
1✔
188
                done = false;
189
            }
190
        }
191

192
        QL_REQUIRE(rightIndex_ > leftIndex_,
246✔
193
                   "arbitrage free region must at least contain two "
194
                   "points (only index is "
195
                       << leftIndex_ << ")");
196

197
    }
246✔
198

UNCOV
199
    std::pair<Real, Real> SmileSectionUtils::arbitragefreeRegion() const {
×
UNCOV
200
        return {k_[leftIndex_], k_[rightIndex_]};
×
201
    }
202

203
    std::pair<Size, Size> SmileSectionUtils::arbitragefreeIndices() const {
150✔
204
        return {leftIndex_, rightIndex_};
150✔
205
    }
206

207
    bool SmileSectionUtils::af(const Size i0, const Size i,
4,620✔
208
                               const Size i1) const {
209
        if (i == 0)
4,620✔
210
            return true;
211
        Size im = i - 1 >= i0 ? i - 1 : 0;
4,620✔
212
        Real q1 = (c_[i] - c_[im]) / (k_[i] - k_[im]);
4,620✔
213
        if (q1 < -1.0 || q1 > 0.0)
4,620✔
214
            return false;
215
        if (i >= i1)
4,602✔
216
            return true;
217
        Real q2 = (c_[i + 1] - c_[i]) / (k_[i + 1] - k_[i]);
3,474✔
218
        return q1 <= q2 && q2 <= 0.0;
3,474✔
219
    }
220
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc