Coverage Report

Created: 2026-06-23 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/pricingengines/bond/discretizedconvertible.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2005, 2006 Theo Boafo
5
 Copyright (C) 2006, 2007 StatPro Italia srl
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
 <https://www.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/pricingengines/bond/discretizedconvertible.hpp>
22
#include <ql/math/comparison.hpp>
23
#include <ql/processes/blackscholesprocess.hpp>
24
#include <utility>
25
26
namespace QuantLib {
27
28
    DiscretizedConvertible::DiscretizedConvertible(
29
        ConvertibleBond::arguments args,
30
        ext::shared_ptr<GeneralizedBlackScholesProcess> process,
31
        DividendSchedule dividends,
32
        Handle<Quote> creditSpread,
33
        const TimeGrid& grid)
34
0
    : arguments_(std::move(args)), process_(std::move(process)),
35
0
      creditSpread_(std::move(creditSpread)) {
36
37
0
        for (const auto& dividend : dividends) {
38
0
            if (!dividend->hasOccurred(arguments_.settlementDate, false)) {
39
0
                dividends_.push_back(dividend);
40
0
                dividendDates_.push_back(dividend->date());
41
0
            }
42
0
        }
43
44
0
        dividendValues_ = Array(dividends_.size(), 0.0);
45
46
0
        Date settlementDate = process_->riskFreeRate()->referenceDate();
47
0
        for (Size i=0; i<dividends.size(); i++) {
48
0
            if (dividends[i]->date() >= settlementDate) {
49
0
                dividendValues_[i] =
50
0
                    dividends[i]->amount() *
51
0
                    process_->riskFreeRate()->discount(
52
0
                                             dividends[i]->date());
53
0
            }
54
0
        }
55
56
0
        DayCounter dayCounter = process_->riskFreeRate()->dayCounter();
57
0
        Date bondSettlement = arguments_.settlementDate;
58
59
0
        stoppingTimes_.resize(arguments_.exercise->dates().size());
60
0
        for (Size i=0; i<stoppingTimes_.size(); ++i)
61
0
            stoppingTimes_[i] =
62
0
                dayCounter.yearFraction(bondSettlement,
63
0
                                        arguments_.exercise->date(i));
64
65
0
        callabilityTimes_.resize(arguments_.callabilityDates.size());
66
0
        for (Size i=0; i<callabilityTimes_.size(); ++i)
67
0
            callabilityTimes_[i] =
68
0
                dayCounter.yearFraction(bondSettlement,
69
0
                                        arguments_.callabilityDates[i]);
70
71
0
        couponTimes_.clear();
72
0
        couponAmounts_.clear();
73
0
        for (Size i = 0; i < arguments_.cashflows.size() - 1; ++i) {
74
0
            if (!arguments_.cashflows[i]->hasOccurred(bondSettlement, false)) {
75
0
                couponTimes_.push_back(dayCounter.yearFraction(bondSettlement,
76
0
                                                               arguments_.cashflows[i]->date()));
77
0
                couponAmounts_.push_back(arguments_.cashflows[i]->amount());
78
0
            }
79
0
        }
80
81
0
        dividendTimes_.resize(dividendDates_.size());
82
0
        for (Size i=0; i<dividendTimes_.size(); ++i)
83
0
            dividendTimes_[i] =
84
0
                dayCounter.yearFraction(bondSettlement,
85
0
                                        dividendDates_[i]);
86
87
0
        if (!grid.empty()) {
88
            // adjust times to grid
89
0
            for (Real& stoppingTime : stoppingTimes_)
90
0
                stoppingTime = grid.closestTime(stoppingTime);
91
0
            for (Real& couponTime : couponTimes_)
92
0
                couponTime = grid.closestTime(couponTime);
93
0
            for (Real& callabilityTime : callabilityTimes_)
94
0
                callabilityTime = grid.closestTime(callabilityTime);
95
0
            for (Real& dividendTime : dividendTimes_)
96
0
                dividendTime = grid.closestTime(dividendTime);
97
0
        }
98
0
    }
99
100
0
    void DiscretizedConvertible::reset(Size size) {
101
102
        // Set to bond redemption values
103
0
        values_ = Array(size, arguments_.redemption);
104
105
        // coupon amounts should be added when adjusting
106
        // values_ = Array(size, arguments_.cashFlows.back()->amount());
107
108
0
        conversionProbability_ = Array(size, 0.0);
109
0
        spreadAdjustedRate_ = Array(size, 0.0);
110
111
0
        DayCounter rfdc  = process_->riskFreeRate()->dayCounter();
112
113
        // this takes care of convertibility and conversion probabilities
114
0
        adjustValues();
115
116
0
        Real creditSpread = creditSpread_->value();
117
118
0
        Date exercise = arguments_.exercise->lastDate();
119
120
0
        Rate riskFreeRate =
121
0
            process_->riskFreeRate()->zeroRate(exercise, rfdc,
122
0
                                               Continuous, NoFrequency);
123
124
        // Calculate blended discount rate to be used on roll back.
125
0
        for (Size j=0; j<values_.size(); j++) {
126
0
           spreadAdjustedRate_[j] =
127
0
               conversionProbability_[j] * riskFreeRate +
128
0
               (1-conversionProbability_[j])*(riskFreeRate + creditSpread);
129
0
        }
130
0
    }
131
132
0
    void DiscretizedConvertible::postAdjustValuesImpl() {
133
134
0
        bool convertible = false;
135
0
        switch (arguments_.exercise->type()) {
136
0
          case Exercise::American:
137
0
            if (time() <= stoppingTimes_[1] && time() >= stoppingTimes_[0])
138
0
                convertible = true;
139
0
            break;
140
0
          case Exercise::European:
141
0
            if (isOnTime(stoppingTimes_[0]))
142
0
                convertible = true;
143
0
            break;
144
0
          case Exercise::Bermudan:
145
0
              for (Real stoppingTime : stoppingTimes_) {
146
0
                  if (isOnTime(stoppingTime))
147
0
                      convertible = true;
148
0
              }
149
0
            break;
150
0
          default:
151
0
            QL_FAIL("invalid option type");
152
0
        }
153
154
0
        for (Size i=0; i<callabilityTimes_.size(); i++) {
155
0
            if (isOnTime(callabilityTimes_[i]))
156
0
                applyCallability(i,convertible);
157
0
        }
158
159
0
        for (Size i=0; i<couponTimes_.size(); i++) {
160
0
            if (isOnTime(couponTimes_[i]))
161
0
                addCoupon(i);
162
0
        }
163
164
0
        if (convertible)
165
0
            applyConvertibility();
166
0
    }
167
168
0
    void DiscretizedConvertible::applyConvertibility() {
169
0
        Array grid = adjustedGrid();
170
0
        for (Size j=0; j<values_.size(); j++) {
171
0
            Real payoff = arguments_.conversionRatio*grid[j];
172
0
            if (values_[j] <= payoff) {
173
0
                values_[j] = payoff;
174
0
                conversionProbability_[j] = 1.0;
175
0
            }
176
0
        }
177
0
    }
178
179
0
    void DiscretizedConvertible::applyCallability(Size i, bool convertible) {
180
0
        Size j;
181
0
        Array grid = adjustedGrid();
182
0
        switch (arguments_.callabilityTypes[i]) {
183
0
          case Callability::Call:
184
0
            if (arguments_.callabilityTriggers[i] != Null<Real>()) {
185
0
                Real conversionValue =
186
0
                    arguments_.redemption/arguments_.conversionRatio;
187
0
                Real trigger =
188
0
                    conversionValue*arguments_.callabilityTriggers[i];
189
0
                for (j=0; j<values_.size(); j++) {
190
                    // the callability is conditioned by the trigger...
191
0
                    if (grid[j] >= trigger) {
192
                        // ...and might trigger conversion
193
0
                        values_[j] =
194
0
                            std::min(std::max(
195
0
                                          arguments_.callabilityPrices[i],
196
0
                                          arguments_.conversionRatio*grid[j]),
197
0
                                     values_[j]);
198
0
                    }
199
0
                }
200
0
            } else if (convertible) {
201
0
                for (j=0; j<values_.size(); j++) {
202
                    // exercising the callability might trigger conversion
203
0
                    values_[j] =
204
0
                        std::min(std::max(arguments_.callabilityPrices[i],
205
0
                                          arguments_.conversionRatio*grid[j]),
206
0
                                 values_[j]);
207
0
                }
208
0
            } else {
209
0
                for (j=0; j<values_.size(); j++) {
210
0
                    values_[j] = std::min(arguments_.callabilityPrices[i],
211
0
                                          values_[j]);
212
0
                }
213
0
            }
214
0
            break;
215
0
          case Callability::Put:
216
0
            for (j=0; j<values_.size(); j++) {
217
0
                values_[j] = std::max(values_[j],
218
0
                                      arguments_.callabilityPrices[i]);
219
0
            }
220
0
            break;
221
0
          default:
222
0
            QL_FAIL("unknown callability type");
223
0
        }
224
0
    }
225
226
0
    void DiscretizedConvertible::addCoupon(Size i) {
227
0
        values_ += couponAmounts_[i];
228
0
    }
229
230
0
    Array DiscretizedConvertible::adjustedGrid() const {
231
0
        Time t = time();
232
0
        Array grid = method()->grid(t);
233
        // add back all dividend amounts in the future
234
0
        for (Size i=0; i<dividends_.size(); i++) {
235
0
            Time dividendTime = dividendTimes_[i];
236
0
            if (dividendTime >= t || close(dividendTime,t)) {
237
0
                const ext::shared_ptr<Dividend>& d = dividends_[i];
238
0
                DiscountFactor dividendDiscount =
239
0
                    process_->riskFreeRate()->discount(dividendTime) /
240
0
                    process_->riskFreeRate()->discount(t);
241
0
                for (Real& j : grid)
242
0
                    j += d->amount(j) * dividendDiscount;
243
0
            }
244
0
        }
245
0
        return grid;
246
0
    }
247
248
}
249