Coverage Report

Created: 2026-01-25 06:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/pricingengines/blackdeltacalculator.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2010 Dimitri Reiswich
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/pricingengines/blackdeltacalculator.hpp>
21
22
namespace QuantLib {
23
24
    QL_DEPRECATED_DISABLE_WARNING
25
26
    BlackDeltaCalculator::BlackDeltaCalculator(
27
                        Option::Type ot,
28
                        DeltaVolQuote::DeltaType dt,
29
                        Real spot,
30
                        DiscountFactor dDiscount,   // domestic discount
31
                        DiscountFactor fDiscount,   // foreign  discount
32
                        Real stdDev):
33
0
    dt_(dt), ot_(ot),
34
0
    dDiscount_(dDiscount), fDiscount_(fDiscount),
35
0
    stdDev_(stdDev), spot_(spot),
36
0
    forward_(spot*fDiscount/dDiscount), phi_(Integer(ot)) {
37
38
0
        QL_REQUIRE(spot_>0.0,
39
0
                   "positive spot value required: " <<
40
0
                   spot_ << " not allowed");
41
0
        QL_REQUIRE(dDiscount_>0.0,
42
0
                   "positive domestic discount factor required: " <<
43
0
                   dDiscount_ << " not allowed");
44
0
        QL_REQUIRE(fDiscount_>0.0,
45
0
                   "positive foreign discount factor required: " <<
46
0
                   fDiscount_ << " not allowed");
47
0
        QL_REQUIRE(stdDev_>=0.0,
48
0
                   "non-negative standard deviation required: "
49
0
                   << stdDev_ << " not allowed");
50
51
0
        fExpPos_    = forward_ * std::exp(0.5 * stdDev_ * stdDev_);
52
0
        fExpNeg_    = forward_ * std::exp(-0.5 * stdDev_ * stdDev_);
53
0
    }
54
55
56
0
    Real BlackDeltaCalculator::deltaFromStrike(Real strike) const {
57
58
0
        QL_REQUIRE(strike >=0.0,
59
0
                   "positive strike value required: " <<
60
0
                   strike << " not allowed");
61
62
0
        Real res = 0.0;
63
64
0
        switch(dt_){
65
0
          case DeltaVolQuote::Spot:
66
0
            res = phi_ * fDiscount_ * cumD1(strike);
67
0
            break;
68
69
0
          case DeltaVolQuote::Fwd:
70
0
            res = phi_ * cumD1(strike);
71
0
            break;
72
73
0
          case DeltaVolQuote::PaSpot:
74
0
            res = phi_ * fDiscount_ * cumD2(strike) * strike / forward_;
75
0
            break;
76
77
0
          case DeltaVolQuote::PaFwd:
78
0
            res = phi_ * cumD2(strike) * strike/  forward_;
79
0
            break;
80
81
0
          default:
82
0
            QL_FAIL("invalid delta type");
83
0
        }
84
0
        return res;
85
0
    }
86
87
0
    Real BlackDeltaCalculator::strikeFromDelta(Real delta) const {
88
0
        return(strikeFromDelta(delta, dt_));
89
0
    }
90
91
    Real BlackDeltaCalculator::strikeFromDelta(Real delta,
92
                                               DeltaVolQuote::DeltaType dt)
93
0
                                                                        const{
94
0
        Real res = 0.0;
95
0
        Real arg = 0.0;
96
0
        InverseCumulativeNormal invNorm;
97
98
0
        QL_REQUIRE(delta*phi_ >= 0.0, "Option type and delta are incoherent.");
99
100
0
        switch (dt) {
101
0
          case DeltaVolQuote::Spot:
102
0
            QL_REQUIRE(std::fabs(delta)<=fDiscount_,
103
0
                       "Spot delta out of range.");
104
105
0
            arg = -phi_ * invNorm(phi_ * delta / fDiscount_) * stdDev_ + 0.5 * stdDev_ * stdDev_;
106
0
            res = forward_ * std::exp(arg);
107
0
            break;
108
109
0
          case DeltaVolQuote::Fwd:
110
0
            QL_REQUIRE(std::fabs(delta)<=1.0,
111
0
                       "Forward delta out of range.");
112
113
0
            arg = -phi_ * invNorm(phi_ * delta) * stdDev_ + 0.5 * stdDev_ * stdDev_;
114
0
            res = forward_ * std::exp(arg);
115
0
            break;
116
117
0
          case DeltaVolQuote::PaSpot:
118
0
          case DeltaVolQuote::PaFwd: {
119
              // This has to be solved numerically. One of the
120
              // problems is that the premium adjusted call delta is
121
              // not monotonic in strike, such that two solutions
122
              // might occur. The one right to the max of the delta is
123
              // considered to be the correct strike.  Some proper
124
              // interval bounds for the strike need to be chosen, the
125
              // numerics can otherwise be very unreliable and
126
              // unstable.  I've chosen Brent over Newton, since the
127
              // interval can be specified explicitly and we can not
128
              // run into the area on the left of the maximum.  The
129
              // put delta doesn't have this property and can be
130
              // solved without any problems, but also numerically.
131
0
              auto f = [&](Real strike) {
132
0
                return deltaFromStrike(strike) - delta;
133
0
              };
134
135
0
              Brent solver;
136
0
              solver.setMaxEvaluations(1000);
137
0
              Real accuracy = 1.0e-10;
138
139
0
              Real rightLimit = 0.0;
140
0
              Real leftLimit = 0.0;
141
142
              // Strike of not premium adjusted is always to the right of premium adjusted
143
0
              if (dt == DeltaVolQuote::PaSpot) {
144
0
                  rightLimit = strikeFromDelta(delta, DeltaVolQuote::Spot);
145
0
              } else {
146
0
                  rightLimit = strikeFromDelta(delta, DeltaVolQuote::Fwd);
147
0
              }
148
149
0
              if (phi_ < 0) { // if put
150
0
                  res = solver.solve(f, accuracy, rightLimit, 0.0, spot_*100.0);
151
0
                  break;
152
0
              } else {
153
154
                  // find out the left limit which is the strike
155
                  // corresponding to the value where premium adjusted
156
                  // deltas have their maximum
157
0
                  auto g = [&](Real strike) {
158
0
                    return cumD2(strike) * stdDev_ - nD2(strike);
159
0
                  };
160
161
0
                  leftLimit = solver.solve(g, accuracy, rightLimit * 0.5,
162
0
                                         0.0, rightLimit);
163
164
0
                  Real guess = leftLimit+(rightLimit - leftLimit) * 0.5;
165
166
0
                  res = solver.solve(f, accuracy, guess, leftLimit, rightLimit);
167
0
              } // end if phi<0 else
168
169
0
              break;
170
0
          }
171
172
0
          default:
173
0
            QL_FAIL("invalid delta type");
174
0
        }
175
176
0
        return res;
177
0
    }
178
179
0
    Real BlackDeltaCalculator::atmStrike(DeltaVolQuote::AtmType atmT) const {
180
181
0
        Real res = 0.0;
182
183
0
        switch(atmT) {
184
0
          case DeltaVolQuote::AtmSpot:
185
0
            res = spot_;
186
0
            break;
187
188
0
          case DeltaVolQuote::AtmDeltaNeutral:
189
0
            if(dt_ == DeltaVolQuote::Spot || dt_ == DeltaVolQuote::Fwd){
190
0
                res = fExpPos_;
191
0
            } else {
192
0
                res = fExpNeg_;
193
0
            }
194
0
            break;
195
196
0
          case DeltaVolQuote::AtmFwd:
197
0
            res = forward_;
198
0
            break;
199
200
0
          case DeltaVolQuote::AtmGammaMax: case DeltaVolQuote::AtmVegaMax:
201
0
            res = fExpPos_;
202
0
            break;
203
204
0
          case DeltaVolQuote::AtmPutCall50:
205
0
            QL_REQUIRE(dt_ == DeltaVolQuote::Fwd,
206
0
                       "|PutDelta|=CallDelta=0.50 only possible for forward delta.");
207
0
            res = fExpPos_;
208
0
            break;
209
210
0
          default:
211
0
            QL_FAIL("invalid atm type");
212
0
        }
213
214
0
        return res;
215
0
    }
216
217
218
0
    Real BlackDeltaCalculator::cumD1(Real strike) const {
219
220
0
        Real d1_=0.0;
221
0
        Real cum_d1_pos_ = 1.0; // N(d1)
222
0
        Real cum_d1_neg_ = 0.0; // N(-d1)
223
224
0
        CumulativeNormalDistribution f;
225
226
0
        if (stdDev_>=QL_EPSILON) {
227
0
            if(strike>0) {
228
0
                d1_ = std::log(forward_/strike)/stdDev_ + 0.5*stdDev_;
229
0
                return f(phi_*d1_);
230
0
            }
231
0
        } else {
232
0
            if (forward_<strike) {
233
0
                cum_d1_pos_ = 0.0;
234
0
                cum_d1_neg_ = 1.0;
235
0
            } else if(forward_==strike){
236
0
                d1_ = 0.5*stdDev_;
237
0
                return f(phi_*d1_);
238
0
            }
239
0
        }
240
241
0
        if (phi_>0) { // if Call
242
0
            return cum_d1_pos_;
243
0
        } else {
244
0
            return cum_d1_neg_;
245
0
        }
246
0
    }
247
248
249
0
    Real BlackDeltaCalculator::nD1(Real strike) const {
250
251
0
        Real d1_=0.0;
252
0
        Real n_d1_ = 0.0; // n(d1)
253
254
0
        if (stdDev_>=QL_EPSILON){
255
0
            if(strike>0){
256
0
                d1_ = std::log(forward_/strike)/stdDev_ + 0.5*stdDev_;
257
0
                CumulativeNormalDistribution f;
258
0
                n_d1_ = f.derivative(d1_);
259
0
            }
260
0
        }
261
262
0
        return n_d1_;
263
0
    }
264
265
266
0
    Real BlackDeltaCalculator::cumD2(Real strike) const {
267
268
0
        Real d2_=0.0;
269
0
        Real cum_d2_pos_= 1.0;  // N(d2)
270
0
        Real cum_d2_neg_= 0.0;  // N(-d2)
271
272
0
        CumulativeNormalDistribution f;
273
274
0
        if (stdDev_>=QL_EPSILON){
275
276
0
            if(strike>0){
277
0
                d2_ = std::log(forward_/strike)/stdDev_ - 0.5*stdDev_;
278
0
                return f(phi_*d2_);
279
0
            }
280
281
0
        } else {
282
283
0
            if (forward_<strike) {
284
0
                cum_d2_pos_= 0.0;
285
0
                cum_d2_neg_= 1.0;
286
0
            } else if (forward_==strike) {
287
0
                d2_ = -0.5*stdDev_;
288
0
                return(f(phi_*d2_));
289
0
            }
290
291
0
        }
292
293
0
        if (phi_>0) { // if Call
294
0
            return cum_d2_pos_;
295
0
        } else {
296
0
            return cum_d2_neg_;
297
0
        }
298
0
    }
299
300
301
0
    Real BlackDeltaCalculator::nD2(Real strike) const {
302
303
0
        Real d2_=0.0;
304
0
        Real n_d2_= 0.0; // n(d2)
305
306
0
        if (stdDev_>=QL_EPSILON){
307
0
            if(strike>0){
308
0
                d2_ = std::log(forward_/strike)/stdDev_ - 0.5*stdDev_;
309
0
                CumulativeNormalDistribution f;
310
0
                n_d2_ = f.derivative(d2_);
311
0
            }
312
0
        }
313
314
0
        return n_d2_;
315
0
    }
316
317
318
0
    void BlackDeltaCalculator::setDeltaType(DeltaVolQuote::DeltaType dt){
319
0
        dt_=dt;
320
0
    }
321
322
0
    void BlackDeltaCalculator::setOptionType(Option::Type ot){
323
0
        ot_=ot;
324
0
        phi_=Integer(ot_);
325
0
    }
326
327
328
    // helper classes
329
330
    BlackDeltaPremiumAdjustedSolverClass::BlackDeltaPremiumAdjustedSolverClass(
331
                        Option::Type ot,
332
                        DeltaVolQuote::DeltaType dt,
333
                        Real spot,
334
                        DiscountFactor dDiscount,   // domestic discount
335
                        DiscountFactor fDiscount,   // foreign  discount
336
                        Real stdDev,
337
                        Real delta):
338
0
    bdc_(ot,dt,spot,dDiscount,fDiscount,stdDev), delta_(delta) {}
339
340
341
0
    Real BlackDeltaPremiumAdjustedSolverClass::operator()(Real strike) const {
342
0
        return bdc_.deltaFromStrike(strike)-delta_;
343
0
    }
344
345
346
    BlackDeltaPremiumAdjustedMaxStrikeClass::BlackDeltaPremiumAdjustedMaxStrikeClass(
347
                        Option::Type ot,
348
                        DeltaVolQuote::DeltaType dt,
349
                        Real spot,
350
                        DiscountFactor dDiscount,   // domestic discount
351
                        DiscountFactor fDiscount,   // foreign  discount
352
                        Real stdDev):
353
0
    bdc_(ot,dt,spot,dDiscount,fDiscount,stdDev), stdDev_(stdDev) {}
354
355
0
    Real BlackDeltaPremiumAdjustedMaxStrikeClass::operator()(Real strike) const {
356
0
        return bdc_.cumD2(strike)*stdDev_ - bdc_.nD2(strike);
357
0
    }
358
359
    QL_DEPRECATED_ENABLE_WARNING
360
}