Coverage Report

Created: 2025-08-05 06:45

/src/quantlib/ql/pricingengines/basket/choibasketengine.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) 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
 <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
/*! \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 Array stdDev = Sqrt(pExtractor.getBlackVariance(maturityDate));
74
0
        const DiscountFactor dr0 = pExtractor.getInterestRateDf(maturityDate);
75
76
0
        const Array fwd = s * dq/dr0;
77
78
0
        const ext::shared_ptr<AverageBasketPayoff> avgPayoff =
79
0
            (ext::dynamic_pointer_cast<AverageBasketPayoff>(arguments_.payoff) != nullptr)
80
0
            ? ext::dynamic_pointer_cast<AverageBasketPayoff>(arguments_.payoff)
81
0
            : (ext::dynamic_pointer_cast<SpreadBasketPayoff>(arguments_.payoff) != nullptr)
82
0
                ? ext::make_shared<AverageBasketPayoff>(
83
0
                    ext::dynamic_pointer_cast<SpreadBasketPayoff>(
84
0
                        arguments_.payoff)->basePayoff(),
85
0
                    Array({1.0, -1.0})
86
0
                  )
87
0
                : ext::shared_ptr<AverageBasketPayoff>();
88
89
0
        QL_REQUIRE(avgPayoff, "average or spread basket payoff expected");
90
91
0
        const Array weights = avgPayoff->weights();
92
0
        QL_REQUIRE(n_ == weights.size() && n_ > 1,
93
0
             "wrong number of weights arguments in payoff");
94
95
0
        const Array g = weights*fwd / Norm2(weights*fwd);
96
97
0
        const Matrix Sigma = getCovariance(stdDev.begin(), stdDev.end(), rho_);
98
0
        Array vStar1 = Sigma*g;
99
0
        vStar1 /= std::sqrt(DotProduct(g, vStar1));
100
101
0
        const Matrix C = CholeskyDecomposition(Sigma);
102
103
0
        const Real eps = 100*std::sqrt(QL_EPSILON);
104
        // publication sets tol=0, pyfeng implementation sets tol=0.01
105
0
        const Real tol = 100*std::sqrt(QL_EPSILON);
106
107
0
        bool flip = false;
108
0
        for (Size i=0; i < n_; ++i)
109
0
            if (boost::math::sign(g[i])*vStar1[i] < tol*stdDev[i]) {
110
0
                flip = true;
111
0
                vStar1[i] = eps * boost::math::sign(g[i]) * stdDev[i];
112
0
            }
113
114
0
        Array q1(n_);
115
0
        if (flip) {
116
            //q1 = inverse(C)*vStar1;
117
0
            for (Size i=0; i < n_; ++i)
118
0
                q1[i] = (vStar1[i] - std::inner_product(
119
0
                    C.row_begin(i), C.row_begin(i) + i, q1.begin(), Real(0.0)))/C[i][i];
120
121
0
            vStar1 /= Norm2(q1);
122
0
        }
123
0
        else {
124
0
            q1 = transpose(C)*g;
125
0
        }
126
0
        q1 /= Norm2(q1);
127
128
0
        Array e1(n_, 0.0);
129
0
        e1[0] = 1.0;
130
131
0
        const Matrix R = HouseholderTransformation(
132
0
            HouseholderReflection(e1).reflectionVector(q1)).getMatrix();
133
0
        Matrix R_2_n = Matrix(n_, n_-1);
134
0
        for (Size i=0; i < n_; ++i)
135
0
            std::copy(R.row_begin(i)+1, R.row_end(i), R_2_n.row_begin(i));
136
137
0
        const SVD svd(C*R_2_n);
138
0
        const Matrix& U = svd.U();
139
0
        const Array& sv = svd.singularValues();
140
141
0
        Matrix v(n_, n_-1);
142
0
        for (Size i=0; i < n_-1; ++i)
143
0
            std::transform(
144
0
                U.column_begin(i), U.column_end(i), v.column_begin(i),
145
0
                [i, &sv](Real x) -> Real { return sv[i]*x; }
146
0
            );
147
148
0
        std::vector<Size> nIntOrder(n_-1);
149
0
        Real lambda = lambda_;
150
0
        const Real alpha = 1/std::abs(DotProduct(g, vStar1));
151
0
        do {
152
0
            const Real intScale = lambda * alpha;
153
0
            for (Size i=0; i < n_-1; ++i)
154
0
                nIntOrder[i] = Size(std::lround(1 + intScale*sv[i]));
155
156
0
            lambda*=0.9;
157
0
            QL_REQUIRE(lambda/lambda_ > 1e-10,
158
0
                "can not rescale lambda to fit max integration order");
159
0
        } while (std::accumulate(
160
0
                     nIntOrder.begin(), nIntOrder.end(), 1.0, std::multiplies<>())
161
0
                > Real(maxNrIntegrationSteps_));
162
163
0
        std::vector<ext::shared_ptr<SimpleQuote> > quotes;
164
0
        std::vector<ext::shared_ptr<GeneralizedBlackScholesProcess> > p;
165
0
        for (Size i=0; i < n_; ++i) {
166
0
            quotes.push_back(ext::make_shared<SimpleQuote>(fwd[i]));
167
168
0
            const Handle<BlackVolTermStructure> bv = processes_[i]->blackVolatility();
169
0
            const Volatility vol = vStar1[i] / std::sqrt(
170
0
                bv->dayCounter().yearFraction(bv->referenceDate(), maturityDate)
171
0
            );
172
0
            p.push_back(
173
0
                ext::make_shared<BlackProcess>(
174
0
                    Handle<Quote>(quotes[i]),
175
0
                    processes_[i]->riskFreeRate(),
176
0
                    Handle<BlackVolTermStructure>(
177
0
                       ext::make_shared<BlackConstantVol>(
178
0
                          bv->referenceDate(), bv->calendar(),
179
0
                          Handle<Quote>(ext::make_shared<SimpleQuote>(vol)),
180
0
                          bv->dayCounter()
181
0
                       )
182
0
                    )
183
0
                )
184
0
            );
185
0
        }
186
187
0
        BasketOption option(avgPayoff, exercise);
188
0
        option.setPricingEngine(ext::make_shared<SingleFactorBsmBasketEngine>(p));
189
190
0
        Array vq(n_);
191
0
        for (Size i=0; i < n_; ++i)
192
0
            vq[i] = 0.5*std::accumulate(
193
0
                v.row_begin(i), v.row_end(i), Real(0.0),
194
0
                [](Real acc, Real x) -> Real { return acc + x*x; }
195
0
            );
196
197
0
        MultiDimGaussianIntegration ghq(
198
0
            nIntOrder,
199
0
            [](const Size n) { return ext::make_shared<GaussHermiteIntegration>(n); }
200
0
        );
201
0
        const Real normFactor = std::pow(M_PI, -0.5*nIntOrder.size());
202
203
0
        std::vector<Real> dStore;
204
0
        dStore.reserve(ghq.weights().size());
205
0
        const auto bsm1dPricer = [&](const Array& z) -> Real {
206
0
            const Array f = Exp(-M_SQRT2*(v*z) - vq) * fwd;
207
208
0
            for (Size i=0; i < f.size(); ++i)
209
0
                quotes[i]->setValue(f[i]);
210
211
0
            dStore.push_back(ext::any_cast<Real>(option.additionalResults().at("d")));
212
0
            return std::exp(-DotProduct(z, z)) * option.NPV();
213
0
        };
214
215
0
        results_.value = ghq(bsm1dPricer) * normFactor;
216
217
0
        if (calcFwdDelta_) {
218
0
            const ext::shared_ptr<PlainVanillaPayoff> payoff =
219
0
                 ext::dynamic_pointer_cast<PlainVanillaPayoff>(avgPayoff->basePayoff());
220
0
            QL_REQUIRE(payoff, "non-plain vanilla payoff given");
221
0
            const Real putIndicator = (payoff->optionType() == Option::Call) ? 0.0 : -1.0;
222
223
0
            Size dStoreCounter;
224
0
            const CumulativeNormalDistribution N;
225
226
0
            Array fwdDelta(n_), fHat(n_);
227
0
            for (Size k=0; k < n_; ++k) {
228
0
                dStoreCounter = 0;
229
230
0
                const auto deltaPricer = [&](const Array& z) -> Real {
231
0
                    const Real d = dStore[dStoreCounter++];
232
0
                    const Real vz = std::inner_product(
233
0
                        v.row_begin(k), v.row_end(k), z.begin(), Real(0.0));
234
0
                    const Real f = std::exp(-M_SQRT2*vz - vq[k]);
235
236
0
                    return std::exp(-DotProduct(z, z)) * f * N(d + vStar1[k]);
237
0
                };
238
239
0
                fwdDelta[k] = dr0*weights[k]*(ghq(deltaPricer) * normFactor + putIndicator);
240
241
0
                const std::string deltaName = "forwardDelta " + std::to_string(k);
242
0
                results_.additionalResults[deltaName] = fwdDelta[k];
243
0
            }
244
245
0
            if (controlVariate_) {
246
0
                for (Size k=0; k < n_; ++k) {
247
0
                    const auto fHatPricer =  [&](const Array& z) -> Real {
248
0
                        const Real vz = std::inner_product(
249
0
                            v.row_begin(k), v.row_end(k), z.begin(), Real(0.0));
250
0
                        const Real f = std::exp(-M_SQRT2*vz - vq[k]);
251
252
0
                        return std::exp(-DotProduct(z, z)) * f;
253
0
                    };
254
0
                fHat[k] = ghq(fHatPricer) * normFactor;
255
0
                }
256
0
                const Array cv = fwdDelta*fwd*(fHat-1.0);
257
0
                results_.value -= std::accumulate(cv.begin(), cv.end(), Real(0.0));
258
0
            }
259
0
        }
260
0
    }
261
}