Coverage Report

Created: 2025-08-11 06:28

/src/quantlib/ql/experimental/callablebonds/callablebond.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) 2008 Allen Kuo
5
 Copyright (C) 2017 BN Algorithms Ltd
6
7
 This file is part of QuantLib, a free-software/open-source library
8
 for financial quantitative analysts and developers - http://quantlib.org/
9
10
 QuantLib is free software: you can redistribute it and/or modify it
11
 under the terms of the QuantLib license.  You should have received a
12
 copy of the license along with this program; if not, please email
13
 <quantlib-dev@lists.sf.net>. The license is also available online at
14
 <http://quantlib.org/license.shtml>.
15
16
 This program is distributed in the hope that it will be useful, but WITHOUT
17
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18
 FOR A PARTICULAR PURPOSE.  See the license for more details.
19
*/
20
21
#include <ql/cashflows/cashflowvectors.hpp>
22
#include <ql/experimental/callablebonds/blackcallablebondengine.hpp>
23
#include <ql/experimental/callablebonds/callablebond.hpp>
24
#include <ql/math/solvers1d/brent.hpp>
25
#include <ql/termstructures/yield/zerospreadedtermstructure.hpp>
26
#include <utility>
27
28
namespace QuantLib {
29
30
    CallableBond::CallableBond(Natural settlementDays,
31
                               const Date& maturityDate,
32
                               const Calendar& calendar,
33
                               DayCounter paymentDayCounter,
34
                               Real faceAmount,
35
                               const Date& issueDate,
36
                               CallabilitySchedule putCallSchedule)
37
0
    : Bond(settlementDays, calendar, issueDate),
38
0
      paymentDayCounter_(std::move(paymentDayCounter)),
39
0
      putCallSchedule_(std::move(putCallSchedule)), faceAmount_(faceAmount) {
40
41
0
        maturityDate_ = maturityDate;
42
43
0
        if (!putCallSchedule_.empty()) {
44
0
            Date finalOptionDate = Date::minDate();
45
0
            for (auto& i : putCallSchedule_) {
46
0
                finalOptionDate = std::max(finalOptionDate, i->date());
47
0
            }
48
0
            QL_REQUIRE(finalOptionDate <= maturityDate_ ,
49
0
                       "Bond cannot mature before last call/put date");
50
0
        }
51
52
        // derived classes must set cashflows_ and frequency_
53
0
    }
Unexecuted instantiation: QuantLib::CallableBond::CallableBond(unsigned int, QuantLib::Date const&, QuantLib::Calendar const&, QuantLib::DayCounter, double, QuantLib::Date const&, std::__1::vector<boost::shared_ptr<QuantLib::Callability>, std::__1::allocator<boost::shared_ptr<QuantLib::Callability> > >)
Unexecuted instantiation: QuantLib::CallableBond::CallableBond(unsigned int, QuantLib::Date const&, QuantLib::Calendar const&, QuantLib::DayCounter, double, QuantLib::Date const&, std::__1::vector<boost::shared_ptr<QuantLib::Callability>, std::__1::allocator<boost::shared_ptr<QuantLib::Callability> > >)
54
55
56
0
    void CallableBond::arguments::validate() const {
57
58
0
        QL_REQUIRE(Bond::arguments::settlementDate != Date(),
59
0
                   "null settlement date");
60
61
0
        QL_REQUIRE(redemption != Null<Real>(), "null redemption");
62
0
        QL_REQUIRE(redemption >= 0.0,
63
0
                   "positive redemption required: "
64
0
                   << redemption << " not allowed");
65
66
0
        QL_REQUIRE(callabilityDates.size() == callabilityPrices.size(),
67
0
                   "different number of callability dates and prices");
68
0
        QL_REQUIRE(couponDates.size() == couponAmounts.size(),
69
0
                   "different number of coupon dates and amounts");
70
0
    }
71
72
73
    class CallableBond::ImpliedVolHelper {
74
      public:
75
        ImpliedVolHelper(const CallableBond& bond,
76
                         const Handle<YieldTermStructure>& discountCurve,
77
                         Real targetValue,
78
                         bool matchNPV);
79
        Real operator()(Volatility x) const;
80
      private:
81
        ext::shared_ptr<PricingEngine> engine_;
82
        Real targetValue_;
83
        bool matchNPV_;
84
        ext::shared_ptr<SimpleQuote> vol_;
85
        const CallableBond::results* results_;
86
    };
87
88
    CallableBond::ImpliedVolHelper::ImpliedVolHelper(
89
                              const CallableBond& bond,
90
                              const Handle<YieldTermStructure>& discountCurve,
91
                              Real targetValue,
92
                              bool matchNPV)
93
0
    : targetValue_(targetValue), matchNPV_(matchNPV) {
94
95
0
        vol_ = ext::make_shared<SimpleQuote>(0.0);
96
0
        engine_ = ext::make_shared<BlackCallableFixedRateBondEngine>(Handle<Quote>(vol_),
97
0
                                                                     discountCurve);
98
99
0
        bond.setupArguments(engine_->getArguments());
100
0
        results_ =
101
0
            dynamic_cast<const CallableBond::results*>(engine_->getResults());
102
0
    }
103
104
0
    Real CallableBond::ImpliedVolHelper::operator()(Volatility x) const {
105
0
        vol_->setValue(x);
106
0
        engine_->calculate(); // get the Black NPV based on vol x
107
0
        Real value = matchNPV_ ? results_->value : results_->settlementValue;
108
0
        return value - targetValue_;
109
0
    }
110
111
112
    Volatility CallableBond::impliedVolatility(
113
                              const Bond::Price& targetPrice,
114
                              const Handle<YieldTermStructure>& discountCurve,
115
                              Real accuracy,
116
                              Size maxEvaluations,
117
                              Volatility minVol,
118
0
                              Volatility maxVol) const {
119
0
        QL_REQUIRE(!isExpired(), "instrument expired");
120
121
0
        Real dirtyTargetPrice;
122
0
        switch (targetPrice.type()) {
123
0
          case Bond::Price::Dirty:
124
0
            dirtyTargetPrice = targetPrice.amount();
125
0
            break;
126
0
          case Bond::Price::Clean:
127
0
            dirtyTargetPrice = targetPrice.amount() + accruedAmount();
128
0
            break;
129
0
          default:
130
0
            QL_FAIL("unknown price type");
131
0
        }
132
133
0
        Real targetValue = dirtyTargetPrice * faceAmount_ / 100.0;
134
0
        Volatility guess = 0.5 * (minVol + maxVol);
135
0
        ImpliedVolHelper f(*this, discountCurve, targetValue, false);
136
0
        Brent solver;
137
0
        solver.setMaxEvaluations(maxEvaluations);
138
0
        return solver.solve(f, accuracy, guess, minVol, maxVol);
139
0
    }
140
141
142
    namespace {
143
144
    template<class T>
145
    class RestoreVal { // NOLINT(cppcoreguidelines-special-member-functions)
146
        T orig_;
147
        T &ref_;
148
    public:
149
        explicit RestoreVal(T &ref):
150
0
            orig_(ref),
151
0
            ref_(ref)  { }
152
        ~RestoreVal()
153
0
        {
154
0
            ref_=orig_;
155
0
        }
156
    };
157
158
    class OASHelper {
159
    public:
160
        OASHelper(const std::function<Real(Real)>& npvhelper,
161
                  Real targetValue):
162
0
            npvhelper_(npvhelper),
163
0
            targetValue_(targetValue)
164
0
        {
165
0
        }
166
167
        Real operator()(Spread x) const
168
0
        {
169
0
            return targetValue_ - npvhelper_(x);
170
0
        }
171
    private:
172
        const std::function<Real(Real)>& npvhelper_;
173
        Real targetValue_;
174
    };
175
176
177
    /* Convert a continuous spread to a conventional spread to a
178
       reference yield curve
179
    */
180
    Real continuousToConv(Real oas,
181
                          const Bond &b,
182
                          const Handle<YieldTermStructure>& yts,
183
                          const DayCounter& dayCounter,
184
                          Compounding compounding,
185
                          Frequency frequency)
186
0
    {
187
0
        Real zz=yts->zeroRate(b.maturityDate(),
188
0
                              dayCounter,
189
0
                              Continuous,
190
0
                              NoFrequency);
191
0
        InterestRate baseRate(zz,
192
0
                              dayCounter,
193
0
                              Continuous,
194
0
                              NoFrequency);
195
0
        InterestRate spreadedRate(oas+zz,
196
0
                                  dayCounter,
197
0
                                  Continuous,
198
0
                                  NoFrequency);
199
0
        Real br=baseRate.equivalentRate(dayCounter,
200
0
                                        compounding,
201
0
                                        frequency,
202
0
                                        yts->referenceDate(),
203
0
                                        b.maturityDate()).rate();
204
0
        Real sr=spreadedRate.equivalentRate(dayCounter,
205
0
                                            compounding,
206
0
                                            frequency,
207
0
                                            yts->referenceDate(),
208
0
                                            b.maturityDate()).rate();
209
        // Return the spread
210
0
        return sr-br;
211
0
    }
212
213
    /* Convert a conventional spread to a reference yield curve to a
214
       continuous spread
215
    */
216
    Real convToContinuous(Real oas,
217
                          const Bond &b,
218
                          const Handle<YieldTermStructure>& yts,
219
                          const DayCounter& dayCounter,
220
                          Compounding compounding,
221
                          Frequency frequency)
222
0
    {
223
0
        Real zz=yts->zeroRate(b.maturityDate(),
224
0
                              dayCounter,
225
0
                              compounding,
226
0
                              frequency);
227
0
        InterestRate baseRate(zz,
228
0
                              dayCounter,
229
0
                              compounding,
230
0
                              frequency);
231
232
0
        InterestRate spreadedRate(oas+zz,
233
0
                                  dayCounter,
234
0
                                  compounding,
235
0
                                  frequency);
236
0
        Real br=baseRate.equivalentRate(dayCounter,
237
0
                                        Continuous,
238
0
                                        NoFrequency,
239
0
                                        yts->referenceDate(),
240
0
                                        b.maturityDate()).rate();
241
0
        Real sr=spreadedRate.equivalentRate(dayCounter,
242
0
                                            Continuous,
243
0
                                            NoFrequency,
244
0
                                            yts->referenceDate(),
245
0
                                            b.maturityDate()).rate();
246
        // Return the spread
247
0
        return sr-br;
248
0
    }
249
250
    }
251
252
253
    class CallableBond::NPVSpreadHelper {
254
      public:
255
        explicit NPVSpreadHelper(CallableBond& bond);
256
        Real operator()(Spread x) const;
257
      private:
258
        CallableBond& bond_;
259
        const Instrument::results* results_;
260
    };
261
262
    CallableBond::NPVSpreadHelper::NPVSpreadHelper(CallableBond& bond):
263
0
        bond_(bond),
264
0
        results_(dynamic_cast<const Instrument::results*>(bond.engine_->getResults()))
265
0
    {
266
0
        bond.setupArguments(bond.engine_->getArguments());
267
0
    }
268
269
    Real CallableBond::NPVSpreadHelper::operator()(Real x) const
270
0
    {
271
0
        auto* args = dynamic_cast<CallableBond::arguments*>(bond_.engine_->getArguments());
272
        // Pops the original value when function finishes
273
0
        RestoreVal<Spread> restorer(args->spread);
274
0
        args->spread=x;
275
0
        bond_.engine_->calculate();
276
0
        return results_->value;
277
0
    }
278
279
    Spread CallableBond::OAS(Real cleanPrice,
280
                             const Handle<YieldTermStructure>& engineTS,
281
                             const DayCounter& dayCounter,
282
                             Compounding compounding,
283
                             Frequency frequency,
284
                             Date settlement,
285
                             Real accuracy,
286
                             Size maxIterations,
287
                             Spread guess)
288
0
    {
289
0
        if (settlement == Date())
290
0
            settlement = settlementDate();
291
292
0
        Real dirtyPrice = cleanPrice + accruedAmount(settlement);
293
0
        dirtyPrice /= 100.0 / notional(settlement);
294
295
0
        std::function<Real(Real)> f = NPVSpreadHelper(*this);
296
0
        OASHelper obj(f, dirtyPrice);
297
298
0
        Brent solver;
299
0
        solver.setMaxEvaluations(maxIterations);
300
301
0
        Real step = 0.001;
302
0
        Spread oas=solver.solve(obj, accuracy, guess, step);
303
304
0
        return continuousToConv(oas,
305
0
                                *this,
306
0
                                engineTS,
307
0
                                dayCounter,
308
0
                                compounding,
309
0
                                frequency);
310
0
    }
311
312
313
314
    Real CallableBond::cleanPriceOAS(Real oas,
315
                                     const Handle<YieldTermStructure>& engineTS,
316
                                     const DayCounter& dayCounter,
317
                                     Compounding compounding,
318
                                     Frequency frequency,
319
                                     Date settlement)
320
0
    {
321
0
        if (settlement == Date())
322
0
            settlement = settlementDate();
323
324
0
        oas=convToContinuous(oas,
325
0
                             *this,
326
0
                             engineTS,
327
0
                             dayCounter,
328
0
                             compounding,
329
0
                             frequency);
330
331
0
        std::function<Real(Real)> f = NPVSpreadHelper(*this);
332
333
0
        Real P = f(oas) * 100.0 / notional(settlement) - accruedAmount(settlement);
334
335
0
        return P;
336
0
    }
337
338
    Real CallableBond::effectiveDuration(Real oas,
339
                                         const Handle<YieldTermStructure>& engineTS,
340
                                         const DayCounter& dayCounter,
341
                                         Compounding compounding,
342
                                         Frequency frequency,
343
                                         Real bump)
344
0
    {
345
0
        Real P = cleanPriceOAS(oas,
346
0
                               engineTS,
347
0
                               dayCounter,
348
0
                               compounding,
349
0
                               frequency);
350
351
0
        Real Ppp = cleanPriceOAS(oas+bump,
352
0
                                 engineTS,
353
0
                                 dayCounter,
354
0
                                 compounding,
355
0
                                 frequency);
356
0
        Real Pmm = cleanPriceOAS(oas-bump,
357
0
                                 engineTS,
358
0
                                 dayCounter,
359
0
                                 compounding,
360
0
                                 frequency);
361
362
0
        if ( P == 0.0 )
363
0
            return 0;
364
0
        else
365
0
            {
366
0
                return (Pmm-Ppp)/(2*P*bump);
367
0
            }
368
0
    }
369
370
    Real CallableBond::effectiveConvexity(Real oas,
371
                                          const Handle<YieldTermStructure>& engineTS,
372
                                          const DayCounter& dayCounter,
373
                                          Compounding compounding,
374
                                          Frequency frequency,
375
                                          Real bump)
376
0
    {
377
0
        Real P = cleanPriceOAS(oas,
378
0
                               engineTS,
379
0
                               dayCounter,
380
0
                               compounding,
381
0
                               frequency);
382
383
0
        Real Ppp = cleanPriceOAS(oas+bump,
384
0
                                 engineTS,
385
0
                                 dayCounter,
386
0
                                 compounding,
387
0
                                 frequency);
388
0
        Real Pmm = cleanPriceOAS(oas-bump,
389
0
                                 engineTS,
390
0
                                 dayCounter,
391
0
                                 compounding,
392
0
                                 frequency);
393
394
0
        if ( P == 0.0 )
395
0
            return 0;
396
0
        else
397
0
            {
398
0
                return (Ppp + Pmm - 2*P) / ( std::pow(bump,2) * P);
399
0
            }
400
401
0
    }
402
403
404
0
    void CallableBond::setupArguments(PricingEngine::arguments* args) const {
405
406
0
        Bond::setupArguments(args);
407
408
0
        auto* arguments = dynamic_cast<CallableBond::arguments*>(args);
409
410
0
        QL_REQUIRE(arguments != nullptr, "no arguments given");
411
412
0
        Date settlement = arguments->settlementDate;
413
414
0
        arguments->faceAmount = faceAmount_;
415
0
        arguments->redemption = redemption()->amount();
416
0
        arguments->redemptionDate = redemption()->date();
417
418
0
        const Leg& cfs = cashflows();
419
420
0
        arguments->couponDates.clear();
421
0
        arguments->couponDates.reserve(cfs.size()-1);
422
0
        arguments->couponAmounts.clear();
423
0
        arguments->couponAmounts.reserve(cfs.size()-1);
424
425
0
        for (Size i=0; i<cfs.size()-1; i++) {
426
0
            if (!cfs[i]->hasOccurred(settlement, false)
427
0
                && !cfs[i]->tradingExCoupon(settlement)) {
428
0
                arguments->couponDates.push_back(cfs[i]->date());
429
0
                arguments->couponAmounts.push_back(cfs[i]->amount());
430
0
            }
431
0
        }
432
433
0
        arguments->callabilityPrices.clear();
434
0
        arguments->callabilityDates.clear();
435
0
        arguments->callabilityPrices.reserve(putCallSchedule_.size());
436
0
        arguments->callabilityDates.reserve(putCallSchedule_.size());
437
438
0
        arguments->paymentDayCounter = paymentDayCounter_;
439
0
        arguments->frequency = frequency_;
440
441
0
        arguments->putCallSchedule = putCallSchedule_;
442
0
        for (const auto& i : putCallSchedule_) {
443
0
            if (!i->hasOccurred(settlement, false)) {
444
0
                arguments->callabilityDates.push_back(i->date());
445
0
                arguments->callabilityPrices.push_back(i->price().amount());
446
447
0
                if (i->price().type() == Bond::Price::Clean) {
448
                    /* calling accrued() forces accrued interest to be zero
449
                       if future option date is also coupon date, so that dirty
450
                       price = clean price. Use here because callability is
451
                       always applied before coupon in the tree engine.
452
                    */
453
0
                    arguments->callabilityPrices.back() += this->accrued(i->date());
454
0
                }
455
0
            }
456
0
        }
457
458
0
        arguments->spread = 0.0;
459
0
    }
460
461
462
0
    Real CallableBond::accrued(Date settlement) const {
463
464
0
        if (settlement == Date()) settlement = settlementDate();
465
466
0
        const bool IncludeToday = false;
467
0
        for (const auto& cashflow : cashflows_) {
468
            // the first coupon paying after d is the one we're after
469
0
            if (!cashflow->hasOccurred(settlement, IncludeToday)) {
470
0
                ext::shared_ptr<Coupon> coupon = ext::dynamic_pointer_cast<Coupon>(cashflow);
471
0
                if (coupon != nullptr)
472
                    // !!!
473
0
                    return coupon->accruedAmount(settlement) /
474
0
                           notional(settlement) * 100.0;
475
0
                else
476
0
                    return 0.0;
477
0
            }
478
0
        }
479
0
        return 0.0;
480
0
    }
481
482
483
    CallableFixedRateBond::CallableFixedRateBond(
484
                              Natural settlementDays,
485
                              Real faceAmount,
486
                              Schedule schedule,
487
                              const std::vector<Rate>& coupons,
488
                              const DayCounter& accrualDayCounter,
489
                              BusinessDayConvention paymentConvention,
490
                              Real redemption,
491
                              const Date& issueDate,
492
                              const CallabilitySchedule& putCallSchedule,
493
                              const Period& exCouponPeriod,
494
                              const Calendar& exCouponCalendar,
495
                              BusinessDayConvention exCouponConvention,
496
                              bool exCouponEndOfMonth)
497
0
    : CallableBond(settlementDays, schedule.dates().back(), schedule.calendar(),
498
0
                   accrualDayCounter, faceAmount, issueDate, putCallSchedule) {
499
500
0
        frequency_ = schedule.hasTenor() ? schedule.tenor().frequency() : NoFrequency;
501
502
0
        cashflows_ =
503
0
            FixedRateLeg(std::move(schedule))
504
0
            .withNotionals(faceAmount)
505
0
            .withCouponRates(coupons, accrualDayCounter)
506
0
            .withPaymentAdjustment(paymentConvention)
507
0
            .withExCouponPeriod(exCouponPeriod,
508
0
                                exCouponCalendar,
509
0
                                exCouponConvention,
510
0
                                exCouponEndOfMonth);
511
512
0
        addRedemptionsToCashflows({redemption});
513
0
    }
Unexecuted instantiation: QuantLib::CallableFixedRateBond::CallableFixedRateBond(unsigned int, double, QuantLib::Schedule, std::__1::vector<double, std::__1::allocator<double> > const&, QuantLib::DayCounter const&, QuantLib::BusinessDayConvention, double, QuantLib::Date const&, std::__1::vector<boost::shared_ptr<QuantLib::Callability>, std::__1::allocator<boost::shared_ptr<QuantLib::Callability> > > const&, QuantLib::Period const&, QuantLib::Calendar const&, QuantLib::BusinessDayConvention, bool)
Unexecuted instantiation: QuantLib::CallableFixedRateBond::CallableFixedRateBond(unsigned int, double, QuantLib::Schedule, std::__1::vector<double, std::__1::allocator<double> > const&, QuantLib::DayCounter const&, QuantLib::BusinessDayConvention, double, QuantLib::Date const&, std::__1::vector<boost::shared_ptr<QuantLib::Callability>, std::__1::allocator<boost::shared_ptr<QuantLib::Callability> > > const&, QuantLib::Period const&, QuantLib::Calendar const&, QuantLib::BusinessDayConvention, bool)
514
515
516
    CallableZeroCouponBond::CallableZeroCouponBond(
517
                              Natural settlementDays,
518
                              Real faceAmount,
519
                              const Calendar& calendar,
520
                              const Date& maturityDate,
521
                              const DayCounter& dayCounter,
522
                              BusinessDayConvention paymentConvention,
523
                              Real redemption,
524
                              const Date& issueDate,
525
                              const CallabilitySchedule& putCallSchedule)
526
0
    : CallableBond(settlementDays, maturityDate, calendar,
527
0
                   dayCounter, faceAmount, issueDate, putCallSchedule) {
528
529
0
        frequency_ = Once;
530
531
0
        Date redemptionDate = calendar_.adjust(maturityDate_,
532
0
                                               paymentConvention);
533
0
        setSingleRedemption(faceAmount, redemption, redemptionDate);
534
0
    }
Unexecuted instantiation: QuantLib::CallableZeroCouponBond::CallableZeroCouponBond(unsigned int, double, QuantLib::Calendar const&, QuantLib::Date const&, QuantLib::DayCounter const&, QuantLib::BusinessDayConvention, double, QuantLib::Date const&, std::__1::vector<boost::shared_ptr<QuantLib::Callability>, std::__1::allocator<boost::shared_ptr<QuantLib::Callability> > > const&)
Unexecuted instantiation: QuantLib::CallableZeroCouponBond::CallableZeroCouponBond(unsigned int, double, QuantLib::Calendar const&, QuantLib::Date const&, QuantLib::DayCounter const&, QuantLib::BusinessDayConvention, double, QuantLib::Date const&, std::__1::vector<boost::shared_ptr<QuantLib::Callability>, std::__1::allocator<boost::shared_ptr<QuantLib::Callability> > > const&)
535
536
}