Coverage Report

Created: 2025-08-11 06:28

/src/quantlib/ql/pricingengines/basket/fdndimblackscholesvanillaengine.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
#include <ql/exercise.hpp>
21
#include <ql/processes/blackscholesprocess.hpp>
22
#include <ql/math/matrixutilities/getcovariance.hpp>
23
#include <ql/math/distributions/normaldistribution.hpp>
24
#include <ql/math/matrixutilities/symmetricschurdecomposition.hpp>
25
#include <ql/methods/finitedifferences/meshers/fdmmeshercomposite.hpp>
26
#include <ql/methods/finitedifferences/meshers/predefined1dmesher.hpp>
27
#include <ql/methods/finitedifferences/operators/fdmwienerop.hpp>
28
#include <ql/methods/finitedifferences/solvers/fdmndimsolver.hpp>
29
#include <ql/methods/finitedifferences/utilities/fdminnervaluecalculator.hpp>
30
#include <ql/methods/finitedifferences/stepconditions/fdmstepconditioncomposite.hpp>
31
#include <ql/pricingengines/basket/fdndimblackscholesvanillaengine.hpp>
32
#include <ql/pricingengines/basket/vectorbsmprocessextractor.hpp>
33
#include <boost/preprocessor/iteration/local.hpp>
34
#include <utility>
35
36
namespace QuantLib {
37
38
    namespace detail {
39
        class FdmPCABasketInnerValue: public FdmInnerValueCalculator {
40
          public:
41
            FdmPCABasketInnerValue(
42
                ext::shared_ptr<BasketPayoff> payoff,
43
                ext::shared_ptr<FdmMesher> mesher,
44
                Array logS0, const Array& vols,
45
                std::vector<ext::shared_ptr<YieldTermStructure>> qTS,
46
                ext::shared_ptr<YieldTermStructure> rTS,
47
                Matrix Q, Array l)
48
0
            : n_(logS0.size()),
49
0
              payoff_(std::move(payoff)),
50
0
              mesher_(std::move(mesher)),
51
0
              logS0_(std::move(logS0)), v_(vols*vols),
52
0
              qTS_(std::move(qTS)),
53
0
              rTS_(std::move(rTS)),
54
0
              Q_(std::move(Q)), l_(std::move(l)),
55
0
              cachedT_(Null<Real>()),
56
0
              qf_(n_) { }
57
58
0
            Real innerValue(const FdmLinearOpIterator& iter, Time t) override {
59
0
                if (!close_enough(t, cachedT_)) {
60
0
                    rf_ = rTS_->discount(t);
61
0
                    for (Size i=0; i < n_; ++i)
62
0
                        qf_[i] = qTS_[i]->discount(t);
63
0
                }
64
0
                Array x(n_);
65
0
                for (Size i=0; i < n_; ++i)
66
0
                    x[i] = mesher_->location(iter, i);
67
68
0
                const Array S = Exp(Q_*x - 0.5*v_*t + logS0_)*qf_/rf_;
69
70
0
                return (*payoff_)(S);
71
0
            }
72
0
            Real avgInnerValue(const FdmLinearOpIterator& iter, Time t) override {
73
0
                return innerValue(iter, t);
74
0
            }
75
76
          private:
77
            const Size n_;
78
            const ext::shared_ptr<BasketPayoff> payoff_;
79
            const ext::shared_ptr<FdmMesher> mesher_;
80
            const Array logS0_, v_;
81
            const std::vector<ext::shared_ptr<YieldTermStructure>> qTS_;
82
            const ext::shared_ptr<YieldTermStructure> rTS_;
83
            const Matrix Q_;
84
            const Array l_;
85
            Time cachedT_;
86
            Array qf_;
87
            DiscountFactor rf_;
88
        };
89
    }
90
91
    FdndimBlackScholesVanillaEngine::FdndimBlackScholesVanillaEngine(
92
        std::vector<ext::shared_ptr<GeneralizedBlackScholesProcess> > processes,
93
        Matrix rho,
94
        std::vector<Size> xGrids,
95
        Size tGrid, Size dampingSteps,
96
        const FdmSchemeDesc& schemeDesc)
97
0
    : processes_(std::move(processes)),
98
0
      rho_(std::move(rho)),
99
0
      xGrids_(std::move(xGrids)),
100
0
      tGrid_(tGrid),
101
0
      dampingSteps_(dampingSteps),
102
0
      schemeDesc_(schemeDesc) {
103
104
0
        QL_REQUIRE(!processes_.empty(), "no Black-Scholes process is given.");
105
0
        QL_REQUIRE(rho_.size1() == rho_.size2()
106
0
                && rho_.size1() == processes_.size(),
107
0
                "correlation matrix has the wrong size.");
108
0
        QL_REQUIRE(xGrids_.size() == 1 || xGrids_.size() == processes_.size(),
109
0
                "wrong number of xGrids is given.");
110
111
0
        std::for_each(processes_.begin(), processes_.end(),
112
0
            [this](const auto& p) { registerWith(p); });
113
0
    }
114
115
    FdndimBlackScholesVanillaEngine::FdndimBlackScholesVanillaEngine(
116
        std::vector<ext::shared_ptr<GeneralizedBlackScholesProcess> > processes,
117
        Matrix rho, Size xGrid, Size tGrid, Size dampingSteps,
118
        const FdmSchemeDesc& schemeDesc)
119
0
    : FdndimBlackScholesVanillaEngine(
120
0
        std::move(processes), std::move(rho), std::vector<Size>(1, xGrid), tGrid, dampingSteps, schemeDesc)
121
0
    {}
122
123
124
0
    void FdndimBlackScholesVanillaEngine::calculate() const {
125
0
        #ifndef PDE_MAX_SUPPORTED_DIM
126
0
        #define PDE_MAX_SUPPORTED_DIM 4
127
0
        #endif
128
0
        QL_REQUIRE(processes_.size() <= PDE_MAX_SUPPORTED_DIM,
129
0
            "This engine does not support " << processes_.size() << " underlyings. "
130
0
            << "Max number of underlyings is " << PDE_MAX_SUPPORTED_DIM << ". "
131
0
            << "Please change preprocessor constant PDE_MAX_SUPPORTED_DIM and recompile "
132
0
            << "if a larger number of underlyings is needed.");
133
134
0
        const Date maturityDate = arguments_.exercise->lastDate();
135
0
        const Time maturity = processes_[0]->time(maturityDate);
136
0
        const Real sqrtT = std::sqrt(maturity);
137
138
0
        const detail::VectorBsmProcessExtractor pExtractor(processes_);
139
0
        const Array s = pExtractor.getSpot();
140
0
        const Array dq = pExtractor.getDividendYieldDf(maturityDate);
141
0
        const Array stdDev = Sqrt(pExtractor.getBlackVariance(maturityDate));
142
0
        const Array vols = stdDev / sqrtT;
143
144
0
        const SymmetricSchurDecomposition schur(
145
0
            getCovariance(vols.begin(), vols.end(), rho_));
146
0
        const Matrix& Q = schur.eigenvectors();
147
0
        const Array& l = schur.eigenvalues();
148
149
0
        const Real eps = 1e-4;
150
0
        std::vector<ext::shared_ptr<Fdm1dMesher> > meshers;
151
152
0
        for (Size i=0; i < processes_.size(); ++i) {
153
0
            const Size xGrid = (xGrids_.size() > 1)
154
0
                ? xGrids_[i]
155
0
                : std::max(Size(4), Size(xGrids_[0]*std::pow(l[i]/l[0], 0.1)));
156
0
            QL_REQUIRE(xGrid >= 4, "minimum grid size is four");
157
158
0
            const Real xStepStize = (1.0-2*eps)/(xGrid-1);
159
160
0
            std::vector<Real> x(xGrid);
161
0
            for (Size j=0; j < xGrid; ++j)
162
0
                x[j] = 1.3*std::sqrt(l[i])*sqrtT
163
0
                    *InverseCumulativeNormal()(eps + j*xStepStize);
164
165
0
            meshers.emplace_back(ext::make_shared<Predefined1dMesher>(x));
166
0
        }
167
168
0
        const ext::shared_ptr<FdmMesherComposite> mesher =
169
0
            ext::make_shared<FdmMesherComposite>(meshers);
170
171
0
        const ext::shared_ptr<BasketPayoff> payoff
172
0
            = ext::dynamic_pointer_cast<BasketPayoff>(arguments_.payoff);
173
0
        QL_REQUIRE(payoff, "basket payoff expected");
174
175
0
        const ext::shared_ptr<YieldTermStructure> rTS =
176
0
            processes_[0]->riskFreeRate().currentLink();
177
178
0
        std::vector<ext::shared_ptr<YieldTermStructure>> qTS(processes_.size());
179
0
        for (Size i=0; i < processes_.size(); ++i)
180
0
            qTS[i] = processes_[i]->dividendYield().currentLink();
181
182
0
        const ext::shared_ptr<FdmInnerValueCalculator> calculator =
183
0
            ext::make_shared<detail::FdmPCABasketInnerValue>(
184
0
                payoff, mesher,
185
0
                Log(s), stdDev/sqrtT,
186
0
                qTS, rTS,
187
0
                Q, l
188
0
            );
189
190
0
        const ext::shared_ptr<FdmStepConditionComposite> conditions
191
0
            = FdmStepConditionComposite::vanillaComposite(
192
0
                DividendSchedule(), arguments_.exercise,
193
0
                mesher, calculator,
194
0
                rTS->referenceDate(), rTS->dayCounter());
195
196
0
        const FdmBoundaryConditionSet boundaries;
197
0
        const FdmSolverDesc solverDesc
198
0
            = { mesher, boundaries, conditions, calculator,
199
0
                maturity, tGrid_, dampingSteps_ };
200
201
0
        const bool isEuropean =
202
0
            ext::dynamic_pointer_cast<EuropeanExercise>(arguments_.exercise) != nullptr;
203
0
        const ext::shared_ptr<FdmWienerOp> op =
204
0
            ext::make_shared<FdmWienerOp>(
205
0
                mesher,
206
0
                (isEuropean)? ext::shared_ptr<YieldTermStructure>() : rTS,
207
0
                l);
208
209
0
        switch(processes_.size()) {
210
0
            #define BOOST_PP_LOCAL_MACRO(n) \
211
0
                case n : \
212
0
                    results_.value = ext::make_shared<FdmNdimSolver<n>>( \
213
0
                        solverDesc, schemeDesc_, op)->interpolateAt( \
214
0
                            std::vector<Real>(processes_.size(), 0.0)); \
215
0
                break;
216
0
            #define BOOST_PP_LOCAL_LIMITS (1, PDE_MAX_SUPPORTED_DIM)
217
0
            #include BOOST_PP_LOCAL_ITERATE()
218
0
          default:
219
0
            QL_FAIL("Not implemented for " << processes_.size() << " processes");
220
0
        }
221
222
0
        if (isEuropean)
223
0
            results_.value *= pExtractor.getInterestRateDf(maturityDate);
224
0
    }
225
}