Coverage Report

Created: 2026-10-31 06:47

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