/src/quantlib/ql/experimental/swaptions/haganirregularswaptionengine.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) 2011, 2012, 2013, 2023 Andre Miemiec |
5 | | Copyright (C) 2012 Samuel Tebege |
6 | | This file is part of QuantLib, a free-software/open-source library |
7 | | for financial quantitative analysts and developers - http://quantlib.org/ |
8 | | QuantLib is free software: you can redistribute it and/or modify it |
9 | | under the terms of the QuantLib license. You should have received a |
10 | | copy of the license along with this program; if not, please email |
11 | | <quantlib-dev@lists.sf.net>. The license is also available online at |
12 | | <https://www.quantlib.org/license.shtml>. |
13 | | This program is distributed in the hope that it will be useful, but WITHOUT |
14 | | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
15 | | FOR A PARTICULAR PURPOSE. See the license for more details. |
16 | | */ |
17 | | |
18 | | #include <ql/cashflows/cashflows.hpp> |
19 | | #include <ql/cashflows/couponpricer.hpp> |
20 | | #include <ql/exercise.hpp> |
21 | | #include <ql/experimental/swaptions/haganirregularswaptionengine.hpp> |
22 | | #include <ql/instruments/swaption.hpp> |
23 | | #include <ql/math/distributions/normaldistribution.hpp> |
24 | | #include <ql/math/interpolations/linearinterpolation.hpp> |
25 | | #include <ql/math/matrixutilities/svd.hpp> |
26 | | #include <ql/math/solvers1d/bisection.hpp> |
27 | | #include <ql/math/solvers1d/brent.hpp> |
28 | | #include <ql/pricingengines/swap/discountingswapengine.hpp> |
29 | | #include <ql/pricingengines/swaption/blackswaptionengine.hpp> |
30 | | #include <utility> |
31 | | |
32 | | namespace QuantLib { |
33 | | |
34 | | ////////////////////////////////////////////////////////////////////////// |
35 | | // Implementation of helper class HaganIrregularSwaptionEngine::Basket // |
36 | | ////////////////////////////////////////////////////////////////////////// |
37 | | |
38 | | HaganIrregularSwaptionEngine::Basket::Basket( |
39 | | ext::shared_ptr<IrregularSwap> swap, |
40 | | Handle<YieldTermStructure> termStructure, |
41 | | Handle<SwaptionVolatilityStructure> volatilityStructure) |
42 | 0 | : swap_(std::move(swap)), termStructure_(std::move(termStructure)), |
43 | 0 | volatilityStructure_(std::move(volatilityStructure)) { |
44 | |
|
45 | 0 | engine_ = ext::shared_ptr<PricingEngine>(new DiscountingSwapEngine(termStructure_)); |
46 | | |
47 | | // store swap npv |
48 | 0 | swap_->setPricingEngine(engine_); |
49 | 0 | targetNPV_ = swap_->NPV(); |
50 | | |
51 | | // build standard swaps |
52 | |
|
53 | 0 | const Leg& fixedLeg = swap_->fixedLeg(); |
54 | 0 | const Leg& floatLeg = swap_->floatingLeg(); |
55 | |
|
56 | 0 | Leg fixedCFS, floatCFS; |
57 | |
|
58 | 0 | for (Size i = 0; i < fixedLeg.size(); ++i) { |
59 | | // retrieve fixed rate coupon from fixed leg |
60 | 0 | ext::shared_ptr<FixedRateCoupon> coupon = |
61 | 0 | ext::dynamic_pointer_cast<FixedRateCoupon>(fixedLeg[i]); |
62 | 0 | QL_REQUIRE(coupon, "dynamic cast of fixed leg coupon failed."); |
63 | | |
64 | 0 | expiries_.push_back(coupon->date()); |
65 | |
|
66 | 0 | ext::shared_ptr<FixedRateCoupon> newCpn = ext::make_shared<FixedRateCoupon>( |
67 | 0 | coupon->date(), 1.0, coupon->rate(), coupon->dayCounter(), |
68 | 0 | coupon->accrualStartDate(), coupon->accrualEndDate(), |
69 | 0 | coupon->referencePeriodStart(), coupon->referencePeriodEnd()); |
70 | |
|
71 | 0 | fixedCFS.push_back(newCpn); |
72 | |
|
73 | 0 | annuities_.push_back(10000 * CashFlows::bps(fixedCFS, **termStructure_, true)); |
74 | |
|
75 | 0 | floatCFS.clear(); |
76 | |
|
77 | 0 | for (const auto& j : floatLeg) { |
78 | | // retrieve ibor coupon from floating leg |
79 | 0 | ext::shared_ptr<IborCoupon> coupon = ext::dynamic_pointer_cast<IborCoupon>(j); |
80 | 0 | QL_REQUIRE(coupon, "dynamic cast of float leg coupon failed."); |
81 | | |
82 | 0 | if (coupon->date() <= expiries_[i]) { |
83 | 0 | ext::shared_ptr<IborCoupon> newCpn = ext::make_shared<IborCoupon>( |
84 | 0 | coupon->date(), 1.0, coupon->accrualStartDate(), coupon->accrualEndDate(), |
85 | 0 | coupon->fixingDays(), coupon->iborIndex(), 1.0, coupon->spread(), |
86 | 0 | coupon->referencePeriodStart(), coupon->referencePeriodEnd(), |
87 | 0 | coupon->dayCounter(), coupon->isInArrears()); |
88 | | |
89 | |
|
90 | 0 | if (!newCpn->isInArrears()) |
91 | 0 | newCpn->setPricer( |
92 | 0 | ext::shared_ptr<FloatingRateCouponPricer>(new BlackIborCouponPricer())); |
93 | |
|
94 | 0 | floatCFS.push_back(newCpn); |
95 | 0 | } |
96 | 0 | } |
97 | | |
98 | 0 | Real floatLegNPV = CashFlows::npv(floatCFS, **termStructure_, true); |
99 | |
|
100 | 0 | fairRates_.push_back(floatLegNPV / annuities_[i]); |
101 | 0 | } |
102 | 0 | } |
103 | | |
104 | | |
105 | | // computes a replication of the swap in terms of a basket of vanilla swaps |
106 | | // by solving a linear system of equation |
107 | 0 | Array HaganIrregularSwaptionEngine::Basket::compute(Rate lambda) const { |
108 | | |
109 | | // update members |
110 | 0 | lambda_ = lambda; |
111 | |
|
112 | 0 | Size n = swap_->fixedLeg().size(); |
113 | | |
114 | | // build linear system of equations |
115 | 0 | Matrix arr(n, n, 0.0); |
116 | 0 | Array rhs(n); |
117 | | |
118 | | |
119 | | // fill the matrix describing the linear system of equations by looping over rows |
120 | 0 | for (Size r = 0; r < n; ++r) { |
121 | |
|
122 | 0 | ext::shared_ptr<FixedRateCoupon> cpn_r = |
123 | 0 | ext::dynamic_pointer_cast<FixedRateCoupon>(swap_->fixedLeg()[r]); |
124 | 0 | QL_REQUIRE(cpn_r, "Cast to fixed rate coupon failed."); |
125 | | |
126 | | // looping over columns |
127 | 0 | for (Size c = r; c < n; ++c) { |
128 | | |
129 | | // set homogenous part of lse |
130 | 0 | arr[r][c] = (fairRates_[c] + lambda_) * cpn_r->accrualPeriod(); |
131 | 0 | } |
132 | | |
133 | | // add nominal repayment for i-th swap |
134 | 0 | arr[r][r] += 1; |
135 | 0 | } |
136 | | |
137 | | |
138 | 0 | for (Size r = 0; r < n; ++r) { |
139 | 0 | ext::shared_ptr<FixedRateCoupon> cpn_r = |
140 | 0 | ext::dynamic_pointer_cast<FixedRateCoupon>(swap_->fixedLeg()[r]); |
141 | | |
142 | | // set inhomogenity of lse |
143 | 0 | Real N_r = cpn_r->nominal(); |
144 | |
|
145 | 0 | if (r < n - 1) { |
146 | |
|
147 | 0 | ext::shared_ptr<FixedRateCoupon> cpn_rp1 = |
148 | 0 | ext::dynamic_pointer_cast<FixedRateCoupon>(swap_->fixedLeg()[r + 1]); |
149 | |
|
150 | 0 | Real N_rp1 = cpn_rp1->nominal(); |
151 | |
|
152 | 0 | rhs[r] = N_r * (cpn_r->rate()) * cpn_r->accrualPeriod() + (N_r - N_rp1); |
153 | |
|
154 | 0 | } else { |
155 | |
|
156 | 0 | rhs[r] = N_r * (cpn_r->rate()) * cpn_r->accrualPeriod() + N_r; |
157 | 0 | } |
158 | 0 | } |
159 | | |
160 | |
|
161 | 0 | SVD svd(arr); |
162 | |
|
163 | 0 | return svd.solveFor(rhs); |
164 | 0 | } |
165 | | |
166 | | |
167 | 0 | Real HaganIrregularSwaptionEngine::Basket::operator()(Rate lambda) const { |
168 | |
|
169 | 0 | Array weights = compute(lambda); |
170 | |
|
171 | 0 | Real defect = -targetNPV_; |
172 | |
|
173 | 0 | for (Size i = 0; i < weights.size(); ++i) |
174 | 0 | defect -= Integer(swap_->type()) * lambda * weights[i] * annuities_[i]; |
175 | |
|
176 | 0 | return defect; |
177 | 0 | } |
178 | | |
179 | | |
180 | | // creates a standard swap by deducing its conventions from market data objects |
181 | 0 | ext::shared_ptr<VanillaSwap> HaganIrregularSwaptionEngine::Basket::component(Size i) const { |
182 | |
|
183 | 0 | ext::shared_ptr<IborCoupon> iborCpn = |
184 | 0 | ext::dynamic_pointer_cast<IborCoupon>(swap_->floatingLeg()[0]); |
185 | 0 | QL_REQUIRE(iborCpn, "dynamic cast of float leg coupon failed. Can't find index."); |
186 | 0 | ext::shared_ptr<IborIndex> iborIndex = iborCpn->iborIndex(); |
187 | | |
188 | |
|
189 | 0 | Period dummySwapLength = Period(1, Years); |
190 | |
|
191 | 0 | ext::shared_ptr<VanillaSwap> memberSwap_ = |
192 | 0 | MakeVanillaSwap(dummySwapLength, iborIndex) |
193 | 0 | .withType(swap_->type()) |
194 | 0 | .withEffectiveDate(swap_->startDate()) |
195 | 0 | .withTerminationDate(expiries_[i]) |
196 | 0 | .withRule(DateGeneration::Backward) |
197 | 0 | .withDiscountingTermStructure(termStructure_); |
198 | |
|
199 | 0 | Real stdAnnuity = 10000 * CashFlows::bps(memberSwap_->fixedLeg(), **termStructure_, true); |
200 | | |
201 | | // compute annuity transformed rate |
202 | 0 | Rate transformedRate = (fairRates_[i] + lambda_) * annuities_[i] / stdAnnuity; |
203 | |
|
204 | 0 | memberSwap_ = MakeVanillaSwap(dummySwapLength, iborIndex, transformedRate) |
205 | 0 | .withType(swap_->type()) |
206 | 0 | .withEffectiveDate(swap_->startDate()) |
207 | 0 | .withTerminationDate(expiries_[i]) |
208 | 0 | .withRule(DateGeneration::Backward) |
209 | 0 | .withDiscountingTermStructure(termStructure_); |
210 | | |
211 | |
|
212 | 0 | return memberSwap_; |
213 | 0 | } |
214 | | |
215 | | |
216 | | /////////////////////////////////////////////////////////// |
217 | | // Implementation of class HaganIrregularSwaptionEngine // |
218 | | /////////////////////////////////////////////////////////// |
219 | | |
220 | | |
221 | | HaganIrregularSwaptionEngine::HaganIrregularSwaptionEngine( |
222 | | Handle<SwaptionVolatilityStructure> volatilityStructure, |
223 | | Handle<YieldTermStructure> termStructure) |
224 | 0 | : termStructure_(std::move(termStructure)), |
225 | 0 | volatilityStructure_(std::move(volatilityStructure)) { |
226 | 0 | registerWith(termStructure_); |
227 | 0 | registerWith(volatilityStructure_); |
228 | 0 | } |
229 | | |
230 | | |
231 | 0 | void HaganIrregularSwaptionEngine::calculate() const { |
232 | | |
233 | | // check exercise type |
234 | 0 | ext::shared_ptr<Exercise> exercise_ = this->arguments_.exercise; |
235 | 0 | QL_REQUIRE(exercise_->type() == QuantLib::Exercise::European, "swaption must be european"); |
236 | | |
237 | | // extract the underlying irregular swap |
238 | 0 | ext::shared_ptr<IrregularSwap> swap_ = this->arguments_.swap; |
239 | | |
240 | | |
241 | | // Reshuffle spread from float to fixed (, i.e. remove spread from float side by finding the |
242 | | // adjusted fixed coupon such that the NPV of the swap stays constant). |
243 | 0 | Leg fixedLeg = swap_->fixedLeg(); |
244 | 0 | Real fxdLgBPS = CashFlows::bps(fixedLeg, **termStructure_, true); |
245 | |
|
246 | 0 | Leg floatLeg = swap_->floatingLeg(); |
247 | 0 | Real fltLgNPV = CashFlows::npv(floatLeg, **termStructure_, true); |
248 | 0 | Real fltLgBPS = CashFlows::bps(floatLeg, **termStructure_, true); |
249 | | |
250 | |
|
251 | 0 | Leg floatCFS, fixedCFS; |
252 | |
|
253 | 0 | floatCFS.clear(); |
254 | |
|
255 | 0 | for (auto& j : floatLeg) { |
256 | | // retrieve ibor coupon from floating leg |
257 | 0 | ext::shared_ptr<IborCoupon> coupon = ext::dynamic_pointer_cast<IborCoupon>(j); |
258 | 0 | QL_REQUIRE(coupon, "dynamic cast of float leg coupon failed."); |
259 | | |
260 | 0 | ext::shared_ptr<IborCoupon> newCpn = ext::make_shared<IborCoupon>( |
261 | 0 | coupon->date(), coupon->nominal(), coupon->accrualStartDate(), |
262 | 0 | coupon->accrualEndDate(), coupon->fixingDays(), coupon->iborIndex(), |
263 | 0 | coupon->gearing(), 0.0, coupon->referencePeriodStart(), |
264 | 0 | coupon->referencePeriodEnd(), coupon->dayCounter(), coupon->isInArrears()); |
265 | | |
266 | |
|
267 | 0 | if (!newCpn->isInArrears()) |
268 | 0 | newCpn->setPricer( |
269 | 0 | ext::shared_ptr<FloatingRateCouponPricer>(new BlackIborCouponPricer())); |
270 | |
|
271 | 0 | floatCFS.push_back(newCpn); |
272 | 0 | } |
273 | | |
274 | | |
275 | 0 | Real sprdLgNPV = fltLgNPV - CashFlows::npv(floatCFS, **termStructure_, true); |
276 | 0 | Rate avgSpread = sprdLgNPV / fltLgBPS / 10000; |
277 | |
|
278 | 0 | Rate cpn_adjustment = avgSpread * fltLgBPS / fxdLgBPS; |
279 | |
|
280 | 0 | fixedCFS.clear(); |
281 | |
|
282 | 0 | for (auto& i : fixedLeg) { |
283 | | // retrieve fixed rate coupon from fixed leg |
284 | 0 | ext::shared_ptr<FixedRateCoupon> coupon = ext::dynamic_pointer_cast<FixedRateCoupon>(i); |
285 | 0 | QL_REQUIRE(coupon, "dynamic cast of fixed leg coupon failed."); |
286 | | |
287 | 0 | ext::shared_ptr<FixedRateCoupon> newCpn = ext::make_shared<FixedRateCoupon>( |
288 | 0 | coupon->date(), coupon->nominal(), coupon->rate() - cpn_adjustment, |
289 | 0 | coupon->dayCounter(), coupon->accrualStartDate(), coupon->accrualEndDate(), |
290 | 0 | coupon->referencePeriodStart(), coupon->referencePeriodEnd()); |
291 | |
|
292 | 0 | fixedCFS.push_back(newCpn); |
293 | 0 | } |
294 | | |
295 | | |
296 | | // this is the irregular swap with spread removed |
297 | 0 | swap_ = ext::make_shared<IrregularSwap>(arguments_.swap->type(), fixedCFS, floatCFS); |
298 | | |
299 | | |
300 | | // Sets up the basket by implementing the methodology described in |
301 | | // P.S.Hagan "Callable Swaps and Bermudan 'Exercise into Swaptions'" |
302 | 0 | Basket basket(swap_, termStructure_, volatilityStructure_); |
303 | | |
304 | | |
305 | | /////////////////////////////////////////////////////////////////////////////////////////////////// |
306 | | // find lambda // |
307 | | /////////////////////////////////////////////////////////////////////////////////////////////////// |
308 | |
|
309 | 0 | Bisection s1d; |
310 | |
|
311 | 0 | Rate minLambda = -0.5; |
312 | 0 | Rate maxLambda = 0.5; |
313 | 0 | s1d.setMaxEvaluations(10000); |
314 | 0 | s1d.setLowerBound(minLambda); |
315 | 0 | s1d.setUpperBound(maxLambda); |
316 | 0 | s1d.solve(basket, 1.0e-8, 0.01, minLambda, maxLambda); |
317 | | |
318 | | |
319 | | ///////////////////////////////////////////////////////////////////////////////////////////////// |
320 | | // compute the price of the irreg swaption as the sum of the prices of the regular |
321 | | // swaptions // |
322 | | ///////////////////////////////////////////////////////////////////////////////////////////////// |
323 | | |
324 | |
|
325 | 0 | results_.value = HKPrice(basket, exercise_); |
326 | 0 | } |
327 | | |
328 | | |
329 | | ///////////////////////////////////////////////////////////////////////////////////////// |
330 | | // Computes irregular swaption price according to P.J. Hunt, J.E. Kennedy: // |
331 | | // "Implied interest rate pricing models", Finance Stochast. 2, 275-293 (1998) // |
332 | | ///////////////////////////////////////////////////////////////////////////////////////// |
333 | | |
334 | | Real HaganIrregularSwaptionEngine::HKPrice(Basket& basket, |
335 | 0 | ext::shared_ptr<Exercise>& exercise) const { |
336 | | |
337 | | // Swaption Engine: assumes that the swaptions exercise date equals the swap start date |
338 | 0 | QL_REQUIRE((volatilityStructure_->volatilityType() == Normal), |
339 | 0 | "swaptionEngine: only normal volatility implemented."); |
340 | | |
341 | | |
342 | 0 | ext::shared_ptr<PricingEngine> swaptionEngine = ext::shared_ptr<PricingEngine>( |
343 | 0 | new BachelierSwaptionEngine(termStructure_, volatilityStructure_)); |
344 | | |
345 | | |
346 | | // retrieve weights of underlying swaps |
347 | 0 | Array weights = basket.weights(); |
348 | |
|
349 | 0 | Real npv = 0.0; |
350 | |
|
351 | 0 | for (Size i = 0; i < weights.size(); ++i) { |
352 | 0 | ext::shared_ptr<VanillaSwap> pvSwap_ = basket.component(i); |
353 | 0 | Swaption swaption = Swaption(pvSwap_, exercise); |
354 | 0 | swaption.setPricingEngine(swaptionEngine); |
355 | 0 | npv += weights[i] * swaption.NPV(); |
356 | 0 | } |
357 | |
|
358 | 0 | return npv; |
359 | 0 | } |
360 | | |
361 | | |
362 | | } |