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 <http://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
32namespace 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 : swap_(std::move(swap)), termStructure_(std::move(termStructure)),
43 volatilityStructure_(std::move(volatilityStructure)) {
44
45 engine_ = ext::shared_ptr<PricingEngine>(new DiscountingSwapEngine(termStructure_));
46
47 // store swap npv
48 swap_->setPricingEngine(engine_);
49 targetNPV_ = swap_->NPV();
50
51 // build standard swaps
52
53 const Leg& fixedLeg = swap_->fixedLeg();
54 const Leg& floatLeg = swap_->floatingLeg();
55
56 Leg fixedCFS, floatCFS;
57
58 for (Size i = 0; i < fixedLeg.size(); ++i) {
59 // retrieve fixed rate coupon from fixed leg
60 ext::shared_ptr<FixedRateCoupon> coupon =
61 ext::dynamic_pointer_cast<FixedRateCoupon>(r: fixedLeg[i]);
62 QL_REQUIRE(coupon, "dynamic cast of fixed leg coupon failed.");
63
64 expiries_.push_back(x: coupon->date());
65
66 ext::shared_ptr<FixedRateCoupon> newCpn = ext::make_shared<FixedRateCoupon>(
67 args: coupon->date(), args: 1.0, args: coupon->rate(), args: coupon->dayCounter(),
68 args: coupon->accrualStartDate(), args: coupon->accrualEndDate(),
69 args: coupon->referencePeriodStart(), args: coupon->referencePeriodEnd());
70
71 fixedCFS.push_back(x: newCpn);
72
73 annuities_.push_back(x: 10000 * CashFlows::bps(leg: fixedCFS, discountCurve: **termStructure_, includeSettlementDateFlows: true));
74
75 floatCFS.clear();
76
77 for (const auto& j : floatLeg) {
78 // retrieve ibor coupon from floating leg
79 ext::shared_ptr<IborCoupon> coupon = ext::dynamic_pointer_cast<IborCoupon>(r: j);
80 QL_REQUIRE(coupon, "dynamic cast of float leg coupon failed.");
81
82 if (coupon->date() <= expiries_[i]) {
83 ext::shared_ptr<IborCoupon> newCpn = ext::make_shared<IborCoupon>(
84 args: coupon->date(), args: 1.0, args: coupon->accrualStartDate(), args: coupon->accrualEndDate(),
85 args: coupon->fixingDays(), args: coupon->iborIndex(), args: 1.0, args: coupon->spread(),
86 args: coupon->referencePeriodStart(), args: coupon->referencePeriodEnd(),
87 args: coupon->dayCounter(), args: coupon->isInArrears());
88
89
90 if (!newCpn->isInArrears())
91 newCpn->setPricer(
92 ext::shared_ptr<FloatingRateCouponPricer>(new BlackIborCouponPricer()));
93
94 floatCFS.push_back(x: newCpn);
95 }
96 }
97
98 Real floatLegNPV = CashFlows::npv(leg: floatCFS, discountCurve: **termStructure_, includeSettlementDateFlows: true);
99
100 fairRates_.push_back(x: floatLegNPV / annuities_[i]);
101 }
102 }
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 Array HaganIrregularSwaptionEngine::Basket::compute(Rate lambda) const {
108
109 // update members
110 lambda_ = lambda;
111
112 Size n = swap_->fixedLeg().size();
113
114 // build linear system of equations
115 Matrix arr(n, n, 0.0);
116 Array rhs(n);
117
118
119 // fill the matrix describing the linear system of equations by looping over rows
120 for (Size r = 0; r < n; ++r) {
121
122 ext::shared_ptr<FixedRateCoupon> cpn_r =
123 ext::dynamic_pointer_cast<FixedRateCoupon>(r: swap_->fixedLeg()[r]);
124 QL_REQUIRE(cpn_r, "Cast to fixed rate coupon failed.");
125
126 // looping over columns
127 for (Size c = r; c < n; ++c) {
128
129 // set homogenous part of lse
130 arr[r][c] = (fairRates_[c] + lambda_) * cpn_r->accrualPeriod();
131 }
132
133 // add nominal repayment for i-th swap
134 arr[r][r] += 1;
135 }
136
137
138 for (Size r = 0; r < n; ++r) {
139 ext::shared_ptr<FixedRateCoupon> cpn_r =
140 ext::dynamic_pointer_cast<FixedRateCoupon>(r: swap_->fixedLeg()[r]);
141
142 // set inhomogenity of lse
143 Real N_r = cpn_r->nominal();
144
145 if (r < n - 1) {
146
147 ext::shared_ptr<FixedRateCoupon> cpn_rp1 =
148 ext::dynamic_pointer_cast<FixedRateCoupon>(r: swap_->fixedLeg()[r + 1]);
149
150 Real N_rp1 = cpn_rp1->nominal();
151
152 rhs[r] = N_r * (cpn_r->rate()) * cpn_r->accrualPeriod() + (N_r - N_rp1);
153
154 } else {
155
156 rhs[r] = N_r * (cpn_r->rate()) * cpn_r->accrualPeriod() + N_r;
157 }
158 }
159
160
161 SVD svd(arr);
162
163 return svd.solveFor(rhs);
164 }
165
166
167 Real HaganIrregularSwaptionEngine::Basket::operator()(Rate lambda) const {
168
169 Array weights = compute(lambda);
170
171 Real defect = -targetNPV_;
172
173 for (Size i = 0; i < weights.size(); ++i)
174 defect -= Integer(swap_->type()) * lambda * weights[i] * annuities_[i];
175
176 return defect;
177 }
178
179
180 // creates a standard swap by deducing its conventions from market data objects
181 ext::shared_ptr<VanillaSwap> HaganIrregularSwaptionEngine::Basket::component(Size i) const {
182
183 ext::shared_ptr<IborCoupon> iborCpn =
184 ext::dynamic_pointer_cast<IborCoupon>(r: swap_->floatingLeg()[0]);
185 QL_REQUIRE(iborCpn, "dynamic cast of float leg coupon failed. Can't find index.");
186 ext::shared_ptr<IborIndex> iborIndex = iborCpn->iborIndex();
187
188
189 Period dummySwapLength = Period(1, Years);
190
191 ext::shared_ptr<VanillaSwap> memberSwap_ =
192 MakeVanillaSwap(dummySwapLength, iborIndex)
193 .withType(type: swap_->type())
194 .withEffectiveDate(swap_->startDate())
195 .withTerminationDate(expiries_[i])
196 .withRule(r: DateGeneration::Backward)
197 .withDiscountingTermStructure(discountCurve: termStructure_);
198
199 Real stdAnnuity = 10000 * CashFlows::bps(leg: memberSwap_->fixedLeg(), discountCurve: **termStructure_, includeSettlementDateFlows: true);
200
201 // compute annuity transformed rate
202 Rate transformedRate = (fairRates_[i] + lambda_) * annuities_[i] / stdAnnuity;
203
204 memberSwap_ = MakeVanillaSwap(dummySwapLength, iborIndex, transformedRate)
205 .withType(type: swap_->type())
206 .withEffectiveDate(swap_->startDate())
207 .withTerminationDate(expiries_[i])
208 .withRule(r: DateGeneration::Backward)
209 .withDiscountingTermStructure(discountCurve: termStructure_);
210
211
212 return memberSwap_;
213 }
214
215
216 ///////////////////////////////////////////////////////////
217 // Implementation of class HaganIrregularSwaptionEngine //
218 ///////////////////////////////////////////////////////////
219
220
221 HaganIrregularSwaptionEngine::HaganIrregularSwaptionEngine(
222 Handle<SwaptionVolatilityStructure> volatilityStructure,
223 Handle<YieldTermStructure> termStructure)
224 : termStructure_(std::move(termStructure)),
225 volatilityStructure_(std::move(volatilityStructure)) {
226 registerWith(h: termStructure_);
227 registerWith(h: volatilityStructure_);
228 }
229
230
231 void HaganIrregularSwaptionEngine::calculate() const {
232
233 // check exercise type
234 ext::shared_ptr<Exercise> exercise_ = this->arguments_.exercise;
235 QL_REQUIRE(exercise_->type() == QuantLib::Exercise::European, "swaption must be european");
236
237 // extract the underlying irregular swap
238 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 Leg fixedLeg = swap_->fixedLeg();
244 Real fxdLgBPS = CashFlows::bps(leg: fixedLeg, discountCurve: **termStructure_, includeSettlementDateFlows: true);
245
246 Leg floatLeg = swap_->floatingLeg();
247 Real fltLgNPV = CashFlows::npv(leg: floatLeg, discountCurve: **termStructure_, includeSettlementDateFlows: true);
248 Real fltLgBPS = CashFlows::bps(leg: floatLeg, discountCurve: **termStructure_, includeSettlementDateFlows: true);
249
250
251 Leg floatCFS, fixedCFS;
252
253 floatCFS.clear();
254
255 for (auto& j : floatLeg) {
256 // retrieve ibor coupon from floating leg
257 ext::shared_ptr<IborCoupon> coupon = ext::dynamic_pointer_cast<IborCoupon>(r: j);
258 QL_REQUIRE(coupon, "dynamic cast of float leg coupon failed.");
259
260 ext::shared_ptr<IborCoupon> newCpn = ext::make_shared<IborCoupon>(
261 args: coupon->date(), args: coupon->nominal(), args: coupon->accrualStartDate(),
262 args: coupon->accrualEndDate(), args: coupon->fixingDays(), args: coupon->iborIndex(),
263 args: coupon->gearing(), args: 0.0, args: coupon->referencePeriodStart(),
264 args: coupon->referencePeriodEnd(), args: coupon->dayCounter(), args: coupon->isInArrears());
265
266
267 if (!newCpn->isInArrears())
268 newCpn->setPricer(
269 ext::shared_ptr<FloatingRateCouponPricer>(new BlackIborCouponPricer()));
270
271 floatCFS.push_back(x: newCpn);
272 }
273
274
275 Real sprdLgNPV = fltLgNPV - CashFlows::npv(leg: floatCFS, discountCurve: **termStructure_, includeSettlementDateFlows: true);
276 Rate avgSpread = sprdLgNPV / fltLgBPS / 10000;
277
278 Rate cpn_adjustment = avgSpread * fltLgBPS / fxdLgBPS;
279
280 fixedCFS.clear();
281
282 for (auto& i : fixedLeg) {
283 // retrieve fixed rate coupon from fixed leg
284 ext::shared_ptr<FixedRateCoupon> coupon = ext::dynamic_pointer_cast<FixedRateCoupon>(r: i);
285 QL_REQUIRE(coupon, "dynamic cast of fixed leg coupon failed.");
286
287 ext::shared_ptr<FixedRateCoupon> newCpn = ext::make_shared<FixedRateCoupon>(
288 args: coupon->date(), args: coupon->nominal(), args: coupon->rate() - cpn_adjustment,
289 args: coupon->dayCounter(), args: coupon->accrualStartDate(), args: coupon->accrualEndDate(),
290 args: coupon->referencePeriodStart(), args: coupon->referencePeriodEnd());
291
292 fixedCFS.push_back(x: newCpn);
293 }
294
295
296 // this is the irregular swap with spread removed
297 swap_ = ext::make_shared<IrregularSwap>(args: arguments_.swap->type(), args&: fixedCFS, args&: 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 Basket basket(swap_, termStructure_, volatilityStructure_);
303
304
305 ///////////////////////////////////////////////////////////////////////////////////////////////////
306 // find lambda //
307 ///////////////////////////////////////////////////////////////////////////////////////////////////
308
309 Bisection s1d;
310
311 Rate minLambda = -0.5;
312 Rate maxLambda = 0.5;
313 s1d.setMaxEvaluations(10000);
314 s1d.setLowerBound(minLambda);
315 s1d.setUpperBound(maxLambda);
316 s1d.solve(f: basket, accuracy: 1.0e-8, guess: 0.01, xMin: minLambda, xMax: 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 results_.value = HKPrice(basket, exercise&: exercise_);
326 }
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 ext::shared_ptr<Exercise>& exercise) const {
336
337 // Swaption Engine: assumes that the swaptions exercise date equals the swap start date
338 QL_REQUIRE((volatilityStructure_->volatilityType() == Normal),
339 "swaptionEngine: only normal volatility implemented.");
340
341
342 ext::shared_ptr<PricingEngine> swaptionEngine = ext::shared_ptr<PricingEngine>(
343 new BachelierSwaptionEngine(termStructure_, volatilityStructure_));
344
345
346 // retrieve weights of underlying swaps
347 Array weights = basket.weights();
348
349 Real npv = 0.0;
350
351 for (Size i = 0; i < weights.size(); ++i) {
352 ext::shared_ptr<VanillaSwap> pvSwap_ = basket.component(i);
353 Swaption swaption = Swaption(pvSwap_, exercise);
354 swaption.setPricingEngine(swaptionEngine);
355 npv += weights[i] * swaption.NPV();
356 }
357
358 return npv;
359 }
360
361
362}
363

source code of quantlib/ql/experimental/swaptions/haganirregularswaptionengine.cpp