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