Coverage Report

Created: 2026-01-25 06:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/experimental/forward/analytichestonforwardeuropeanengine.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2020 Jack Gillett
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
#include <ql/experimental/forward/analytichestonforwardeuropeanengine.hpp>
21
#include <complex>
22
#include <utility>
23
24
namespace QuantLib {
25
26
27
    class P12Integrand {
28
      private:
29
        ext::shared_ptr<AnalyticHestonEngine>& engine_;
30
        Real logK_, phiRightLimit_;
31
        Time tenor_;
32
        std::complex<Real> i_, adj_;
33
      public:
34
        P12Integrand(ext::shared_ptr<AnalyticHestonEngine>& engine,
35
                     Real logK,
36
                     Time tenor,
37
                     bool P1, // true for P1, false for P2
38
0
                     Real phiRightLimit = 100) : engine_(engine), logK_(logK),
39
0
            phiRightLimit_(phiRightLimit), tenor_(tenor), i_(std::complex<Real>(0.0, 1.0)) {
40
41
            // Only difference between P1 and P2 integral is the additional term in the chF evaluation
42
0
            if (P1) {
43
0
                adj_ = std::complex<Real>(0.0, -1.0);
44
0
            } else {
45
0
                adj_ = std::complex<Real>(0.0, 0.0);
46
0
            }
47
0
        }
48
49
        // QL Gaussian Quadrature - map phi from [-1 to 1] to {0, phiRightLimit] 
50
0
        Real operator()(Real phi) const {
51
0
            Real phiDash = (0.5+1e-8+0.5*phi) * phiRightLimit_; // Map phi to full range
52
0
            return 0.5*phiRightLimit_*std::real((std::exp(-phiDash*logK_*i_) / (phiDash*i_)) * engine_->chF(phiDash+adj_, tenor_));
53
0
        }
54
    };
55
56
57
    class P12HatIntegrand {
58
      private:
59
        Time tenor_, resetTime_;
60
        Handle<Quote>& s0_;
61
        bool P1_;
62
        Real logK_, phiRightLimit_, nuRightLimit_;
63
        const AnalyticHestonForwardEuropeanEngine* const parent_;
64
        GaussLegendreIntegration innerIntegrator_;
65
      public:
66
        P12HatIntegrand(Time tenor,
67
                        Time resetTime,
68
                        Handle<Quote>& s0,
69
                        Real logK,
70
                        bool P1, // true for P1, false for P2
71
                        const AnalyticHestonForwardEuropeanEngine* const parent,
72
                        Real phiRightLimit,
73
0
                        Real nuRightLimit) : tenor_(tenor), resetTime_(resetTime),
74
0
            s0_(s0), P1_(P1), logK_(logK), phiRightLimit_(phiRightLimit),
75
0
            nuRightLimit_(nuRightLimit), parent_(parent), innerIntegrator_(128) {}
76
0
        Real operator()(Real nu) const {
77
78
            // Rescale nu to [-1, 1]
79
0
            Real nuDash = nuRightLimit_ * (0.5 * nu + 0.5 + 1e-8);
80
81
            // Calculate the chF from var(t) to expiry
82
0
            ext::shared_ptr<AnalyticHestonEngine> engine = parent_->forwardChF(s0_, nuDash);
83
0
            P12Integrand pIntegrand(engine, logK_, tenor_, P1_, phiRightLimit_);
84
0
            Real p1Integral = innerIntegrator_(pIntegrand);
85
86
            // Calculate the value of the propagator to nu
87
0
            Real propagator = parent_->propagator(resetTime_, nuDash);
88
89
            // Take the product, and scale integral's value back up to [0, right_lim]
90
0
            return propagator * (0.5 + p1Integral/M_PI);
91
0
        }
92
    };
93
94
95
    AnalyticHestonForwardEuropeanEngine::AnalyticHestonForwardEuropeanEngine(
96
        ext::shared_ptr<HestonProcess> process, Size integrationOrder)
97
0
    : process_(std::move(process)), integrationOrder_(integrationOrder), outerIntegrator_(128) {
98
99
0
        v0_ = process_->v0();
100
0
        rho_ = process_->rho();
101
0
        kappa_ = process_->kappa();
102
0
        theta_ = process_->theta();
103
0
        sigma_ = process_->sigma();
104
0
        s0_ = process_->s0();
105
106
0
        QL_REQUIRE(sigma_ > 0.1,
107
0
                   "Very low values (<~10%) for Heston Vol-of-Vol cause numerical issues" \
108
0
                   "in this implementation of the propagator function, try using" \
109
0
                   "MCForwardEuropeanHestonEngine Monte-Carlo engine instead");
110
111
0
        riskFreeRate_ = process_->riskFreeRate();
112
0
        dividendYield_ = process_->dividendYield();
113
114
        // Some of the required constant intermediate variables can be calculated now
115
0
        kappaHat_ = kappa_ - rho_ * sigma_;
116
0
        thetaHat_ = kappa_ * theta_ / kappaHat_;
117
0
        R_ = 4 * kappaHat_ * thetaHat_ / (sigma_ * sigma_);
118
0
    }
119
120
121
0
    void AnalyticHestonForwardEuropeanEngine::calculate() const {
122
        // This is a european option pricer
123
0
        QL_REQUIRE(this->arguments_.exercise->type() == Exercise::European,
124
0
                   "not an European option");
125
126
        // We only price plain vanillas
127
0
        ext::shared_ptr<PlainVanillaPayoff> payoff =
128
0
            ext::dynamic_pointer_cast<PlainVanillaPayoff>(this->arguments_.payoff);
129
0
        QL_REQUIRE(payoff, "non plain vanilla payoff given");
130
131
0
        Time resetTime = this->process_->time(this->arguments_.resetDate);
132
0
        Time expiryTime = this->process_->time(this->arguments_.exercise->lastDate());
133
0
        Time tenor = expiryTime - resetTime;
134
0
        Real moneyness = this->arguments_.moneyness;
135
136
        // K needs to be scaled to forward AT RESET TIME, not spot...
137
0
        Real expiryDcf = riskFreeRate_->discount(expiryTime);
138
0
        Real resetDcf = riskFreeRate_->discount(resetTime);
139
0
        Real expiryDividendDiscount = dividendYield_->discount(expiryTime);
140
0
        Real resetDividendDiscount = dividendYield_->discount(resetTime);
141
0
        Real expiryRatio = expiryDcf / expiryDividendDiscount;
142
0
        Real resetRatio = resetDcf / resetDividendDiscount;
143
144
0
        QL_REQUIRE(resetTime >= 0.0, "Reset Date cannot be in the past");
145
0
        QL_REQUIRE(expiryTime >= 0.0, "Expiry Date cannot be in the past");
146
147
        // Use some heuristics to decide upon phiRightLimit and nuRightLimit
148
0
        Real phiRightLimit = 100.0;
149
0
        Real nuRightLimit = std::max(2.0, 10.0 * (1+std::max(0.0, rho_)) * sigma_ * std::sqrt(resetTime * std::max(v0_, theta_)));
150
151
        // do the 2D integral calculation. For very short times, we just fall back on the standard
152
        // calculation, both for accuracy and because tStar==0 causes some numerical issues...
153
0
        std::pair<Real, Real> P1HatP2Hat;
154
0
        if (resetTime <= 1e-3) {
155
0
            Handle<Quote> tempQuote(ext::shared_ptr<Quote>(new SimpleQuote(s0_->value())));
156
0
            P1HatP2Hat = calculateP1P2(tenor, tempQuote, moneyness * s0_->value(), expiryRatio, phiRightLimit);
157
0
        } else {
158
0
            P1HatP2Hat = calculateP1P2Hat(tenor, resetTime, moneyness, expiryRatio/resetRatio, phiRightLimit, nuRightLimit);
159
0
        }
160
161
        // Apply the payoff functions
162
0
        Real value = 0.0;
163
0
        Real F = s0_->value() / expiryRatio;
164
0
        switch (payoff->optionType()){
165
0
            case Option::Call:
166
0
                value = expiryDcf * (F*P1HatP2Hat.first - moneyness*s0_->value()*P1HatP2Hat.second/resetRatio);
167
0
                break;
168
0
            case Option::Put:
169
0
                value = expiryDcf * (moneyness*s0_->value()*(1-P1HatP2Hat.second)/resetRatio - F*(1-P1HatP2Hat.first));
170
0
                break;
171
0
            default:
172
0
                QL_FAIL("unknown option type");
173
0
            }
174
175
0
        results_.value = value;
176
177
0
        results_.additionalResults["dcf"] = expiryDcf;
178
0
        results_.additionalResults["qf"] = expiryDividendDiscount;
179
0
        results_.additionalResults["expiryRatio"] = expiryRatio;
180
0
        results_.additionalResults["resetRatio"] = resetRatio;
181
0
        results_.additionalResults["moneyness"] = moneyness;
182
0
        results_.additionalResults["s0"] = s0_->value();
183
0
        results_.additionalResults["fwd"] = F;
184
0
        results_.additionalResults["resetTime"] = resetTime;
185
0
        results_.additionalResults["expiryTime"] = expiryTime;
186
0
        results_.additionalResults["P1Hat"] = P1HatP2Hat.first;
187
0
        results_.additionalResults["P2Hat"] = P1HatP2Hat.second;
188
0
        results_.additionalResults["phiRightLimit"] = phiRightLimit;
189
0
        results_.additionalResults["nuRightLimit"] = nuRightLimit;
190
0
    }
191
192
193
    std::pair<Real, Real> AnalyticHestonForwardEuropeanEngine::calculateP1P2Hat(Time tenor,
194
                                                                                Time resetTime,
195
                                                                                Real moneyness,
196
                                                                                Real ratio,
197
                                                                                Real phiRightLimit,
198
0
                                                                                Real nuRightLimit) const {
199
200
0
        Handle<Quote> unitQuote(ext::shared_ptr<Quote>(new SimpleQuote(1.0)));
201
202
        // Re-expressing moneyness in terms of the forward here (strike fixes to spot, but in
203
        // our pricing calculation we need to compare it to the future at expiry)
204
0
        Real logMoneyness = std::log(moneyness*ratio);
205
206
0
        P12HatIntegrand p1HatIntegrand(tenor, resetTime, unitQuote, logMoneyness, true, this, phiRightLimit, nuRightLimit);
207
0
        P12HatIntegrand p2HatIntegrand(tenor, resetTime, unitQuote, logMoneyness, false, this, phiRightLimit, nuRightLimit);
208
209
0
        Real p1HatIntegral = 0.5 * nuRightLimit * outerIntegrator_(p1HatIntegrand);
210
0
        Real p2HatIntegral = 0.5 * nuRightLimit * outerIntegrator_(p2HatIntegrand);
211
212
0
        std::pair<Real, Real> P1HatP2Hat(p1HatIntegral, p2HatIntegral);
213
214
0
        return P1HatP2Hat;
215
0
    }
216
217
218
    Real AnalyticHestonForwardEuropeanEngine::propagator(Time resetTime,
219
0
                                                         Real varReset) const {
220
0
        Real B, Lambda, term1, term2, term3;
221
222
0
        B = 4 * kappaHat_ / (sigma_ * sigma_ * (1 - std::exp(-kappaHat_ * resetTime)));
223
0
        Lambda = B * std::exp(-kappaHat_ * resetTime) * v0_;
224
225
        // Now construct equation (18) from the paper term-by-term
226
0
        term1 = std::exp(-0.5*(B * varReset + Lambda)) * B / 2;
227
0
        term2 = std::pow(B * varReset / Lambda, 0.5*(R_/2 - 1));
228
0
        term3 = modifiedBesselFunction_i(Real(R_/2 - 1),Real(std::sqrt(Lambda * B * varReset)));
229
230
0
        return term1 * term2 * term3;
231
0
    }
232
233
    ext::shared_ptr<AnalyticHestonEngine> AnalyticHestonForwardEuropeanEngine::forwardChF(
234
                                      Handle<Quote>& spotReset,
235
0
                                      Real varReset) const {
236
237
        // Probably a wasteful implementation here, could be improved by importing
238
        // only the CF-generating parts of the AnalyticHestonEngine (currently private)
239
0
        ext::shared_ptr<HestonProcess> hestonProcess(new
240
0
            HestonProcess(riskFreeRate_, dividendYield_, spotReset,
241
0
                varReset, kappa_, theta_, sigma_, rho_));
242
243
0
        ext::shared_ptr<HestonModel> hestonModel(new HestonModel(hestonProcess));
244
245
0
        ext::shared_ptr<AnalyticHestonEngine> analyticHestonEngine(
246
0
            new AnalyticHestonEngine(hestonModel, integrationOrder_));
247
248
        // Not sure how to pass only the chF, so just pass the whole thing for now!
249
0
        return analyticHestonEngine;
250
0
    }
251
252
253
    std::pair<Real, Real> AnalyticHestonForwardEuropeanEngine::calculateP1P2(Time tenor,
254
                                                                             Handle<Quote>& St,
255
                                                                             Real K,
256
                                                                             Real ratio,
257
0
                                                                             Real phiRightLimit) const {
258
259
0
        ext::shared_ptr<AnalyticHestonEngine> engine = forwardChF(St, v0_);
260
0
        Real logK = std::log(K*ratio/St->value());
261
262
        // Integrate the CF and the complex integrand over positive phi
263
0
        GaussLegendreIntegration integrator = GaussLegendreIntegration(128);
264
0
        P12Integrand p1Integrand(engine, logK, tenor, true, phiRightLimit);
265
0
        P12Integrand p2Integrand(engine, logK, tenor, false, phiRightLimit);
266
267
0
        Real p1Integral = integrator(p1Integrand);
268
0
        Real p2Integral = integrator(p2Integrand);
269
270
0
        std::pair<Real, Real> P1P2(0.5 + p1Integral/M_PI, 0.5 + p2Integral/M_PI);
271
272
0
        return P1P2;
273
0
    }
274
}