/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 | | } |