Coverage Report

Created: 2025-12-08 06:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/pricingengines/basket/choibasketengine.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2024 Klaus Spanderen
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
/*! \file choibasketengine.cpp
21
*/
22
23
#include <ql/exercise.hpp>
24
#include <ql/quotes/simplequote.hpp>
25
#include <ql/math/matrixutilities/svd.hpp>
26
#include <ql/math/matrixutilities/householder.hpp>
27
#include <ql/math/matrixutilities/getcovariance.hpp>
28
#include <ql/math/matrixutilities/choleskydecomposition.hpp>
29
#include <ql/math/integrals/gaussianquadratures.hpp>
30
#include <ql/math/distributions/normaldistribution.hpp>
31
#include <ql/pricingengines/basket/choibasketengine.hpp>
32
#include <ql/pricingengines/basket/vectorbsmprocessextractor.hpp>
33
#include <ql/pricingengines/basket/singlefactorbsmbasketengine.hpp>
34
#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
35
36
#include <boost/math/special_functions/sign.hpp>
37
38
namespace QuantLib {
39
40
    ChoiBasketEngine::ChoiBasketEngine(
41
        std::vector<ext::shared_ptr<GeneralizedBlackScholesProcess> > processes,
42
        Matrix rho, Real lambda,
43
        Size maxNrIntegrationSteps,
44
        bool calcFwdDelta, bool controlVariate)
45
0
    : n_(processes.size()),
46
0
      processes_(std::move(processes)),
47
0
      rho_(std::move(rho)),
48
0
      lambda_(lambda),
49
0
      maxNrIntegrationSteps_(maxNrIntegrationSteps),
50
0
      calcFwdDelta_(calcFwdDelta || controlVariate),
51
0
      controlVariate_(controlVariate) {
52
53
0
        QL_REQUIRE(n_ > 0, "No Black-Scholes process is given.");
54
0
        QL_REQUIRE(n_ == rho_.size1() && rho_.size1() == rho_.size2(),
55
0
            "process and correlation matrix must have the same size.");
56
57
0
        QL_REQUIRE(lambda_ > 0.0, "lambda must be positive");
58
59
0
        std::for_each(processes_.begin(), processes_.end(),
60
0
            [this](const auto& p) { registerWith(p); });
61
0
    }
62
63
0
    void ChoiBasketEngine::calculate() const {
64
0
        const ext::shared_ptr<EuropeanExercise> exercise =
65
0
            ext::dynamic_pointer_cast<EuropeanExercise>(arguments_.exercise);
66
0
        QL_REQUIRE(exercise, "not an European exercise");
67
68
0
        const Date maturityDate = exercise->lastDate();
69
70
0
        const detail::VectorBsmProcessExtractor pExtractor(processes_);
71
0
        const Array s = pExtractor.getSpot();
72
0
        const Array dq = pExtractor.getDividendYieldDf(maturityDate);
73
0
        const DiscountFactor dr0 = pExtractor.getInterestRateDf(maturityDate);
74
75
0
        Array stdDev = Sqrt(pExtractor.getBlackVariance(maturityDate));
76
0
        std::transform(stdDev.begin(), stdDev.end(), stdDev.begin(),
77
0
            [](Real x) -> Real { return std::max(QL_EPSILON*QL_EPSILON, x); }
78
0
        );
79
80
0
        const Array fwd = s * dq/dr0;
81
82
0
        const ext::shared_ptr<AverageBasketPayoff> avgPayoff =
83
0
            (ext::dynamic_pointer_cast<AverageBasketPayoff>(arguments_.payoff) != nullptr)
84
0
            ? ext::dynamic_pointer_cast<AverageBasketPayoff>(arguments_.payoff)
85
0
            : (ext::dynamic_pointer_cast<SpreadBasketPayoff>(arguments_.payoff) != nullptr)
86
0
                ? ext::make_shared<AverageBasketPayoff>(
87
0
                    ext::dynamic_pointer_cast<SpreadBasketPayoff>(
88
0
                        arguments_.payoff)->basePayoff(),
89
0
                    Array({1.0, -1.0})
90
0
                  )
91
0
                : ext::shared_ptr<AverageBasketPayoff>();
92
93
0
        QL_REQUIRE(avgPayoff, "average or spread basket payoff expected");
94
95
0
        const Array weights = avgPayoff->weights();
96
97
0
        QL_REQUIRE(n_ == weights.size() && n_ > 1,
98
0
             "wrong number of weights arguments in payoff");
99
100
0
        const Array g = weights*fwd / Norm2(weights*fwd);
101
102
0
        const Matrix Sigma = getCovariance(stdDev.begin(), stdDev.end(), rho_);
103
0
        Array vStar1 = Sigma*g;
104
0
        vStar1 /= std::sqrt(DotProduct(g, vStar1));
105
106
0
        const Matrix C = CholeskyDecomposition(Sigma);
107
108
0
        const Real eps = 100*std::sqrt(QL_EPSILON);
109
        // publication sets tol=0, pyfeng implementation sets tol=0.01
110
0
        const Real tol = 100*std::sqrt(QL_EPSILON);
111
112
0
        bool flip = false;
113
0
        for (Size i=0; i < n_; ++i)
114
0
            if (boost::math::sign(g[i])*vStar1[i] < tol*stdDev[i]) {
115
0
                flip = true;
116
0
                vStar1[i] = eps * boost::math::sign(g[i]) * stdDev[i];
117
0
            }
118
119
0
        Array q1(n_);
120
0
        if (flip) {
121
            //q1 = inverse(C)*vStar1;
122
0
            for (Size i=0; i < n_; ++i)
123
0
                q1[i] = (vStar1[i] - std::inner_product(
124
0
                    C.row_begin(i), C.row_begin(i) + i, q1.begin(), Real(0.0)))/C[i][i];
125
126
0
            vStar1 /= Norm2(q1);
127
0
        }
128
0
        else {
129
0
            q1 = transpose(C)*g;
130
0
        }
131
0
        q1 /= Norm2(q1);
132
133
0
        Array e1(n_, 0.0);
134
0
        e1[0] = 1.0;
135
136
0
        const Matrix R = HouseholderTransformation(
137
0
            HouseholderReflection(e1).reflectionVector(q1)).getMatrix();
138
0
        Matrix R_2_n = Matrix(n_, n_-1);
139
0
        for (Size i=0; i < n_; ++i)
140
0
            std::copy(R.row_begin(i)+1, R.row_end(i), R_2_n.row_begin(i));
141
142
0
        const SVD svd(C*R_2_n);
143
0
        const Matrix& U = svd.U();
144
0
        const Array& sv = svd.singularValues();
145
146
0
        Matrix v(n_, n_-1);
147
0
        for (Size i=0; i < n_-1; ++i)
148
0
            std::transform(
149
0
                U.column_begin(i), U.column_end(i), v.column_begin(i),
150
0
                [i, &sv](Real x) -> Real { return sv[i]*x; }
151
0
            );
152
153
0
        std::vector<Size> nIntOrder(n_-1);
154
0
        Real lambda = lambda_;
155
0
        const Real alpha = 1/std::abs(DotProduct(g, vStar1));
156
0
        do {
157
0
            const Real intScale = lambda * alpha;
158
0
            for (Size i=0; i < n_-1; ++i)
159
0
                nIntOrder[i] = Size(std::lround(1 + intScale*sv[i]));
160
161
0
            lambda*=0.9;
162
163
0
            QL_REQUIRE(lambda/lambda_ > 1e-10,
164
0
                "can not rescale lambda to fit max integration order");
165
0
        } while (std::accumulate(
166
0
                     nIntOrder.begin(), nIntOrder.end(), 1.0, std::multiplies<>())
167
0
                > Real(maxNrIntegrationSteps_));
168
169
0
        std::vector<ext::shared_ptr<SimpleQuote> > quotes;
170
0
        std::vector<ext::shared_ptr<GeneralizedBlackScholesProcess> > p;
171
0
        for (Size i=0; i < n_; ++i) {
172
0
            quotes.push_back(ext::make_shared<SimpleQuote>(fwd[i]));
173
174
0
            const Handle<BlackVolTermStructure> bv = processes_[i]->blackVolatility();
175
0
            const Volatility vol = vStar1[i] / std::sqrt(
176
0
                bv->dayCounter().yearFraction(bv->referenceDate(), maturityDate)
177
0
            );
178
0
            p.push_back(
179
0
                ext::make_shared<BlackProcess>(
180
0
                    Handle<Quote>(quotes[i]),
181
0
                    processes_[i]->riskFreeRate(),
182
0
                    Handle<BlackVolTermStructure>(
183
0
                       ext::make_shared<BlackConstantVol>(
184
0
                          bv->referenceDate(), bv->calendar(),
185
0
                          Handle<Quote>(ext::make_shared<SimpleQuote>(vol)),
186
0
                          bv->dayCounter()
187
0
                       )
188
0
                    )
189
0
                )
190
0
            );
191
0
        }
192
193
0
        BasketOption option(avgPayoff, exercise);
194
0
        option.setPricingEngine(ext::make_shared<SingleFactorBsmBasketEngine>(p));
195
196
0
        Array vq(n_);
197
0
        for (Size i=0; i < n_; ++i)
198
0
            vq[i] = 0.5*std::accumulate(
199
0
                v.row_begin(i), v.row_end(i), Real(0.0),
200
0
                [](Real acc, Real x) -> Real { return acc + x*x; }
201
0
            );
202
203
0
        MultiDimGaussianIntegration ghq(
204
0
            nIntOrder,
205
0
            [](const Size n) { return ext::make_shared<GaussHermiteIntegration>(n); }
206
0
        );
207
0
        const Real normFactor = std::pow(M_PI, -0.5*nIntOrder.size());
208
209
0
        std::vector<Real> dStore;
210
0
        dStore.reserve(ghq.weights().size());
211
0
        const auto bsm1dPricer = [&](const Array& z) -> Real {
212
0
            const Array f = Exp(-M_SQRT2*(v*z) - vq) * fwd;
213
214
0
            for (Size i=0; i < f.size(); ++i)
215
0
                quotes[i]->setValue(f[i]);
216
217
0
            dStore.push_back(ext::any_cast<Real>(option.additionalResults().at("d")));
218
0
            return std::exp(-DotProduct(z, z)) * option.NPV();
219
0
        };
220
221
0
        results_.value = ghq(bsm1dPricer) * normFactor;
222
223
0
        if (calcFwdDelta_) {
224
0
            const ext::shared_ptr<PlainVanillaPayoff> payoff =
225
0
                 ext::dynamic_pointer_cast<PlainVanillaPayoff>(avgPayoff->basePayoff());
226
0
            QL_REQUIRE(payoff, "non-plain vanilla payoff given");
227
0
            const Real putIndicator = (payoff->optionType() == Option::Call) ? 0.0 : -1.0;
228
229
0
            Size dStoreCounter;
230
0
            const CumulativeNormalDistribution N;
231
232
0
            Array fwdDelta(n_), fHat(n_);
233
0
            for (Size k=0; k < n_; ++k) {
234
0
                dStoreCounter = 0;
235
236
0
                const auto deltaPricer = [&](const Array& z) -> Real {
237
0
                    const Real d = dStore[dStoreCounter++];
238
0
                    const Real vz = std::inner_product(
239
0
                        v.row_begin(k), v.row_end(k), z.begin(), Real(0.0));
240
0
                    const Real f = std::exp(-M_SQRT2*vz - vq[k]);
241
242
0
                    return std::exp(-DotProduct(z, z)) * f * N(d + vStar1[k]);
243
0
                };
244
245
0
                fwdDelta[k] = dr0*weights[k]*(ghq(deltaPricer) * normFactor + putIndicator);
246
247
0
                const std::string deltaName = "forwardDelta " + std::to_string(k);
248
0
                results_.additionalResults[deltaName] = fwdDelta[k];
249
0
            }
250
251
0
            if (controlVariate_) {
252
0
                for (Size k=0; k < n_; ++k) {
253
0
                    const auto fHatPricer =  [&](const Array& z) -> Real {
254
0
                        const Real vz = std::inner_product(
255
0
                            v.row_begin(k), v.row_end(k), z.begin(), Real(0.0));
256
0
                        const Real f = std::exp(-M_SQRT2*vz - vq[k]);
257
258
0
                        return std::exp(-DotProduct(z, z)) * f;
259
0
                    };
260
0
                fHat[k] = ghq(fHatPricer) * normFactor;
261
0
                }
262
0
                const Array cv = fwdDelta*fwd*(fHat-1.0);
263
0
                results_.value -= std::accumulate(cv.begin(), cv.end(), Real(0.0));
264
0
            }
265
0
        }
266
0
    }
267
}