Coverage Report

Created: 2026-10-31 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/math/distributions/bivariatenormaldistribution.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2002, 2003 Ferdinando Ametrano
5
 Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl
6
 Copyright (C) 2003 StatPro Italia srl
7
 Copyright (C) 2005 Gary Kennedy
8
9
 This file is part of QuantLib, a free-software/open-source library
10
 for financial quantitative analysts and developers - http://quantlib.org/
11
12
 QuantLib is free software: you can redistribute it and/or modify it
13
 under the terms of the QuantLib license.  You should have received a
14
 copy of the license along with this program; if not, please email
15
 <quantlib-dev@lists.sf.net>. The license is also available online at
16
 <https://www.quantlib.org/license.shtml>.
17
18
 This program is distributed in the hope that it will be useful, but WITHOUT
19
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
20
 FOR A PARTICULAR PURPOSE.  See the license for more details.
21
*/
22
23
#include <ql/math/distributions/bivariatenormaldistribution.hpp>
24
#include <ql/math/integrals/gaussianquadratures.hpp>
25
#include <algorithm>
26
#include <cmath>
27
28
namespace QuantLib {
29
30
    // Drezner 1978
31
32
    const Real BivariateCumulativeNormalDistributionDr78::x_[] = {
33
        0.24840615,
34
        0.39233107,
35
        0.21141819,
36
        0.03324666,
37
        0.00082485334
38
    };
39
40
    const Real BivariateCumulativeNormalDistributionDr78::y_[] = {
41
        0.10024215,
42
        0.48281397,
43
        1.06094980,
44
        1.77972940,
45
        2.66976040000
46
    };
47
48
    BivariateCumulativeNormalDistributionDr78::
49
    BivariateCumulativeNormalDistributionDr78(Real rho)
50
0
    : rho_(rho), rho2_(rho*rho) {
51
52
0
        QL_REQUIRE(rho>=-1.0,
53
0
                   "rho must be >= -1.0 (" << rho << " not allowed)");
54
0
        QL_REQUIRE(rho<=1.0,
55
0
                   "rho must be <= 1.0 (" << rho << " not allowed)");
56
0
    }
57
58
    Real BivariateCumulativeNormalDistributionDr78::operator()(Real a,
59
0
                                                               Real b) const {
60
61
0
        CumulativeNormalDistribution cumNormalDist;
62
0
        Real CumNormDistA = cumNormalDist(a);
63
0
        Real CumNormDistB = cumNormalDist(b);
64
0
        Real MaxCumNormDistAB = std::max(CumNormDistA, CumNormDistB);
65
0
        Real MinCumNormDistAB = std::min(CumNormDistA, CumNormDistB);
66
67
0
        if (rho_ == 1.0)
68
0
            return MinCumNormDistAB;
69
70
0
        if (rho_ == -1.0)
71
0
            return std::max<Real>(0.0, CumNormDistA + CumNormDistB - 1.0);
72
73
0
        if (1.0-MaxCumNormDistAB<1e-15)
74
0
            return MinCumNormDistAB;
75
76
0
        if (MinCumNormDistAB<1e-15)
77
0
            return MinCumNormDistAB;
78
79
0
        Real a1 = a / std::sqrt(2.0 * (1.0 - rho2_));
80
0
        Real b1 = b / std::sqrt(2.0 * (1.0 - rho2_));
81
82
0
        Real result=-1.0;
83
84
0
        if (a<=0.0 && b<=0 && rho_<=0) {
85
0
            Real sum=0.0;
86
0
            for (Size i=0; i<5; i++) {
87
0
                for (Size j=0;j<5; j++) {
88
0
                    sum += x_[i]*x_[j]*
89
0
                        std::exp(a1*(2.0*y_[i]-a1)+b1*(2.0*y_[j]-b1)
90
0
                                 +2.0*rho_*(y_[i]-a1)*(y_[j]-b1));
91
0
                }
92
0
            }
93
0
            result = std::sqrt(1.0 - rho2_)/M_PI*sum;
94
0
        } else if (a<=0 && b>=0 && rho_>=0) {
95
0
            BivariateCumulativeNormalDistributionDr78 bivCumNormalDist(-rho_);
96
0
            result= CumNormDistA - bivCumNormalDist(a, -b);
97
0
        } else if (a>=0.0 && b<=0.0 && rho_>=0.0) {
98
0
            BivariateCumulativeNormalDistributionDr78 bivCumNormalDist(-rho_);
99
0
            result= CumNormDistB - bivCumNormalDist(-a, b);
100
0
        } else if (a>=0.0 && b>=0.0 && rho_<=0.0) {
101
0
            result= CumNormDistA + CumNormDistB -1.0 + (*this)(-a, -b);
102
0
        } else if (a*b*rho_>0.0) {
103
            // D^2 = (rho*a - b)^2 + a^2*(1 - rho^2), and likewise for b.
104
            // The hypot forms avoid cancellation near |rho| = 1.
105
0
            const Real sqrtOneMinusRho2 = std::sqrt((1.0 - rho_) * (1.0 + rho_));
106
0
            const Real maxCorrelation = std::nextafter(Real(1.0), Real(2.0));
107
0
            Real rho1 = (rho_*a-b)*(a>0.0 ? 1.0: -1.0)/
108
0
                std::hypot(rho_*a-b, a*sqrtOneMinusRho2);
109
            // Clamp only finite one-ulp overshoots of the correlation range.
110
0
            if (std::fabs(rho1) == maxCorrelation)
111
0
                rho1 = std::clamp(rho1, Real(-1.0), Real(1.0));
112
0
            BivariateCumulativeNormalDistributionDr78 bivCumNormalDist(rho1);
113
114
0
            Real rho2 = (rho_*b-a)*(b>0.0 ? 1.0: -1.0)/
115
0
                std::hypot(rho_*b-a, b*sqrtOneMinusRho2);
116
0
            if (std::fabs(rho2) == maxCorrelation)
117
0
                rho2 = std::clamp(rho2, Real(-1.0), Real(1.0));
118
0
            BivariateCumulativeNormalDistributionDr78 CBND2(rho2);
119
120
0
            Real delta = (1.0-(a>0.0 ? 1.0: -1.0)*(b>0.0 ? 1.0: -1.0))/4.0;
121
122
0
            result= bivCumNormalDist(a, 0.0) + CBND2(b, 0.0) - delta;
123
0
        } else {
124
0
            QL_FAIL("case not handled");
125
0
        }
126
127
0
        return result;
128
0
    }
129
130
131
    // West 2004
132
133
    namespace {
134
135
        class eqn3 { /* Relates to eqn3 Genz 2004 */
136
          public:
137
            eqn3(Real h, Real k, Real asr)
138
430
            : hk_(h * k), asr_(asr), hs_((h * h + k * k) / 2) {}
139
140
5.24k
            Real operator()(Real x) const {
141
5.24k
                Real sn = std::sin(asr_ * (-x + 1) * 0.5);
142
5.24k
                return std::exp((sn * hk_ - hs_) / (1.0 - sn * sn));
143
5.24k
            }
144
          private:
145
            Real hk_, asr_, hs_;
146
        };
147
148
        class eqn6 { /* Relates to eqn6 Genz 2004 */
149
          public:
150
            eqn6(Real a, Real c, Real d, Real bs, Real hk)
151
870
            : a_(a), c_(c), d_(d), bs_(bs), hk_(hk) {}
152
17.4k
            Real operator()(Real x) const {
153
17.4k
                Real xs = a_ * (-x + 1);
154
17.4k
                xs = std::fabs(xs*xs);
155
17.4k
                Real rs = std::sqrt(1 - xs);
156
17.4k
                Real asr = -(bs_ / xs + hk_) / 2;
157
17.4k
                if (asr > -100.0) {
158
6.38k
                    return (a_ * std::exp(asr) *
159
6.38k
                            (std::exp(-hk_ * (1 - rs) / (2 * (1 + rs))) / rs -
160
6.38k
                             (1 + c_ * xs * (1 + d_ * xs))));
161
11.0k
                } else {
162
11.0k
                    return 0.0;
163
11.0k
                }
164
17.4k
            }
165
          private:
166
            Real a_, c_, d_, bs_, hk_;
167
        };
168
169
    }
170
171
    BivariateCumulativeNormalDistributionWe04DP::
172
    BivariateCumulativeNormalDistributionWe04DP(Real rho)
173
1.30k
    : correlation_(rho) {
174
175
1.30k
        QL_REQUIRE(rho>=-1.0,
176
1.30k
                   "rho must be >= -1.0 (" << rho << " not allowed)");
177
1.30k
        QL_REQUIRE(rho<=1.0,
178
1.30k
                   "rho must be <= 1.0 (" << rho << " not allowed)");
179
1.30k
    }
180
181
182
    Real BivariateCumulativeNormalDistributionWe04DP::operator()(
183
1.30k
                                                       Real x, Real y) const {
184
185
        /* The implementation is described at section 2.4 "Hybrid
186
           Numerical Integration Algorithms" of "Numerical Computation
187
           of Rectangular Bivariate an Trivariate Normal and t
188
           Probabilities", Genz (2004), Statistics and Computing 14,
189
           151-160. (available at
190
           www.sci.wsu.edu/math/faculty/henz/homepage)
191
192
           The Gauss-Legendre quadrature have been extracted to
193
           TabulatedGaussLegendre (x,w zero-based)
194
195
           Tthe functions ot be integrated numerically have been moved
196
           to classes eqn3 and eqn6
197
198
           Change some magic numbers to M_PI */
199
200
1.30k
        TabulatedGaussLegendre gaussLegendreQuad(20);
201
1.30k
        if (std::fabs(correlation_) < 0.3) {
202
129
            gaussLegendreQuad.order(6);
203
1.17k
        } else if (std::fabs(correlation_) < 0.75) {
204
197
            gaussLegendreQuad.order(12);
205
197
        }
206
207
1.30k
        Real h = -x;
208
1.30k
        Real k = -y;
209
1.30k
        Real hk = h * k;
210
1.30k
        Real BVN = 0.0;
211
212
1.30k
        if (std::fabs(correlation_) < 0.925)
213
432
        {
214
432
            if (std::fabs(correlation_) > 0)
215
430
            {
216
430
                Real asr = std::asin(correlation_);
217
430
                eqn3 f(h,k,asr);
218
430
                BVN = gaussLegendreQuad(f);
219
430
                BVN *= asr * (0.25 / M_PI);
220
430
            }
221
432
            BVN += cumnorm_(-h) * cumnorm_(-k);
222
432
        }
223
870
        else
224
870
        {
225
870
            if (correlation_ < 0)
226
797
            {
227
797
                k *= -1;
228
797
                hk *= -1;
229
797
            }
230
870
            if (std::fabs(correlation_) < 1)
231
870
            {
232
870
                Real Ass = (1 - correlation_) * (1 + correlation_);
233
870
                Real a = std::sqrt(Ass);
234
870
                Real bs = (h-k)*(h-k);
235
870
                Real c = (4 - hk) / 8;
236
870
                Real d = (12 - hk) / 16;
237
870
                Real asr = -(bs / Ass + hk) / 2;
238
870
                if (asr > -100)
239
507
                {
240
507
                    BVN = a * std::exp(asr) *
241
507
                        (1 - c * (bs - Ass) * (1 - d * bs / 5) / 3 +
242
507
                         c * d * Ass * Ass / 5);
243
507
                }
244
870
                if (-hk < 100)
245
805
                {
246
805
                    Real B = std::sqrt(bs);
247
805
                    BVN -= std::exp(-hk / 2) * 2.506628274631 *
248
805
                        cumnorm_(-B / a) * B *
249
805
                        (1 - c * bs * (1 - d * bs / 5) / 3);
250
805
                }
251
870
                a /= 2;
252
870
                eqn6 f(a,c,d,bs,hk);
253
870
                BVN += gaussLegendreQuad(f);
254
870
                BVN /= (-2.0 * M_PI);
255
870
            }
256
257
870
            if (correlation_ > 0) {
258
73
                BVN += cumnorm_(-std::max(h, k));
259
797
            } else {
260
797
                BVN *= -1;
261
797
                if (k > h) {
262
                    // evaluate cumnorm where it is most precise, that
263
                    // is in the lower tail because of double accuracy
264
                    // around 0.0 vs around 1.0
265
274
                    if (h >= 0) {
266
77
                        BVN += cumnorm_(-h) - cumnorm_(-k);
267
197
                    } else {
268
197
                        BVN += cumnorm_(k) - cumnorm_(h);
269
197
                    }
270
274
                }
271
797
            }
272
870
        }
273
1.30k
        return BVN;
274
1.30k
    }
275
276
}