Coverage Report

Created: 2026-09-28 06:23

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/pricingengines/barrier/analyticsoftbarrierengine.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2025 William Day
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
21
#include <ql/exercise.hpp>
22
#include <ql/pricingengines/barrier/analyticsoftbarrierengine.hpp>
23
#include <ql/instruments/barrieroption.hpp>
24
#include <ql/pricingengines/blackcalculator.hpp>
25
#include <utility>
26
#include <ql/termstructures/yield/flatforward.hpp>
27
#include <ql/time/calendars/target.hpp>
28
#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
29
#include <ql/time/daycounters/actual360.hpp>
30
#include <ql/pricingengines/barrier/analyticbarrierengine.hpp>
31
#include <iostream>
32
33
34
namespace QuantLib {
35
36
    AnalyticSoftBarrierEngine::AnalyticSoftBarrierEngine(
37
        ext::shared_ptr<GeneralizedBlackScholesProcess> process)
38
0
    : process_(std::move(process)) {
39
0
        registerWith(process_);
40
0
    }
41
42
43
0
    void AnalyticSoftBarrierEngine::calculate() const {
44
45
        // Market data
46
0
        Real S = underlying();
47
0
        Real X = strike();
48
0
        Rate r = riskFreeRate();
49
0
        Rate q = dividendYield();
50
0
        Volatility sigma = volatility();
51
52
        // Barrier parameters
53
0
        Real U = barrierHi();
54
0
        Real L = barrierLo();
55
0
        Barrier::Type barrierType = arguments_.barrierType;
56
        
57
        // Stability tweak for r and q
58
0
        const Real epsilon = 1e-6;  
59
0
        if (std::abs(r - q) < 1e-10) {
60
0
            r = q + epsilon;  // Avoids mu = 0.5 singularity
61
0
        }
62
63
        // Option parameters
64
0
        Time T = residualTime();
65
0
        ext::shared_ptr<PlainVanillaPayoff> payoff = ext::dynamic_pointer_cast<PlainVanillaPayoff>(arguments_.payoff); 
66
0
        Option::Type optionType = payoff->optionType();
67
0
        Integer eta = (optionType == Option::Call ? 1 : -1);
68
0
        Rate b = r - q; // cost of carry
69
70
0
        validateInputs(S, X, r, q, T, U, L, optionType, barrierType, sigma);
71
72
0
        bool isKnockedIn = (barrierType == Barrier::DownIn && S <= L) || 
73
0
                          (barrierType == Barrier::UpIn && S >= U);
74
0
        bool isKnockedOut = (barrierType == Barrier::DownOut && S <= L) || 
75
0
                          (barrierType == Barrier::UpOut && S >= U);
76
77
0
        bool isSingleBarrier = (std::fabs(U - L) < 1e-4);  
78
        
79
80
        // edge case 1: fully knocked in options should be priced as vanilla (there are no more barrier features to consider)
81
0
        if (isKnockedIn) {
82
0
              results_.value = vanillaEquivalent();
83
0
              return;
84
0
            }
85
86
        // edge case 2: knocked out options are worthless
87
0
        else if (isKnockedOut) {   
88
0
            results_.value = 0.0;  
89
0
            return;
90
0
            }
91
        
92
        // edge case 3: Haug formula breaks when U=L, use single barrier option formula instead
93
0
        if (isSingleBarrier) {
94
0
            results_.value = standardBarrierEquivalent();
95
0
            return;
96
0
        }
97
98
        // soft barrier pricing logic
99
0
        Real w = knockInValue(S, X, r, sigma, T, U, L, b, optionType,eta);
100
0
        results_.value = (barrierType == Barrier::DownIn || barrierType == Barrier::UpIn)
101
0
            ? w                     // knock in price
102
0
            : vanillaEquivalent() - w;  // knock out price
103
0
        }
104
    
105
106
    // Implements the formula to calculate 'w' from the Haug textbook, used in soft barrier pricing
107
    Real AnalyticSoftBarrierEngine::knockInValue(Real S, Real X, Rate r, Volatility sigma, Time T,
108
                                                Real U, Real L, Real b, Option::Type optionType,
109
0
                                                Integer eta) const {
110
        // constant terms                                              
111
0
        const Real mu = (b + 0.5 * sigma * sigma) / (sigma * sigma);
112
0
        const Real sqrtT = std::sqrt(T);
113
0
        const Real lambda1 = std::exp(-0.5 * sigma * sigma * T * (mu + 0.5) * (mu - 0.5));
114
0
        const Real lambda2 = std::exp(-0.5 * sigma * sigma * T * (mu - 0.5) * (mu - 1.5));
115
0
        const Real SX = S * X;
116
0
        const Real logU2_SX = std::log((U * U) / SX);
117
0
        const Real logL2_SX = std::log((L * L) / SX);
118
119
        // d and e terms
120
0
        const Real d1 = logU2_SX / (sigma * sqrtT) + mu * sigma * sqrtT;
121
0
        const Real d2 = d1 - (mu + 0.5) * sigma * sqrtT;
122
0
        const Real d3 = logU2_SX / (sigma * sqrtT) + (mu - 1) * sigma * sqrtT;
123
0
        const Real d4 = d3 - (mu - 0.5) * sigma * sqrtT;
124
125
0
        const Real e1 = logL2_SX / (sigma * sqrtT) + mu * sigma * sqrtT;
126
0
        const Real e2 = e1 - (mu + 0.5) * sigma * sqrtT;
127
0
        const Real e3 = logL2_SX / (sigma * sqrtT) + (mu - 1) * sigma * sqrtT;
128
0
        const Real e4 = e3 - (mu - 0.5) * sigma * sqrtT;
129
130
0
        const Real Nd1 = f_(eta * d1);
131
0
        const Real Nd2 = f_(eta * d2);
132
0
        const Real Nd3 = f_(eta * d3);
133
0
        const Real Nd4 = f_(eta * d4);
134
0
        const Real Ne1 = f_(eta * e1);
135
0
        const Real Ne2 = f_(eta * e2);
136
0
        const Real Ne3 = f_(eta * e3);
137
0
        const Real Ne4 = f_(eta * e4);
138
139
140
        // term 1
141
0
        Real term1 = eta * S * std::exp((b - r) * T) * std::pow(S, -2.0 * mu)
142
0
            * std::pow(SX, mu + 0.5) / (2.0 * (mu + 0.5));
143
144
145
0
        term1 *= std::pow(U * U / SX, mu + 0.5) * Nd1 - lambda1 * Nd2
146
0
            - std::pow(L * L / SX, mu + 0.5) * Ne1 + lambda1 * Ne2;
147
148
149
        // term 2
150
0
        Real term2 = eta * X * std::exp(-r * T) * std::pow(S, -2.0 * (mu - 1))
151
0
            * std::pow(SX, mu - 0.5) / (2.0 * (mu - 0.5));
152
153
154
0
        term2 *= std::pow(U * U / SX, mu - 0.5) * Nd3 - lambda2 * Nd4
155
0
            - std::pow(L * L / SX, mu - 0.5) * Ne3 + lambda2 * Ne4;
156
157
158
        // final result
159
0
        Real w = (1.0 / (U - L)) * (term1 - term2);
160
0
        return w;
161
0
    }
162
163
164
    // helper function to check inputs are reasonable
165
    void AnalyticSoftBarrierEngine::validateInputs(Real S, Real X, Rate r, Rate q, Time T, Real U, Real L,
166
                                                   Option::Type optionType, Barrier::Type barrierType,
167
0
                                                   Real sigma) const {
168
        // Core Parameter checks                                                
169
0
        QL_REQUIRE(S > 0.0, "Spot price must be > 0");
170
0
        QL_REQUIRE(X > 0.0, "Strike price must be > 0");
171
0
        QL_REQUIRE(T > 0.0, "Option must have time to maturity > 0");
172
0
        QL_REQUIRE(sigma > 0, "Volatility must be > 0");
173
0
        QL_REQUIRE(optionType == Option::Call || optionType == Option::Put, "Invalid option type");                                       
174
0
        QL_REQUIRE(r <= 1.0 && r >= -0.05, "Interest rate must be between -5% and 100%");
175
0
        QL_REQUIRE(q <= 1.0 && q >= -0.1, "Dividend yield must be between -10% and 100%");
176
177
        
178
        // Barrier type checks
179
0
        QL_REQUIRE(
180
0
          barrierType == Barrier::DownIn ||
181
0
          barrierType == Barrier::DownOut ||
182
0
          barrierType == Barrier::UpIn ||
183
0
          barrierType == Barrier::UpOut,
184
0
          "Invalid barrier type");
185
0
        QL_REQUIRE(L != Null<Real>(), "no low barrier given");
186
0
        QL_REQUIRE(U != Null<Real>(), "no high barrier given");
187
0
        QL_REQUIRE(U > 0.0 && L > 0.0, "Barrier levels must be positive");
188
0
        QL_REQUIRE(U >= L, "Upper barrier must be greater than or equal to lower barrier");
189
0
        }
190
    
191
192
    // helper functions 
193
0
    Real AnalyticSoftBarrierEngine::underlying() const {
194
0
        return process_->x0();
195
0
    }
196
197
0
    Real AnalyticSoftBarrierEngine::strike() const {
198
0
        ext::shared_ptr<PlainVanillaPayoff> payoff = ext::dynamic_pointer_cast<PlainVanillaPayoff>(arguments_.payoff);  
199
0
        QL_REQUIRE(payoff, "non-plain payoff given");
200
0
        return payoff->strike();
201
0
    }
202
203
0
    Time AnalyticSoftBarrierEngine::residualTime() const {
204
0
        return process_->time(arguments_.exercise->lastDate());
205
0
    }
206
207
0
    Volatility AnalyticSoftBarrierEngine::volatility() const {
208
0
        return process_->blackVolatility()->blackVol(residualTime(), strike());
209
0
    }
210
211
0
    Real AnalyticSoftBarrierEngine::stdDeviation() const {
212
0
        return volatility() * std::sqrt(residualTime());
213
0
    }
214
215
0
    Real AnalyticSoftBarrierEngine::barrierLo() const {
216
0
        return arguments_.barrier_lo;
217
0
    }
218
219
0
    Real AnalyticSoftBarrierEngine::barrierHi() const {
220
0
        return arguments_.barrier_hi;
221
0
    }
222
223
0
    Rate AnalyticSoftBarrierEngine::riskFreeRate() const {
224
0
        return process_->riskFreeRate()->zeroRate(residualTime(), Continuous, NoFrequency);
225
0
    }
226
227
0
    DiscountFactor AnalyticSoftBarrierEngine::riskFreeDiscount() const {
228
0
        return process_->riskFreeRate()->discount(residualTime());
229
0
    }
230
231
0
    Rate AnalyticSoftBarrierEngine::dividendYield() const {
232
0
        return process_->dividendYield()->zeroRate(residualTime(),Continuous, NoFrequency);
233
0
    }
234
235
0
    DiscountFactor AnalyticSoftBarrierEngine::dividendDiscount() const {
236
0
        return process_->dividendYield()->discount(residualTime());
237
0
    }
238
            
239
240
0
    Real AnalyticSoftBarrierEngine::vanillaEquivalent() const {
241
0
        ext::shared_ptr<StrikedTypePayoff> payoff =
242
0
            ext::dynamic_pointer_cast<StrikedTypePayoff>(arguments_.payoff);
243
0
        Real forwardPrice = underlying() * dividendDiscount() / riskFreeDiscount();
244
0
        BlackCalculator black(payoff, forwardPrice, stdDeviation(), riskFreeDiscount());
245
0
        Real vanilla = black.value();
246
0
        return std::max(vanilla, 0.0);
247
248
0
    }       
249
250
0
    Real AnalyticSoftBarrierEngine::standardBarrierEquivalent() const {
251
252
0
    ext::shared_ptr<StrikedTypePayoff> payoff =
253
0
        ext::dynamic_pointer_cast<StrikedTypePayoff>(arguments_.payoff);
254
0
    QL_REQUIRE(payoff, "Payoff could not be cast to StrikedTypePayoff");
255
256
0
    BarrierOption tempOption(
257
0
        arguments_.barrierType,
258
0
        arguments_.barrier_hi,
259
0
        0.0,
260
0
        payoff,
261
0
        arguments_.exercise
262
0
    );
263
264
0
    Real spotVal = underlying();
265
0
    Real qVal = dividendYield();
266
0
    Real rVal = riskFreeRate();
267
0
    Volatility volVal = volatility();
268
269
0
    Handle<Quote> spot(ext::make_shared<SimpleQuote>(spotVal));
270
0
    Handle<YieldTermStructure> q(ext::make_shared<FlatForward>(0, TARGET(), qVal, Actual360()));
271
0
    Handle<YieldTermStructure> r(ext::make_shared<FlatForward>(0, TARGET(), rVal, Actual360()));
272
0
    Handle<BlackVolTermStructure> vol(ext::make_shared<BlackConstantVol>(0, TARGET(), volVal, Actual360()));
273
274
275
0
    ext::shared_ptr<GeneralizedBlackScholesProcess> process =
276
0
        ext::make_shared<GeneralizedBlackScholesProcess>(spot, q, r, vol);
277
0
    tempOption.setPricingEngine(ext::make_shared<AnalyticBarrierEngine>(process));
278
279
0
    Real npv = tempOption.NPV();
280
0
    Real result = std::max(npv, 0.0);
281
0
    return result;
282
0
}
283
284
}
285