Coverage Report

Created: 2025-08-11 06:28

/src/quantlib/ql/termstructures/volatility/smilesectionutils.cpp
Line
Count
Source (jump to first uncovered line)
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
 <http://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,
27
                                         const std::vector<Real> &moneynessGrid,
28
                                         const Real atm,
29
0
                                         const bool deleteArbitragePoints) {
30
31
0
        if (!moneynessGrid.empty()) {
32
0
            QL_REQUIRE(
33
0
                section.volatilityType() == Normal || moneynessGrid[0] >= 0.0,
34
0
                "moneyness grid should only contain non negative values ("
35
0
                    << moneynessGrid[0] << ")");
36
0
            for (Size i = 0; i < moneynessGrid.size() - 1; i++) {
37
0
                QL_REQUIRE(moneynessGrid[i] < moneynessGrid[i + 1],
38
0
                           "moneyness grid should contain strictly increasing "
39
0
                           "values ("
40
0
                               << moneynessGrid[i] << ","
41
0
                               << moneynessGrid[i + 1] << " at indices " << i
42
0
                               << ", " << i + 1 << ")");
43
0
            }
44
0
        }
45
46
0
        if (atm == Null<Real>()) {
47
0
            f_ = section.atmLevel();
48
0
            QL_REQUIRE(f_ != Null<Real>(),
49
0
                       "atm level must be provided by source section or given "
50
0
                       "in the constructor");
51
0
        } else {
52
0
            f_ = atm;
53
0
        }
54
55
0
        std::vector<Real> tmp;
56
57
0
        static const Real defaultMoney[] = { 0.0,  0.01, 0.05, 0.10, 0.25, 0.40,
58
0
                                             0.50, 0.60, 0.70, 0.80, 0.90, 1.0,
59
0
                                             1.25, 1.5,  1.75, 2.0,  5.0,  7.5,
60
0
                                             10.0, 15.0, 20.0 };
61
0
        static const Real defaultMoneyNormal[] = {
62
0
            -0.20,  -0.15,  -0.10,  -0.075,  -0.05,   -0.04,   -0.03,
63
0
            -0.02,  -0.015, -0.01,  -0.0075, -0.0050, -0.0025, 0.0,
64
0
            0.0025, 0.0050, 0.0075, 0.01,    0.015,   0.02,    0.03,
65
0
            0.04,   0.05,   0.075,  0.10,    0.15,    0.20
66
0
        };
67
68
0
        if (moneynessGrid.empty()) {
69
0
            tmp = section.volatilityType() == Normal
70
0
                      ? std::vector<Real>(defaultMoneyNormal,
71
0
                                          defaultMoneyNormal + 27)
72
0
                      : std::vector<Real>(defaultMoney, defaultMoney + 21);
73
0
        }
74
0
        else
75
0
            tmp = std::vector<Real>(moneynessGrid);
76
77
0
        Real shift = section.shift();
78
79
0
        if (section.volatilityType() == ShiftedLognormal && tmp[0] > QL_EPSILON) {
80
0
            m_.push_back(0.0);
81
0
            k_.push_back(-shift);
82
0
        }
83
84
0
        bool minStrikeAdded = false, maxStrikeAdded = false;
85
0
        for (Real& i : tmp) {
86
0
            Real k = section.volatilityType() == Normal ? Real(f_ + i) : Real(i * (f_ + shift) - shift);
87
0
            if ((section.volatilityType() == ShiftedLognormal && i <= QL_EPSILON) ||
88
0
                (k >= section.minStrike() && k <= section.maxStrike())) {
89
0
                if (!minStrikeAdded || !close(k, section.minStrike())) {
90
0
                    m_.push_back(i);
91
0
                    k_.push_back(k);
92
0
                }
93
0
                if (close(k, section.maxStrike()))
94
0
                    maxStrikeAdded = true;
95
0
            } 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
0
                if (k < section.minStrike() && !minStrikeAdded) {
99
0
                    m_.push_back(section.volatilityType() == Normal
100
0
                                     ? Real(section.minStrike() - f_)
101
0
                                     : Real((section.minStrike() + shift) / f_));
102
0
                    k_.push_back(section.minStrike());
103
0
                    minStrikeAdded = true;
104
0
                }
105
0
                if (k > section.maxStrike() && !maxStrikeAdded) {
106
0
                    m_.push_back(section.volatilityType() == Normal
107
0
                                     ? Real(section.maxStrike() - f_)
108
0
                                     : Real((section.maxStrike() + shift) / f_));
109
0
                    k_.push_back(section.maxStrike());
110
0
                    maxStrikeAdded = true;
111
0
                }
112
0
            }
113
0
        }
114
115
        // only known for shifted lognormal vols, otherwise we include
116
        // the lower strike in the loop below
117
0
        if(section.volatilityType() == ShiftedLognormal)
118
0
            c_.push_back(f_ + shift);
119
120
0
        for (Size i = (section.volatilityType() == Normal ? 0 : 1);
121
0
             i < k_.size(); i++) {
122
0
            c_.push_back(section.optionPrice(k_[i], Option::Call, 1.0));
123
0
        }
124
125
0
        Size centralIndex =
126
0
            std::upper_bound(m_.begin(), m_.end(),
127
0
                             (section.volatilityType() == Normal ? 0.0 : 1.0) -
128
0
                                 QL_EPSILON) -
129
0
            m_.begin();
130
0
        QL_REQUIRE(centralIndex < k_.size() - 1 && centralIndex > 1,
131
0
                   "Atm point in moneyness grid ("
132
0
                       << 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
0
        while (!af(centralIndex, centralIndex, centralIndex + 1) &&
138
0
               centralIndex < k_.size() - 1)
139
0
            centralIndex++;
140
141
0
        QL_REQUIRE(centralIndex < k_.size(),
142
0
                   "central index is at right boundary");
143
144
0
        leftIndex_ = centralIndex;
145
0
        rightIndex_ = centralIndex;
146
147
0
        bool done = false;
148
0
        while (!done) {
149
150
0
            bool isAf = true;
151
0
            done = true;
152
153
0
            while (isAf && rightIndex_ < k_.size() - 1) {
154
0
                rightIndex_++;
155
0
                isAf = af(leftIndex_, rightIndex_, rightIndex_) &&
156
0
                       af(leftIndex_, rightIndex_ - 1, rightIndex_);
157
0
            }
158
0
            if (!isAf)
159
0
                rightIndex_--;
160
161
0
            isAf = true;
162
0
            while (isAf && leftIndex_ > 1) {
163
0
                leftIndex_--;
164
0
                isAf = af(leftIndex_, leftIndex_, rightIndex_) &&
165
0
                       af(leftIndex_, leftIndex_ + 1, rightIndex_);
166
0
            }
167
0
            if (!isAf)
168
0
                leftIndex_++;
169
170
0
            if (rightIndex_ < leftIndex_)
171
0
                rightIndex_ = leftIndex_;
172
173
0
            if (deleteArbitragePoints && leftIndex_ > 1) {
174
0
                m_.erase(m_.begin() + leftIndex_ - 1);
175
0
                k_.erase(k_.begin() + leftIndex_ - 1);
176
0
                c_.erase(c_.begin() + leftIndex_ - 1);
177
0
                leftIndex_--;
178
0
                rightIndex_--;
179
0
                done = false;
180
0
            }
181
0
            if (deleteArbitragePoints && rightIndex_ < k_.size() - 1) {
182
0
                m_.erase(m_.begin() + rightIndex_ + 1);
183
0
                k_.erase(k_.begin() + rightIndex_ + 1);
184
0
                c_.erase(c_.begin() + rightIndex_ + 1);
185
0
                rightIndex_--;
186
0
                done = false;
187
0
            }
188
0
        }
189
190
0
        QL_REQUIRE(rightIndex_ > leftIndex_,
191
0
                   "arbitrage free region must at least contain two "
192
0
                   "points (only index is "
193
0
                       << leftIndex_ << ")");
194
195
0
    }
196
197
0
    std::pair<Real, Real> SmileSectionUtils::arbitragefreeRegion() const {
198
0
        return {k_[leftIndex_], k_[rightIndex_]};
199
0
    }
200
201
0
    std::pair<Size, Size> SmileSectionUtils::arbitragefreeIndices() const {
202
0
        return {leftIndex_, rightIndex_};
203
0
    }
204
205
    bool SmileSectionUtils::af(const Size i0, const Size i,
206
0
                               const Size i1) const {
207
0
        if (i == 0)
208
0
            return true;
209
0
        Size im = i - 1 >= i0 ? i - 1 : 0;
210
0
        Real q1 = (c_[i] - c_[im]) / (k_[i] - k_[im]);
211
0
        if (q1 < -1.0 || q1 > 0.0)
212
0
            return false;
213
0
        if (i >= i1)
214
0
            return true;
215
0
        Real q2 = (c_[i + 1] - c_[i]) / (k_[i + 1] - k_[i]);
216
0
        return q1 <= q2 && q2 <= 0.0;
217
0
    }
218
}