Coverage Report

Created: 2025-09-04 07:11

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