Coverage Report

Created: 2026-08-14 07:10

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/pricingengines/basket/stulzengine.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2004 Ferdinando Ametrano
5
 Copyright (C) 2004 Neil Firth
6
 Copyright (C) 2007 StatPro Italia srl
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/exercise.hpp>
23
#include <ql/math/distributions/bivariatenormaldistribution.hpp>
24
#include <ql/math/distributions/normaldistribution.hpp>
25
#include <ql/pricingengines/basket/stulzengine.hpp>
26
#include <ql/pricingengines/blackcalculator.hpp>
27
#include <ql/pricingengines/blackformula.hpp>
28
#include <utility>
29
30
namespace QuantLib {
31
32
    namespace {
33
34
        // calculate the value of euro min basket call
35
        Real euroTwoAssetMinBasketCall(Real forward1, Real forward2,
36
                                       Real strike,
37
                                       DiscountFactor riskFreeDiscount,
38
                                       Real variance1, Real variance2,
39
634
                                       Real rho) {
40
41
634
            Real stdDev1 = std::sqrt(variance1);
42
634
            Real stdDev2 = std::sqrt(variance2);
43
44
634
            Real variance = variance1 + variance2 - 2*rho*stdDev1*stdDev2;
45
634
            Real stdDev = std::sqrt(variance);
46
47
634
            Real modRho1 = (rho * stdDev2 - stdDev1) / stdDev;
48
634
            Real modRho2 = (rho * stdDev1 - stdDev2) / stdDev;
49
50
634
            Real D1 = (std::log(forward1/forward2) + 0.5*variance) / stdDev;
51
52
634
            Real alfa, beta, gamma;
53
634
            if (strike != 0.0) {
54
385
                auto bivCNorm =
55
385
                    BivariateCumulativeNormalDistribution(rho);
56
385
                auto bivCNormMod2 =
57
385
                    BivariateCumulativeNormalDistribution(modRho2);
58
385
                auto bivCNormMod1 =
59
385
                    BivariateCumulativeNormalDistribution(modRho1);
60
61
385
                Real D1_1 =
62
385
                    (std::log(forward1/strike) + 0.5*variance1) / stdDev1;
63
385
                Real D1_2 =
64
385
                    (std::log(forward2/strike) + 0.5*variance2) / stdDev2;
65
385
                alfa = bivCNormMod1(D1_1, -D1);
66
385
                beta = bivCNormMod2(D1_2, D1 - stdDev);
67
385
                gamma = bivCNorm(D1_1 - stdDev1, D1_2 - stdDev2);
68
385
            } else {
69
249
                CumulativeNormalDistribution cum;
70
249
                alfa = cum(-D1);
71
249
                beta = cum(D1 - stdDev);
72
249
                gamma = 1.0;
73
249
            }
74
75
634
            return riskFreeDiscount *
76
634
                (forward1*alfa + forward2*beta - strike*gamma);
77
78
634
        }
79
80
        // calculate the value of euro max basket call
81
        Real euroTwoAssetMaxBasketCall(Real forward1, Real forward2,
82
                                       Real strike,
83
                                       DiscountFactor riskFreeDiscount,
84
                                       Real variance1, Real variance2,
85
506
                                       Real rho) {
86
87
506
            auto payoff = ext::make_shared<PlainVanillaPayoff>(Option::Call, strike);
88
89
506
            Real black1 = blackFormula(payoff->optionType(), payoff->strike(),
90
506
                forward1, std::sqrt(variance1)) * riskFreeDiscount;
91
92
506
            Real black2 = blackFormula(payoff->optionType(), payoff->strike(),
93
506
                forward2, std::sqrt(variance2)) * riskFreeDiscount;
94
95
506
            return black1 + black2 -
96
506
                euroTwoAssetMinBasketCall(forward1, forward2, strike,
97
506
                                          riskFreeDiscount,
98
506
                                          variance1, variance2, rho);
99
506
        }
100
    }
101
102
    StulzEngine::StulzEngine(ext::shared_ptr<GeneralizedBlackScholesProcess> process1,
103
                             ext::shared_ptr<GeneralizedBlackScholesProcess> process2,
104
                             Real correlation)
105
408
    : process1_(std::move(process1)), process2_(std::move(process2)), rho_(correlation) {
106
408
        registerWith(process1_);
107
408
        registerWith(process2_);
108
408
    }
109
110
408
    void StulzEngine::calculate() const {
111
112
408
        QL_REQUIRE(arguments_.exercise->type() == Exercise::European,
113
408
                   "not an European Option");
114
115
408
        ext::shared_ptr<EuropeanExercise> exercise =
116
408
            ext::dynamic_pointer_cast<EuropeanExercise>(arguments_.exercise);
117
408
        QL_REQUIRE(exercise, "not an European Option");
118
119
408
        ext::shared_ptr<BasketPayoff> basket_payoff =
120
408
            ext::dynamic_pointer_cast<BasketPayoff>(arguments_.payoff);
121
122
408
        ext::shared_ptr<MinBasketPayoff> min_basket =
123
408
            ext::dynamic_pointer_cast<MinBasketPayoff>(arguments_.payoff);
124
125
408
        ext::shared_ptr<MaxBasketPayoff> max_basket =
126
408
            ext::dynamic_pointer_cast<MaxBasketPayoff>(arguments_.payoff);
127
408
        QL_REQUIRE(min_basket || max_basket, "unknown basket type");
128
129
385
        ext::shared_ptr<PlainVanillaPayoff> payoff =
130
385
            ext::dynamic_pointer_cast<PlainVanillaPayoff>(basket_payoff->basePayoff());
131
385
        QL_REQUIRE(payoff, "non-plain payoff given");
132
133
385
        Real strike = payoff->strike();
134
135
385
        Real variance1 = process1_->blackVolatility()->blackVariance(
136
385
                                                exercise->lastDate(), strike);
137
385
        Real variance2 = process2_->blackVolatility()->blackVariance(
138
385
                                                exercise->lastDate(), strike);
139
140
385
        DiscountFactor riskFreeDiscount =
141
385
            process1_->riskFreeRate()->discount(exercise->lastDate());
142
143
        // cannot handle non zero dividends, so don't believe this...
144
385
        DiscountFactor dividendDiscount1 =
145
385
            process1_->dividendYield()->discount(exercise->lastDate());
146
385
        DiscountFactor dividendDiscount2 =
147
385
            process2_->dividendYield()->discount(exercise->lastDate());
148
149
385
        Real forward1 = process1_->stateVariable()->value() *
150
385
            dividendDiscount1 / riskFreeDiscount;
151
385
        Real forward2 = process2_->stateVariable()->value() *
152
385
            dividendDiscount2 / riskFreeDiscount;
153
154
385
        if (max_basket != nullptr) {
155
307
            switch (payoff->optionType()) {
156
              // euro call on a two asset max basket
157
108
              case Option::Call:
158
108
                results_.value =
159
108
                    euroTwoAssetMaxBasketCall(forward1, forward2, strike,
160
108
                                              riskFreeDiscount,
161
108
                                              variance1, variance2,
162
108
                                              rho_);
163
164
108
                break;
165
              // euro put on a two asset max basket
166
199
              case Option::Put:
167
199
                results_.value = strike * riskFreeDiscount -
168
199
                    euroTwoAssetMaxBasketCall(forward1, forward2, 0.0,
169
199
                                              riskFreeDiscount,
170
199
                                              variance1, variance2, rho_) +
171
199
                    euroTwoAssetMaxBasketCall(forward1, forward2, strike,
172
199
                                              riskFreeDiscount,
173
199
                                              variance1, variance2, rho_);
174
199
                break;
175
0
              default:
176
0
                QL_FAIL("unknown option type");
177
307
            }
178
307
        } else if (min_basket != nullptr) {
179
78
            switch (payoff->optionType()) {
180
              // euro call on a two asset min basket
181
28
              case Option::Call:
182
28
                results_.value =
183
28
                    euroTwoAssetMinBasketCall(forward1, forward2, strike,
184
28
                                              riskFreeDiscount,
185
28
                                              variance1, variance2,
186
28
                                              rho_);
187
28
                break;
188
              // euro put on a two asset min basket
189
50
              case Option::Put:
190
50
                results_.value = strike * riskFreeDiscount -
191
50
                    euroTwoAssetMinBasketCall(forward1, forward2, 0.0,
192
50
                                              riskFreeDiscount,
193
50
                                              variance1, variance2, rho_) +
194
50
                    euroTwoAssetMinBasketCall(forward1, forward2, strike,
195
50
                                              riskFreeDiscount,
196
50
                                              variance1, variance2, rho_);
197
50
                break;
198
0
              default:
199
0
                QL_FAIL("unknown option type");
200
78
            }
201
78
        } else {
202
            QL_FAIL("unknown type");
203
0
        }
204
385
    }
205
206
}