Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
fddefaultableequityjumpdiffusionconvertiblebondengine.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2021 Quaternion Risk Management Ltd.
3 All rights reserved.
4
5 This file is part of ORE, a free-software/open-source library
6 for transparent pricing and risk analysis - http://opensourcerisk.org
7
8 ORE is free software: you can redistribute it and/or modify it
9 under the terms of the Modified BSD License. You should have received a
10 copy of the license along with this program.
11 The license is also available online at <http://opensourcerisk.org>
12
13 This program is distributed on the basis that it will form a useful
14 contribution to risk analytics and model standardisation, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
23
24#include <ql/cashflows/coupon.hpp>
25#include <ql/math/distributions/normaldistribution.hpp>
26#include <ql/math/interpolations/cubicinterpolation.hpp>
27#include <ql/methods/finitedifferences/meshers/fdmmeshercomposite.hpp>
28#include <ql/methods/finitedifferences/meshers/uniform1dmesher.hpp>
29#include <ql/methods/finitedifferences/solvers/fdmbackwardsolver.hpp>
30#include <ql/timegrid.hpp>
31
32namespace QuantExt {
33
34// get amount to be paid on call (or put) exercise dependent on outstanding notional and call details
35
36Real getCallPriceAmount(const FdConvertibleBondEvents::CallData& cd, Real notional, Real accruals) {
37 Real price = cd.price * notional;
39 price += accruals;
40 if (!cd.includeAccrual)
41 price -= accruals;
42 return price;
43}
44
45// interpolate value from PDE planes
46Real interpolateValueFromPlanes(const Real conversionRatio, const std::vector<Array>& value,
47 const std::vector<Real>& stochasticConversionRatios, const Size j) {
48 if (value.size() == 1)
49 return value.front()[j];
50 // linear interpolation and flat extrapolation
51 Size idx = std::distance(
52 stochasticConversionRatios.begin(),
53 std::upper_bound(stochasticConversionRatios.begin(), stochasticConversionRatios.end(), conversionRatio));
54 if (idx == 0)
55 return value.front()[j];
56 else if (idx == stochasticConversionRatios.size())
57 return value.back()[j];
58 else {
59 Real x0 = stochasticConversionRatios[idx - 1];
60 Real x1 = stochasticConversionRatios[idx];
61 Real y0 = value[idx - 1][j];
62 Real y1 = value[idx][j];
63 Real alpha = (x1 - conversionRatio) / (x1 - x0);
64 return alpha * y0 + (1.0 - alpha) * y1;
65 }
66}
67
69 const Handle<DefaultableEquityJumpDiffusionModel>& model,
70 const Handle<QuantLib::YieldTermStructure>& discountingCurve, const Handle<QuantLib::Quote>& discountingSpread,
71 const Handle<QuantLib::DefaultProbabilityTermStructure>& creditCurve, const Handle<QuantLib::Quote>& recoveryRate,
72 const Handle<FxIndex>& fxConversion, const bool staticMesher, const Size timeStepsPerYear,
73 const Size stateGridPoints, const Real mesherEpsilon, const Real mesherScaling,
74 const std::vector<Real> conversionRatioDiscretisationGrid, const bool generateAdditionalResults)
75 : model_(model), discountingCurve_(discountingCurve), discountingSpread_(discountingSpread),
76 creditCurve_(creditCurve), recoveryRate_(recoveryRate), fxConversion_(fxConversion), staticMesher_(staticMesher),
77 timeStepsPerYear_(timeStepsPerYear), stateGridPoints_(stateGridPoints), mesherEpsilon_(mesherEpsilon),
78 mesherScaling_(mesherScaling), conversionRatioDiscretisationGrid_(conversionRatioDiscretisationGrid),
79 generateAdditionalResults_(generateAdditionalResults) {
80 registerWith(model_);
81 registerWith(discountingCurve_);
82 registerWith(discountingSpread_);
83 registerWith(creditCurve_);
84 registerWith(recoveryRate_);
85 registerWith(fxConversion_);
86}
87
89
90 // 0 if there are no cashflows in the underlying bond, we do not calculate anyything
91
92 if (arguments_.cashflows.empty())
93 return;
94
95 // 1 set up events
96
97 Date today = Settings::instance().evaluationDate();
98 FdConvertibleBondEvents events(today, model_->volDayCounter(), arguments_.notionals.front(), model_->equity(),
99 fxConversion_.empty() ? nullptr : *fxConversion_);
100
101 // 1a bond cashflows
102
103 for (Size i = 0; i < arguments_.cashflows.size(); ++i) {
104 if (arguments_.cashflows[i]->date() > today) {
105 events.registerBondCashflow(arguments_.cashflows[i]);
106 }
107 }
108
109 // 1b call and put data
110
111 for (auto const& c : arguments_.callData) {
112 events.registerCall(c);
113 }
114 for (auto const& c : arguments_.putData) {
115 events.registerPut(c);
116 }
117
118 // 1c conversion ratio data
119 for (auto const& c : arguments_.conversionRatioData) {
120 events.registerConversionRatio(c);
121 }
122
123 // 1d conversion data
124
125 for (auto const& c : arguments_.conversionData) {
126 events.registerConversion(c);
127 }
128
129 // 1e mandatory conversion data
130
131 for (auto const& c : arguments_.mandatoryConversionData) {
133 }
134
135 // 1f conversion reset data
136
137 for (auto const& c : arguments_.conversionResetData) {
138 events.registerConversionReset(c);
139 }
140
141 // 1g dividend protection data
142
143 for (auto const& c : arguments_.dividendProtectionData) {
145 }
146
147 // 1h make whole data
148
149 events.registerMakeWhole(arguments_.makeWholeData);
150
151 // 2 set up PDE time grid
152
153 QL_REQUIRE(!events.times().empty(),
154 "FdDefaultableEquityJumpDiffusionConvertibleEngine: internal error, times are empty");
155 Size steps = std::max<Size>(std::lround(timeStepsPerYear_ * (*events.times().rbegin()) + 0.5), 1);
156 TimeGrid grid(events.times().begin(), events.times().end(), steps);
157
158 // 3 build mesher if we do not have one or if we want to rebuild the mesher every time
159
160 Real spot = model_->equity()->equitySpot()->value();
161 Real logSpot = std::log(spot);
162
163 if (mesher_ == nullptr || !staticMesher_) {
164 Real mi = spot;
165 Real ma = spot;
166 Real forward = spot;
167 for (Size i = 1; i < grid.size(); ++i) {
168 forward = spot * model_->equity()->equityDividendCurve()->discount(grid[i]) /
169 model_->equity()->equityForecastCurve()->discount(grid[i]);
170 mi = std::min(mi, forward);
171 ma = std::max(ma, forward);
172 }
173 Real sigmaSqrtT = std::max(1E-2 * std::sqrt(grid.back()), std::sqrt(model_->totalBlackVariance()));
174 Real normInvEps = InverseCumulativeNormal()(1.0 - mesherEpsilon_);
175 Real xMin = std::log(mi) - sigmaSqrtT * normInvEps * mesherScaling_;
176 Real xMax = std::log(ma) + sigmaSqrtT * normInvEps * mesherScaling_;
177
178 mesher_ = QuantLib::ext::make_shared<Uniform1dMesher>(xMin, xMax, stateGridPoints_);
179 }
180
181 // 4 set up functions accrual(t), notional(t), recovery(t, S)
182
183 Real N0 = arguments_.notionals.front();
184 std::vector<Real> notionalTimes = {0.0};
185 std::vector<Real> notionals = {N0};
186 std::vector<Real> couponAmounts, couponAccrualStartTimes, couponAccrualEndTimes, couponPayTimes;
187 for (auto const& c : arguments_.cashflows) {
188 if (c->date() <= today)
189 continue;
190 if (auto cpn = QuantLib::ext::dynamic_pointer_cast<Coupon>(c)) {
191 if (!close_enough(cpn->nominal(), notionals.back())) {
192 notionalTimes.push_back(model_->timeFromReference(c->date()));
193 notionals.push_back(cpn->nominal());
194 }
195 couponAmounts.push_back(cpn->amount());
196 couponAccrualStartTimes.push_back(model_->timeFromReference(cpn->accrualStartDate()));
197 couponAccrualEndTimes.push_back(model_->timeFromReference(cpn->accrualEndDate()));
198 couponPayTimes.push_back(model_->timeFromReference(cpn->date()));
199 }
200 }
201
202 auto notional = [&notionalTimes, &notionals](const Real t) {
203 auto cn = std::upper_bound(notionalTimes.begin(), notionalTimes.end(), t,
204 [](Real s, Real t) { return s < t && !close_enough(s, t); });
205 return notionals[std::max<Size>(std::distance(notionalTimes.begin(), cn), 1) - 1];
206 };
207
208 auto recovery = [this, &notional, N0](const Real t, const Real S, const Real conversionRatio) {
209 Real currentBondNotional = notional(t);
210 Real rr = this->recoveryRate_.empty() ? 0.0 : this->recoveryRate_->value();
211 Real conversionValue = 0.0;
212 if (conversionRatio != Null<Real>())
213 conversionValue = currentBondNotional / N0 * conversionRatio * S * (1.0 - this->model_->eta());
214 if (!this->arguments_.exchangeableData.isExchangeable) {
215 // recovery term for non-exchangeables
216 return std::max(rr * currentBondNotional, conversionValue);
217 } else {
218 // equity-related recovery term for exchangeables (same for secured / non-secured)
219 return currentBondNotional;
220 }
221 };
222
223 std::function<Real(Real, Real, Real)> addRecovery;
224 if (arguments_.exchangeableData.isExchangeable) {
225 addRecovery = [this, &notional, N0](const Real t, const Real S, const Real conversionRatio) {
226 Real currentBondNotional = notional(t);
227 Real rr = this->recoveryRate_.empty() ? 0.0 : this->recoveryRate_->value();
228 Real conversionValue = 0.0;
229 if (conversionRatio != Null<Real>())
230 conversionValue = currentBondNotional / N0 * conversionRatio * S * (1.0 - this->model_->eta());
231 if (!this->arguments_.exchangeableData.isSecured) {
232 // bond-related recovery term for exchangeables / non-secured
233 return rr * currentBondNotional;
234 } else {
235 // bond-related recovery term for exchangeables / secured
236 return conversionValue + rr * std::max(currentBondNotional - conversionValue, 0.0);
237 }
238 };
239 }
240
241 auto accrual = [&couponAmounts, &couponPayTimes, &couponAccrualStartTimes, &couponAccrualEndTimes](const Real t) {
242 Real accruals = 0.0;
243 for (Size i = 0; i < couponAmounts.size(); ++i) {
244 if (couponPayTimes[i] > t && t > couponAccrualStartTimes[i]) {
245 accruals += (t - couponAccrualStartTimes[i]) / (couponAccrualEndTimes[i] - couponAccrualStartTimes[i]) *
246 couponAmounts[i];
247 }
248 }
249 return accruals;
250 };
251
252 // 5 build operator
253
254 auto fdmOp = QuantLib::ext::make_shared<FdmDefaultableEquityJumpDiffusionOp>(
255 QuantLib::ext::make_shared<FdmMesherComposite>(mesher_), *model_, 0, recovery, discountingCurve_, discountingSpread_,
256 creditCurve_, addRecovery);
257
258 // 6 build solver
259
260 auto solver = QuantLib::ext::make_shared<FdmBackwardSolver>(
261 fdmOp, std::vector<QuantLib::ext::shared_ptr<BoundaryCondition<FdmLinearOp>>>(), nullptr, FdmSchemeDesc::Douglas());
262
263 // 7 prepare event container
264
265 events.finalise(grid);
266
267 // 8 set up discretisation grid for conversion ratio (for cr resets and div protection with cr adjustment)
268
269 std::vector<Real> stochasticConversionRatios(1, Null<Real>());
270 if (events.hasStochasticConversionRatio(grid.size() - 1)) {
271 stochasticConversionRatios.resize(conversionRatioDiscretisationGrid_.size());
272 for (Size i = 0; i < conversionRatioDiscretisationGrid_.size(); ++i) {
273 stochasticConversionRatios[i] = events.getInitialConversionRatio() * conversionRatioDiscretisationGrid_[i];
274 }
275 }
276
277 // 9 set boundary value at last grid point
278
279 Size n = mesher_->locations().size();
280 std::vector<Array> value(stochasticConversionRatios.size(), Array(n, 0.0)), valueTmp;
281 std::vector<Array> conversionIndicator;
283 conversionIndicator.resize(stochasticConversionRatios.size(), Array(n, 0.0));
284
285 // 10 add no-conversion variants for start of period coco feature
286
287 std::vector<Array> valueNoConversion, valueNoConversionTmp;
288 std::vector<Array> conversionIndicatorNoConversion;
289
290 // 11 perform the backward PDE pricing
291
292 Array S(mesher_->locations().begin(), mesher_->locations().end());
293 S = Exp(S);
294
295 for (Size i = grid.size() - 1; i > 0; --i) {
296
297 // 11.1 we will roll back from t_i = t_from to t_{i-1} = t_to in this step
298
299 Real t_from = grid[i];
300 Real t_to = grid[i - 1];
301
302 // 11.2 Create the no-conversion value array if required (for contingent conversion)
303
304 if (events.hasNoConversionPlane(i) && valueNoConversion.empty()) {
305 valueNoConversion = value;
306 conversionIndicatorNoConversion = conversionIndicator;
307 }
308
309 // 11.3a handle voluntary (contingent) conversion on t_i (overrides call and put)
310
311 std::vector<std::vector<bool>> conversionExercised(value.size(), std::vector<bool>(n, false));
312 for (Size plane = 0; plane < value.size(); ++plane) {
313 if (events.hasConversion(i)) {
314 Real cr = value.size() > 1 ? stochasticConversionRatios[plane] : events.getCurrentConversionRatio(i);
315 for (Size j = 0; j < n; ++j) {
316 bool cocoTriggered = true;
317 if (events.hasContingentConversion(i) && !events.hasNoConversionPlane(i)) {
318 cocoTriggered = cr * S[j] > events.getConversionData(i).cocoBarrier;
319 // update value from no conversion plane, if there is one and coco is not triggered
320 if (!valueNoConversion.empty()) {
321 if (!cocoTriggered) {
322 value[plane][j] = valueNoConversion[plane][j];
323 if (!conversionIndicator.empty())
324 conversionIndicator[plane][j] = conversionIndicatorNoConversion[plane][j];
325 }
326 }
327 }
328 Real exerciseValue = S[j] * cr * notional(grid[i]) / N0 + accrual(grid[i]);
329 // see 11.9, if we do not exercise, we are entitled to receive the final redempion flow
330 if (cocoTriggered && exerciseValue > value[plane][j] + events.getBondFinalRedemption(i)) {
331 value[plane][j] = exerciseValue;
332 conversionExercised[plane][j] = true;
333 if (!conversionIndicator.empty())
334 conversionIndicator[plane][j] = 1.0;
335 }
336 }
337 }
338 } // for plane
339
340 // 11.3b collapse no conversion plane if adequate
341
342 if (events.hasContingentConversion(i) && !events.hasNoConversionPlane(i) && !valueNoConversion.empty()) {
343 valueNoConversion.clear();
344 conversionIndicatorNoConversion.clear();
345 }
346
347 // 11.4 handle cr / DP induced cr resets and resets to specific value on t_i
348
349 valueTmp = value;
350 if (!valueNoConversion.empty())
351 valueNoConversionTmp = valueNoConversion;
352
353 for (Size plane = 0; plane < value.size(); ++plane) {
354 if (events.hasConversionReset(i)) {
355 // this implies we have several planes with stochasticConversionRatios filled
357 Array adjustedConversionRatio(n, Null<Real>());
358 if (rd.resetActive) {
359 Real cr;
361 cr = value.size() > 1 ? stochasticConversionRatios[plane] : events.getCurrentConversionRatio(i);
362 } else {
363 cr = events.getInitialConversionRatio();
364 }
365 if (close_enough(cr, 0.0)) {
366 std::fill(adjustedConversionRatio.begin(), adjustedConversionRatio.end(), 0.0);
367 } else {
368 Real referenceCP = N0 / cr;
369 for (Size j = 0; j < n; ++j) {
370 if (S[j] < rd.threshold * referenceCP) {
371 adjustedConversionRatio[j] = QL_MAX_REAL;
372 if (!close_enough(rd.gearing, 0.0)) {
373 adjustedConversionRatio[j] =
374 std::min(adjustedConversionRatio[j], N0 / (rd.gearing * S[j]));
375 }
376 if (!close_enough(rd.floor, 0.0)) {
377 adjustedConversionRatio[j] =
378 std::min(adjustedConversionRatio[j], N0 / (rd.floor * referenceCP));
379 }
380 if (!close_enough(rd.globalFloor, 0.0)) {
381 adjustedConversionRatio[j] =
382 std::min(adjustedConversionRatio[j], N0 / (rd.globalFloor * referenceCP));
383 }
384 adjustedConversionRatio[j] =
385 std::max(cr, adjustedConversionRatio[j] != QL_MAX_REAL ? adjustedConversionRatio[j]
386 : -QL_MAX_REAL);
387 }
388 }
389 }
390 }
391 if (rd.divProtActive) {
393 Real H = rd.divThreshold;
394 Real s = grid[rd.lastDividendProtectionTimeIndex + 1];
395 Real t = grid[i];
396 for (Size j = 0; j < n; ++j) {
397 // we might have adjusted the cr already above
398 if (adjustedConversionRatio[j] == Null<Real>()) {
399 adjustedConversionRatio[j] = value.size() > 1 ? stochasticConversionRatios[plane]
400 : events.getCurrentConversionRatio(i);
401 }
402 Real D = rd.accruedHistoricalDividends +
403 (std::exp(model_->dividendYield(s, t) * (t - s)) - 1.0) * S[j];
406 Real d = absolute ? D : D / S[j];
407 Real C = rd.adjustmentStyle ==
409 ? std::max(d - H, 0.0)
410 : d - H;
411 adjustedConversionRatio[j] *= (absolute ? S[j] / std::max(S[j] - C, 1E-4) : (1.0 + C));
412 } else {
413 Real f = std::max(S[j] - H, 0.0) / std::max(S[j] - D, 1E-4);
414 if (rd.adjustmentStyle ==
416 f = std::max(f, 1.0);
417 adjustedConversionRatio[j] *= f;
418 }
419 }
420 }
421 for (Size j = 0; j < n; ++j) {
422 Real lookupValue = adjustedConversionRatio[j];
423 if (rd.resetToSpecificValue) {
424 lookupValue = rd.newCr;
425 }
426 if (lookupValue != Null<Real>()) {
427 // update value by interpolating from other planes if cr was reset on this date
428 valueTmp[plane][j] =
429 interpolateValueFromPlanes(lookupValue, value, stochasticConversionRatios, j);
430 if (!valueNoConversion.empty()) {
431 valueNoConversionTmp[plane][j] = interpolateValueFromPlanes(lookupValue, valueNoConversion,
432 stochasticConversionRatios, j);
433 }
434 }
435 }
436 } // has conversion reset
437 } // for plane (stoch cr)
438
439 value.swap(valueTmp);
440 if (!valueNoConversion.empty())
441 valueNoConversion.swap(valueNoConversionTmp);
442
443 // 11.5 collapse stochastic conversion ratio planes to one plane on t_i if possible
444
445 if (!events.hasStochasticConversionRatio(i) && value.size() > 1) {
446 Array collapsedValue(n);
447 for (Size j = 0; j < n; ++j) {
448 collapsedValue[j] = interpolateValueFromPlanes(events.getCurrentConversionRatio(i), value,
449 stochasticConversionRatios, j);
450 }
451 value = std::vector<Array>(1, collapsedValue);
452 if (!valueNoConversion.empty()) {
453 for (Size j = 0; j < n; ++j) {
454 collapsedValue[j] = interpolateValueFromPlanes(events.getCurrentConversionRatio(i),
455 valueNoConversion, stochasticConversionRatios, j);
456 }
457 valueNoConversion = std::vector<Array>(1, collapsedValue);
458 }
459 if (!conversionIndicator.empty()) {
460 for (Size j = 0; j < n; ++j) {
461 collapsedValue[j] = interpolateValueFromPlanes(events.getCurrentConversionRatio(i),
462 conversionIndicator, stochasticConversionRatios, j);
463 }
464 conversionIndicator = std::vector<Array>(1, collapsedValue);
465 }
466 if (!conversionIndicatorNoConversion.empty()) {
467 for (Size j = 0; j < n; ++j) {
468 collapsedValue[j] =
469 interpolateValueFromPlanes(events.getCurrentConversionRatio(i), conversionIndicatorNoConversion,
470 stochasticConversionRatios, j);
471 }
472 conversionIndicatorNoConversion = std::vector<Array>(1, collapsedValue);
473 }
474 }
475
476 for (Size plane = 0; plane < value.size(); ++plane) {
477
478 // 11.6 handle mandatory conversion (overwrites value from voluntary conversion if on same date)
479
480 if (events.hasMandatoryConversion(i)) {
481 for (Size j = 0; j < n; ++j) {
482 // PEPS
483 const auto& d = events.getMandatoryConversionData(i);
484 Real payoff = 0.0;
485 if (S[j] < d.pepsLowerBarrier) {
486 payoff = d.pepsLowerConversionRatio * S[j] * notional(grid[i]) / N0 + accrual(grid[i]);
487 } else if (S[j] > d.pepsUpperBarrier) {
488 payoff = d.pepsUpperConversionRatio * S[j] * notional(grid[i]) / N0 + accrual(grid[i]);
489 } else {
490 payoff = notional(grid[i]) + accrual(grid[i]);
491 }
492 value[plane][j] = payoff;
493 conversionExercised[plane][j] = true;
494 if (!valueNoConversion.empty())
495 valueNoConversion[plane][j] = payoff;
496 if (!conversionIndicator.empty())
497 conversionIndicator[plane][j] = 1.0;
498 if (!conversionIndicatorNoConversion.empty())
499 conversionIndicatorNoConversion[plane][j] = 1.0;
500 }
501 }
502
503 // 11.7 handle call, put on t_i (assume put overrides call, if both are exercised)
504
505 if (events.hasCall(i)) {
506 Real c = getCallPriceAmount(events.getCallData(i), notional(t_from), accrual(t_from));
507 Real cr0 = value.size() > 1 ? stochasticConversionRatios[plane] : events.getCurrentConversionRatio(i);
508 for (Size j = 0; j < n; ++j) {
509 if (!conversionExercised[plane][j]) {
510 // check soft call trigger if applicable
511 if (!events.getCallData(i).isSoft ||
512 S[j] > events.getCallData(i).softTriggerRatio * notionals.front() / cr0) {
513 // apply mw cr increase if applicable
514 Real cr = cr0;
515 if (events.getCallData(i).mwCr)
516 cr = events.getCallData(i).mwCr(S[j], cr0);
517 // compuate forced conversion value and update npv node
518 Real forcedConversionValue = S[j] * cr * notional(grid[i]) / N0 + accrual(grid[i]);
519 if (forcedConversionValue > c && forcedConversionValue < value[plane][j]) {
520 // conversion is forced -> update flags
521 if (!conversionIndicator.empty())
522 conversionIndicator[plane][j] = 0.0;
523 conversionExercised[plane][j] = false;
524 }
525 value[plane][j] = std::min(std::max(forcedConversionValue, c), value[plane][j]);
526 if (!valueNoConversion.empty()) {
527 valueNoConversion[plane][j] =
528 std::min(std::max(forcedConversionValue, c), valueNoConversion[plane][j]);
529 }
530 }
531 }
532 }
533 }
534
535 if (events.hasPut(i)) {
536 Real c = getCallPriceAmount(events.getPutData(i), notional(t_from), accrual(t_from));
537 for (Size j = 0; j < n; ++j) {
538 if (c > value[plane][j]) {
539 // put is more favorable than conversion (if that happened above)
540 value[plane][j] = c;
541 if (!conversionIndicator.empty())
542 conversionIndicator[plane][j] = 0.0;
543 conversionExercised[plane][j] = false;
544 }
545 }
546 if (!valueNoConversion.empty()) {
547 for (Size j = 0; j < n; ++j) {
548 valueNoConversion[plane][j] = std::max(c, valueNoConversion[plane][j]);
549 }
550 }
551 }
552
553 // 11.8 handle dividend protection pass through on t_i, paid even if converted or called / put
554
555 if (events.hasDividendPassThrough(i)) {
557 Real H = d.divThreshold;
558 Real s = grid[d.lastDividendProtectionTimeIndex + 1];
559 Real t = grid[i];
560 Real cr = value.size() > 1 ? stochasticConversionRatios[plane] : events.getCurrentConversionRatio(i);
561 for (Size j = 0; j < n; ++j) {
562 Real D =
563 d.accruedHistoricalDividends + (std::exp(model_->dividendYield(s, t) * (t - s)) - 1.0) * S[j];
564 Real a = d.adjustmentStyle ==
566 ? std::max(D - H, 0.0)
567 : D - H;
568 value[plane][j] += a * cr;
569 if (!valueNoConversion.empty())
570 valueNoConversion[plane][j] += a * cr;
571 }
572 }
573
574 // 11.9 handle bond cashflows on t_i (after calls / puts)
575
576 if (events.hasBondCashflow(i)) {
577 value[plane] += events.getBondCashflow(i);
578 if (!valueNoConversion.empty())
579 valueNoConversion[plane] += events.getBondCashflow(i);
580 // the final redemption flow is only paid, if no conversion was exercised on the same date
581 // and if the bond is not perpetual
582 for (Size j = 0; j < n; ++j) {
583 if (!conversionExercised[plane][j] && !arguments_.perpetual) {
584 value[plane][j] += events.getBondFinalRedemption(i);
585 if (!valueNoConversion.empty())
586 valueNoConversion[plane] += events.getBondFinalRedemption(i);
587 }
588 }
589 }
590
591 // 11.10 set conversion rate function in operator for rollback
592
593 Real cr = value.size() > 1 ? stochasticConversionRatios[plane] : events.getCurrentConversionRatio(i);
594 fdmOp->setConversionRatio([cr](const Real S) { return cr; });
595
596 // 11.11 roll back value from time t_i to t_i{-1}
597
598 solver->rollback(value[plane], t_from, t_to, 1, 0);
599 if (!valueNoConversion.empty())
600 solver->rollback(valueNoConversion[plane], t_from, t_to, 1, 0);
601 if (!conversionIndicator.empty())
602 solver->rollback(conversionIndicator[plane], t_from, t_to, 1, 0);
603 if (!conversionIndicatorNoConversion.empty())
604 solver->rollback(conversionIndicatorNoConversion[plane], t_from, t_to, 1, 0);
605
606 } // loop over stochastic conversion ratio planes
607
608 } // loop over times (PDE rollback)
609
610 // 12 do a second roll back to compute the bond floor (include final redemption even for perpetuals)
611
612 fdmOp->setConversionRatio([](const Real S) { return Null<Real>(); });
613
614 Array valueBondFloor(n, 0.0);
615 for (Size i = grid.size() - 1; i > 0; --i) {
616 Real t_from = grid[i];
617 Real t_to = grid[i - 1];
618 if (events.hasBondCashflow(i)) {
619 valueBondFloor += events.getBondCashflow(i) + events.getBondFinalRedemption(i);
620 }
621 solver->rollback(valueBondFloor, t_from, t_to, 1, 0);
622 }
623
624 // 13 set result
625
626 QL_REQUIRE(
627 value.size() == 1,
628 "FdDefaultableEquityJumpDiffusionConvertibleEngine: internal error, have "
629 << value.size()
630 << " pde planes after complete rollback, the planes should have been collapsed to one during the rollback");
631
632 MonotonicCubicNaturalSpline interpolationValue(mesher_->locations().begin(), mesher_->locations().end(),
633 value[0].begin());
634 MonotonicCubicNaturalSpline interpolationBondFloor(mesher_->locations().begin(), mesher_->locations().end(),
635 valueBondFloor.begin());
636 interpolationValue.enableExtrapolation();
637 interpolationBondFloor.enableExtrapolation();
638 Real npv = interpolationValue(logSpot);
639 Real npvBondFloor = interpolationBondFloor(logSpot);
640 results_.additionalResults["BondFloor"] = npvBondFloor;
641
642 results_.value = arguments_.detachable ? npv - npvBondFloor : npv;
643
644 results_.settlementValue = results_.value; // FIXME this is not entirely correct of course
645
646 // 14 set additional results, if not disabled
647
649 return;
650
651 // 14.1 output events table
652
653 constexpr Size width = 12;
654 std::ostringstream header;
655 header << std::left << "|" << std::setw(width) << "time"
656 << "|" << std::setw(width) << "date"
657 << "|" << std::setw(width) << "notional"
658 << "|" << std::setw(width) << "accrual"
659 << "|" << std::setw(width) << "flow"
660 << "|" << std::setw(width) << "call"
661 << "|" << std::setw(width) << "put"
662 << "|" << std::setw(2 * width) << "conversion"
663 << "|" << std::setw(2 * width) << "CR_reset"
664 << "|" << std::setw(width) << "div_passth"
665 << "|" << std::setw(width) << "curr_cr"
666 << "|" << std::setw(width) << "fxConv"
667 << "|" << std::setw(width) << "eq_fwd"
668 << "|" << std::setw(width) << "div_amt"
669 << "|" << std::setw(width) << "conv_val"
670 << "|" << std::setw(width) << "conv_prc"
671 << "|";
672
673 results_.additionalResults["event_0000!"] = header.str();
674
675 Size counter = 0;
676 for (Size i = 0; i < grid.size(); ++i) {
677 if (true || events.hasBondCashflow(i) || events.hasCall(i) || events.hasPut(i) || events.hasConversion(i)) {
678 std::ostringstream dateStr, bondFlowStr, callStr, putStr, convStr, convResetStr, divStr, currentConvStr,
679 fxConvStr, eqFwdStr, divAmtStr, convValStr, convPrcStr;
680
681 Real eqFwd = model_->equity()->equitySpot()->value() /
682 model_->equity()->equityForecastCurve()->discount(grid[i]) *
683 model_->equity()->equityDividendCurve()->discount(grid[i]);
684 Real divAmt = 0.0;
685
686 if (events.getAssociatedDate(i) != Null<Date>()) {
687 dateStr << QuantLib::io::iso_date(events.getAssociatedDate(i));
688 }
689 if (events.hasBondCashflow(i)) {
690 bondFlowStr << events.getBondCashflow(i) + events.getBondFinalRedemption(i);
691 }
692 if (events.hasCall(i)) {
693 const auto& cd = events.getCallData(i);
694 callStr << "@" << cd.price;
695 if (cd.isSoft)
696 callStr << " s@" << cd.softTriggerRatio;
697 }
698 if (events.hasPut(i)) {
699 const auto& cd = events.getPutData(i);
700 putStr << "@" << cd.price;
701 }
702 if (events.hasConversion(i)) {
703 convStr << "@" << events.getCurrentConversionRatio(i);
704 if (events.hasContingentConversion(i)) {
705 const auto& cd = events.getConversionData(i);
706 convStr << " c@" << cd.cocoBarrier;
707 if (events.hasNoConversionPlane(i)) {
708 convStr << "b";
709 }
710 }
711 }
712 if (events.hasMandatoryConversion(i)) {
713 const auto& cd = events.getMandatoryConversionData(i);
714 convStr << "peps(" << cd.pepsLowerConversionRatio << "/" << cd.pepsUpperConversionRatio << ")";
715 }
716 if (events.hasConversionReset(i)) {
717 const auto& cd = events.getConversionResetData(i);
718 if (cd.resetToSpecificValue) {
719 convResetStr << "->" << cd.newCr << " ";
720 }
721 if (cd.resetActive) {
722 convResetStr << cd.gearing << "@" << cd.threshold;
724 convResetStr << "/CPt ";
725 else
726 convResetStr << "/CP0 ";
727 }
728 if (cd.divProtActive) {
729 convResetStr << "DP(" << cd.lastDividendProtectionTimeIndex << "/"
730 << model_->dividendYield(grid[cd.lastDividendProtectionTimeIndex], grid[i]) << ")@"
731 << cd.divThreshold;
732 Real s = grid[cd.lastDividendProtectionTimeIndex + 1];
733 Real t = grid[i];
734 divAmt +=
735 cd.accruedHistoricalDividends + (std::exp(model_->dividendYield(s, t) * (t - s)) - 1.0) * eqFwd;
736 }
737 }
738 if (events.hasDividendPassThrough(i)) {
739 const auto& cd = events.getDividendPassThroughData(i);
740 divStr << "@" << cd.divThreshold;
741 Real s = grid[cd.lastDividendProtectionTimeIndex + 1];
742 Real t = grid[i];
743 divAmt +=
744 cd.accruedHistoricalDividends + (std::exp(model_->dividendYield(s, t) * (t - s)) - 1.0) * eqFwd;
745 }
746 if (events.getCurrentConversionRatio(i) == Null<Real>()) {
747 currentConvStr << "NA";
748 } else {
749 currentConvStr << events.getCurrentConversionRatio(i);
750 }
751 if (events.hasStochasticConversionRatio(i)) {
752 currentConvStr << "s";
753 }
754 fxConvStr << events.getCurrentFxConversion(i);
755 eqFwdStr << eqFwd;
756 if (!close_enough(divAmt, 0.0))
757 divAmtStr << divAmt;
758 convValStr << events.getCurrentConversionRatio(i) * eqFwd;
759 convPrcStr << N0 / (events.getCurrentConversionRatio(i) * events.getCurrentFxConversion(i));
760
761 std::ostringstream eventDescription;
762 eventDescription << std::left << "|" << std::setw(width) << grid[i] << "|" << std::setw(width)
763 << dateStr.str() << "|" << std::setw(width) << notional(grid[i]) << "|" << std::setw(width)
764 << accrual(grid[i]) << "|" << std::setw(width) << bondFlowStr.str() << "|"
765 << std::setw(width) << callStr.str() << "|" << std::setw(width) << putStr.str() << "|"
766 << std::setw(2 * width) << convStr.str() << "|" << std::setw(2 * width)
767 << convResetStr.str() << "|" << std::setw(width) << divStr.str() << "|" << std::setw(width)
768 << currentConvStr.str() << "|" << std::setw(width) << fxConvStr.str() << "|"
769 << std::setw(width) << eqFwdStr.str() << "|" << std::setw(width) << divAmtStr.str() << "|"
770 << std::setw(width) << convValStr.str() << "|" << std::setw(width) << convPrcStr.str()
771 << "|";
772 std::string label = "0000" + std::to_string(counter++);
773 results_.additionalResults["event_" + label.substr(label.size() - 5)] = eventDescription.str();
774 }
775 // do not log more than 100k events, unlikely that this is ever necessary
776 if (counter >= 100000)
777 break;
778 }
779
780 // 14.2 more additional results
781
782 results_.additionalResults.insert(events.additionalResults().begin(), events.additionalResults().end());
783
784 Real tMax = grid[grid.size() - 1];
785 results_.additionalResults["trade.tMax"] = tMax;
786
787 Real eqFwd = model_->equity()->equitySpot()->value() / model_->equity()->equityForecastCurve()->discount(tMax) *
788 model_->equity()->equityDividendCurve()->discount(tMax);
789 results_.additionalResults["market.discountRate(tMax)"] =
790 discountingCurve_.empty() ? model_->equity()->equityForecastCurve()->zeroRate(tMax, Continuous).rate()
791 : discountingCurve_->zeroRate(tMax, Continuous).rate();
792 results_.additionalResults["market.discountingSpread"] =
793 discountingSpread_.empty() ? 0.0 : discountingSpread_->value();
794 results_.additionalResults["market.creditSpread(tMax)"] =
795 -std::log(model_->creditCurve()->survivalProbability(tMax)) / tMax;
796 if (!creditCurve_.empty()) {
797 results_.additionalResults["market.exchangeableBondSpread(tMax)"] =
798 -std::log(creditCurve_->survivalProbability(tMax)) / tMax;
799 }
800 results_.additionalResults["market.recoveryRate"] = recoveryRate_.empty() ? 0.0 : recoveryRate_->value();
801 results_.additionalResults["market.equitySpot"] = model_->equity()->equitySpot()->value();
802 results_.additionalResults["market.equityForward(tMax)"] = eqFwd;
803 results_.additionalResults["market.equityVolaility(tMax)"] =
804 std::sqrt(model_->totalBlackVariance() / model_->stepTimes().back());
805
806 results_.additionalResults["model.fdGridSize"] = grid.size();
807 results_.additionalResults["model.eta"] = model_->eta();
808 results_.additionalResults["model.p"] = model_->p();
809 results_.additionalResults["model.calibrationTimes"] = model_->stepTimes();
810 results_.additionalResults["model.h0"] = model_->h0();
811 results_.additionalResults["model.sigma"] = model_->sigma();
812
813 if (!conversionIndicator.empty()) {
814 MonotonicCubicNaturalSpline interpolationConversionIndicator(
815 mesher_->locations().begin(), mesher_->locations().end(), conversionIndicator[0].begin());
816 results_.additionalResults["conversionIndicator"] = interpolationConversionIndicator(logSpot);
817 }
818}
819
820} // namespace QuantExt
const Instrument::results * results_
Definition: cdsoption.cpp:81
bool hasDividendPassThrough(const Size i) const
bool hasMandatoryConversion(const Size i) const
bool hasContingentConversion(const Size i) const
const std::set< Real > & times() const
void registerMandatoryConversion(const ConvertibleBond2::MandatoryConversionData &c)
void registerBondCashflow(const QuantLib::ext::shared_ptr< CashFlow > &c)
void registerConversionReset(const ConvertibleBond2::ConversionResetData &c)
const ConversionResetData & getConversionResetData(const Size i) const
void registerPut(const ConvertibleBond2::CallabilityData &c)
Real getCurrentFxConversion(const Size i) const
void registerConversion(const ConvertibleBond2::ConversionData &c)
const MandatoryConversionData & getMandatoryConversionData(const Size i) const
void registerConversionRatio(const ConvertibleBond2::ConversionRatioData &c)
void registerCall(const ConvertibleBond2::CallabilityData &c)
Real getCurrentConversionRatio(const Size i) const
const ConversionData & getConversionData(const Size i) const
const std::map< std::string, boost::any > & additionalResults() const
const DividendPassThroughData & getDividendPassThroughData(const Size i) const
const CallData & getCallData(const Size i) const
Real getBondFinalRedemption(const Size i) const
const CallData & getPutData(const Size i) const
bool hasStochasticConversionRatio(const Size i) const
void registerDividendProtection(const ConvertibleBond2::DividendProtectionData &c)
void registerMakeWhole(const ConvertibleBond2::MakeWholeData &c)
FdDefaultableEquityJumpDiffusionConvertibleBondEngine(const Handle< DefaultableEquityJumpDiffusionModel > &model, const Handle< QuantLib::YieldTermStructure > &discountingCurve=Handle< QuantLib::YieldTermStructure >(), const Handle< QuantLib::Quote > &discountingSpread=Handle< QuantLib::Quote >(), const Handle< QuantLib::DefaultProbabilityTermStructure > &creditCurve=Handle< QuantLib::DefaultProbabilityTermStructure >(), const Handle< QuantLib::Quote > &recoveryRate=Handle< QuantLib::Quote >(), const Handle< QuantExt::FxIndex > &fxConversion=Handle< QuantExt::FxIndex >(), const bool staticMesher=false, const Size timeStepsPerYear=24, const Size stateGridPoints=100, const Real mesherEpsilon=1E-4, const Real mesherScaling=1.5, const std::vector< Real > conversionRatioDiscretisationGrid={0.1, 0.5, 0.7, 0.9, 1.0, 1.1, 1.3, 1.5, 2.0, 5.0, 10.0}, const bool generateAdditionalResults=true)
Real interpolateValueFromPlanes(const Real conversionRatio, const std::vector< Array > &value, const std::vector< Real > &stochasticConversionRatios, const Size j)
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
Real getCallPriceAmount(const FdConvertibleBondEvents::CallData &cd, Real notional, Real accruals)
ConvertibleBond2::CallabilityData::PriceType priceType
ConvertibleBond2::DividendProtectionData::AdjustmentStyle adjustmentStyle
ConvertibleBond2::DividendProtectionData::DividendType dividendType
ConvertibleBond2::ConversionResetData::ReferenceType reference
ConvertibleBond2::DividendProtectionData::AdjustmentStyle adjustmentStyle
Swap::arguments * arguments_
std::vector< Size > steps