Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
convertiblebond.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2020 Quaternion Risk Management Ltd
3
4 This file is part of ORE, a free-software/open-source library
5 for transparent pricing and risk analysis - http://opensourcerisk.org
6
7 ORE is free software: you can redistribute it and/or modify it
8 under the terms of the Modified BSD License. You should have received a
9 copy of the license along with this program.
10 The license is also available online at <http://opensourcerisk.org>
11
12 This program is distributed on the basis that it will form a useful
13 contribution to risk analytics and model standardisation, but WITHOUT
14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
16*/
17
26
29
30#include <ql/cashflows/cashflows.hpp>
31#include <ql/time/calendars/jointcalendar.hpp>
32
33#include <boost/lexical_cast.hpp>
34
35#include <algorithm>
36
37namespace ore {
38namespace data {
39
40using namespace QuantLib;
41using namespace QuantExt;
42
43std::map<AssetClass, std::set<std::string>>
44ConvertibleBond::underlyingIndices(const QuantLib::ext::shared_ptr<ReferenceDataManager>& referenceDataManager) const {
45 data_ = originalData_;
46 data_.populateFromBondReferenceData(referenceDataManager);
47 std::map<AssetClass, std::set<std::string>> result;
48 result[AssetClass::BOND] = {data_.bondData().securityId()};
49 if (!data_.conversionData().equityUnderlying().name().empty())
50 result[AssetClass::EQ] = {data_.conversionData().equityUnderlying().name()};
51 return result;
52}
53
54namespace {
55
56ConvertibleBond2::ExchangeableData buildExchangeableData(const ConvertibleBondData::ConversionData& conversionData) {
57 ConvertibleBond2::ExchangeableData result{false, false};
58 if (conversionData.initialised() && conversionData.exchangeableData().initialised()) {
59 result.isExchangeable = conversionData.exchangeableData().isExchangeable();
60 result.isSecured = conversionData.exchangeableData().secured();
61 }
62 return result;
63}
64
65std::vector<ConvertibleBond2::CallabilityData>
66buildCallabilityData(const ConvertibleBondData::CallabilityData& callData, const Date& openEndDateReplacement) {
67 std::vector<ConvertibleBond2::CallabilityData> result;
68 if (callData.initialised()) {
69 QuantLib::Schedule schedule = makeSchedule(callData.dates(), openEndDateReplacement);
70 std::vector<Date> callDatesPlusInf = schedule.dates();
71 callDatesPlusInf.push_back(Date::maxDate());
72 auto styles = buildScheduledVectorNormalised<std::string>(callData.styles(), callData.styleDates(),
73 callDatesPlusInf, "Bermudan", true);
74 auto prices = buildScheduledVectorNormalised<double>(callData.prices(), callData.priceDates(), callDatesPlusInf,
75 1.0, true);
76 auto priceTypes = buildScheduledVectorNormalised<std::string>(callData.priceTypes(), callData.priceTypeDates(),
77 callDatesPlusInf, "Clean", true);
78 auto includeAccrual = buildScheduledVectorNormalised<bool>(
79 callData.includeAccrual(), callData.includeAccrualDates(), callDatesPlusInf, true, true);
80 auto isSoft = buildScheduledVectorNormalised<bool>(callData.isSoft(), callData.isSoftDates(), callDatesPlusInf,
81 false, true);
82 auto triggerRatios = buildScheduledVectorNormalised<double>(
83 callData.triggerRatios(), callData.triggerRatioDates(), callDatesPlusInf, 0.0, true);
84 auto nOfMTriggers = buildScheduledVectorNormalised<std::string>(
85 callData.nOfMTriggers(), callData.nOfMTriggerDates(), callDatesPlusInf, "0-of-0", true);
86
87 for (Size i = 0; i < callDatesPlusInf.size() - 1; ++i) {
90 if (styles[i] == "Bermudan") {
91 exerciseType = ConvertibleBond2::CallabilityData::ExerciseType::OnThisDate;
92 } else if (styles[i] == "American") {
93 QL_REQUIRE(
94 callDatesPlusInf.size() > 2,
95 "for exercise style 'American' at least two dates (start, end) are required (call/put data)");
96 if (i == callDatesPlusInf.size() - 2)
97 exerciseType = ConvertibleBond2::CallabilityData::ExerciseType::OnThisDate;
98 else
99 exerciseType = ConvertibleBond2::CallabilityData::ExerciseType::FromThisDateOn;
100 } else {
101 QL_FAIL("invalid exercise style '" << styles[i] << "', expected Bermudan, American (call/put data)");
102 }
103 if (priceTypes[i] == "Clean") {
104 priceType = ConvertibleBond2::CallabilityData::PriceType::Clean;
105 } else if (priceTypes[i] == "Dirty") {
106 priceType = ConvertibleBond2::CallabilityData::PriceType::Dirty;
107 } else {
108 QL_FAIL("invalid price type '" << priceTypes[i] << "', expected Clean, Dirty");
109 }
110 result.push_back(ConvertibleBond2::CallabilityData{callDatesPlusInf[i], exerciseType, prices[i], priceType,
111 includeAccrual[i], isSoft[i], triggerRatios[i]});
112 }
113 }
114 return result;
115} // buildCallabilityData()
116
117ConvertibleBond2::MakeWholeData buildMakeWholeData(const ConvertibleBondData::CallabilityData& callData) {
119 if (callData.initialised() && callData.makeWholeData().initialised()) {
120 auto const& crData = callData.makeWholeData().conversionRatioIncreaseData();
121 if (crData.initialised()) {
123 (*d.crIncreaseData).cap = crData.cap().empty() ? Null<Real>() : parseReal(crData.cap());
124 (*d.crIncreaseData).stockPrices = crData.stockPrices();
125 std::vector<Date> dates;
126 std::transform(crData.crIncreaseDates().begin(), crData.crIncreaseDates().end(), std::back_inserter(dates),
127 &parseDate);
128 (*d.crIncreaseData).effectiveDates = dates;
129 (*d.crIncreaseData).crIncrease = crData.crIncrease();
130 }
131 }
132 return d;
133} // buildMakeWholeData()
134
135std::vector<ConvertibleBond2::ConversionRatioData>
136buildConversionRatioData(const ConvertibleBondData::ConversionData& conversionData) {
137 std::vector<ConvertibleBond2::ConversionRatioData> result;
138 std::set<Date> tmp;
139 if (conversionData.initialised()) {
140 for (Size i = 0; i < conversionData.conversionRatios().size(); ++i) {
141 Date d = conversionData.conversionRatioDates()[i].empty()
142 ? Date::minDate()
143 : parseDate(conversionData.conversionRatioDates()[i]);
144 result.push_back({d, conversionData.conversionRatios()[i]});
145 tmp.insert(d);
146 }
147 }
148 // check that we have unique dates
149 QL_REQUIRE(tmp.size() == result.size(), "Found " << result.size() << " conversion ratio definitions, but only "
150 << tmp.size()
151 << " unique start dates, please check for duplicates");
152 return result;
153} // buildConversionRatioData()
154
155std::vector<ConvertibleBond2::ConversionRatioData>
156buildConversionFixedAmountData(const ConvertibleBondData::ConversionData& conversionData) {
157 std::vector<ConvertibleBond2::ConversionRatioData> result;
158 std::set<Date> tmp;
159 if (conversionData.initialised()) {
160 for (Size i = 0; i < conversionData.fixedAmountConversionData().amounts().size(); ++i) {
161 Date d = conversionData.fixedAmountConversionData().amountDates()[i].empty()
162 ? Date::minDate()
163 : parseDate(conversionData.fixedAmountConversionData().amountDates()[i]);
164 result.push_back({d, conversionData.fixedAmountConversionData().amounts()[i]});
165 tmp.insert(d);
166 }
167 }
168 // check that we have unique dates
169 QL_REQUIRE(tmp.size() == result.size(), "Found " << result.size()
170 << " conversion fixed amount definitions, but only " << tmp.size()
171 << " unique start dates, please check for duplicates");
172 return result;
173} // buildConversionFixedAmountData()
174
175namespace {
176Calendar getEqFxFixingCalendar(const QuantLib::ext::shared_ptr<EquityIndex2>& equity, const QuantLib::ext::shared_ptr<FxIndex>& fx) {
177 if (fx == nullptr && equity == nullptr) {
178 return NullCalendar();
179 } else if (fx == nullptr && equity != nullptr) {
180 return equity->fixingCalendar();
181 } else if (fx != nullptr && equity == nullptr) {
182 return fx->fixingCalendar();
183 } else {
184 return JointCalendar(equity->fixingCalendar(), fx->fixingCalendar());
185 }
186}
187} // namespace
188
189std::vector<ConvertibleBond2::ConversionData>
190buildConversionData(const ConvertibleBondData::ConversionData& conversionData, RequiredFixings& requiredFixings,
191 const QuantLib::ext::shared_ptr<EquityIndex2>& equity, const QuantLib::ext::shared_ptr<FxIndex>& fx,
192 const std::string& fxIndexName, const Date& openEndDateReplacement) {
193 std::vector<ConvertibleBond2::ConversionData> result;
194 auto fixingCalendar = getEqFxFixingCalendar(equity, fx);
195 if (conversionData.initialised() && conversionData.dates().hasData()) {
196 QuantLib::Schedule schedule = makeSchedule(conversionData.dates(), openEndDateReplacement);
197 std::vector<Date> conversionDatesPlusInf = schedule.dates();
198 conversionDatesPlusInf.push_back(Date::maxDate());
199 auto styles = buildScheduledVectorNormalised<std::string>(conversionData.styles(), conversionData.styleDates(),
200 conversionDatesPlusInf, "Bermudan", true);
201
202 // no need to check if initiliased, get empty vectors expanding to observations = None / barriers = 0.0
203 auto cocoObservations = buildScheduledVectorNormalised<std::string>(
204 conversionData.contingentConversionData().observations(),
205 conversionData.contingentConversionData().observationDates(), conversionDatesPlusInf, "None", true);
206 auto cocoBarriers = buildScheduledVectorNormalised<double>(
207 conversionData.contingentConversionData().barriers(),
208 conversionData.contingentConversionData().barrierDates(), conversionDatesPlusInf, 0.0, true);
209
210 for (Size i = 0; i < conversionDatesPlusInf.size() - 1; ++i) {
213
214 if (styles[i] == "Bermudan") {
215 exerciseType = ConvertibleBond2::ConversionData::ExerciseType::OnThisDate;
216 } else if (styles[i] == "American") {
217 QL_REQUIRE(
218 conversionDatesPlusInf.size() > 2,
219 "for exercise style 'American' at least two dates (start, end) are required (conversion data)");
220 if (i == conversionDatesPlusInf.size() - 2)
221 exerciseType = ConvertibleBond2::ConversionData::ExerciseType::OnThisDate;
222 else
223 exerciseType = ConvertibleBond2::ConversionData::ExerciseType::FromThisDateOn;
224 } else {
225 QL_FAIL("invalid exercise style '" << styles[i] << "', expected Bermudan, American (conversion data)");
226 }
227
228 QL_REQUIRE(equity != nullptr || cocoObservations[i] == "None",
229 "coco observations must be none if no equity underlying is given.");
230 if (cocoObservations[i] == "Spot" ||
231 exerciseType == ConvertibleBond2::ConversionData::ExerciseType::OnThisDate) {
232 cocoType = ConvertibleBond2::ConversionData::CocoType::Spot;
233 } else if (cocoObservations[i] == "StartOfPeriod") {
234 cocoType = ConvertibleBond2::ConversionData::CocoType::StartOfPeriod;
235 // for start of period coco checks we need the equity fixing
236 requiredFixings.addFixingDate(fixingCalendar.adjust(conversionDatesPlusInf[i], Preceding),
237 "EQ-" + equity->name(), conversionDatesPlusInf[i + 1] + 1);
238 if (fx != nullptr) {
239 requiredFixings.addFixingDate(fixingCalendar.adjust(conversionDatesPlusInf[i], Preceding),
240 fxIndexName, conversionDatesPlusInf[i + 1] + 1);
241 }
242 } else if (cocoObservations[i] == "None") {
243 cocoType = ConvertibleBond2::ConversionData::CocoType::None;
244 } else {
245 QL_FAIL("invalid coco observation style '" << cocoObservations[i]
246 << "', expected Spot, StartOfPeriod, None");
247 }
248
249 result.push_back(
250 ConvertibleBond2::ConversionData{conversionDatesPlusInf[i], exerciseType, cocoType, cocoBarriers[i]});
251 }
252 }
253 return result;
254} // buildConversionData()
255
256std::vector<ConvertibleBond2::MandatoryConversionData>
257buildMandatoryConversionData(const ConvertibleBondData::ConversionData& conversionData) {
258 std::vector<ConvertibleBond2::MandatoryConversionData> result;
259 if (conversionData.initialised() && conversionData.mandatoryConversionData().initialised()) {
260 if (conversionData.mandatoryConversionData().type() == "PEPS") {
261 QL_REQUIRE(conversionData.mandatoryConversionData().pepsData().initialised(),
262 "expected peps detail data for mandatory conversion");
264 parseDate(conversionData.mandatoryConversionData().date()),
265 conversionData.mandatoryConversionData().pepsData().upperBarrier(),
266 conversionData.mandatoryConversionData().pepsData().lowerBarrier(),
267 conversionData.mandatoryConversionData().pepsData().upperConversionRatio(),
268 conversionData.mandatoryConversionData().pepsData().lowerConversionRatio()});
269 } else {
270 QL_FAIL("invalid mandatory conversion type '" << conversionData.mandatoryConversionData().type()
271 << "', expected PEPS");
272 }
273 }
274 return result;
275}
276
277std::vector<ConvertibleBond2::ConversionResetData>
278buildConversionResetData(const ConvertibleBondData::ConversionData& conversionData, RequiredFixings& requiredFixings,
279 const QuantLib::ext::shared_ptr<EquityIndex2>& equity, const QuantLib::ext::shared_ptr<FxIndex>& fx,
280 const std::string& fxIndexName, const Date& openEndDateReplacement) {
281 std::vector<ConvertibleBond2::ConversionResetData> result;
282 auto fixingCalendar = getEqFxFixingCalendar(equity, fx);
283 if (conversionData.initialised() && conversionData.conversionResetData().initialised()) {
284 QL_REQUIRE(equity != nullptr || !conversionData.conversionResetData().initialised(),
285 "no conversion reset data must be specified if no equity underlying is given.");
286 QuantLib::Schedule schedule =
287 makeSchedule(conversionData.conversionResetData().dates(), openEndDateReplacement);
288 std::vector<Date> resetDatesPlusInf = schedule.dates();
289 resetDatesPlusInf.push_back(Date::maxDate());
290 auto references = buildScheduledVectorNormalised<std::string>(
291 conversionData.conversionResetData().references(), conversionData.conversionResetData().referenceDates(),
292 resetDatesPlusInf, "InitialConversionPrice", true);
293 auto thresholds = buildScheduledVectorNormalised<double>(conversionData.conversionResetData().thresholds(),
294 conversionData.conversionResetData().thresholdDates(),
295 resetDatesPlusInf, 0.0, true);
296 auto gearings = buildScheduledVectorNormalised<double>(conversionData.conversionResetData().gearings(),
297 conversionData.conversionResetData().gearingDates(),
298 resetDatesPlusInf, 0.0, true);
299 auto floors = buildScheduledVectorNormalised<double>(conversionData.conversionResetData().floors(),
300 conversionData.conversionResetData().floorDates(),
301 resetDatesPlusInf, 0.0, true);
302 auto globalFloors = buildScheduledVectorNormalised<double>(
303 conversionData.conversionResetData().globalFloors(),
304 conversionData.conversionResetData().globalFloorDates(), resetDatesPlusInf, 0.0, true);
305 for (Size i = 0; i < resetDatesPlusInf.size() - 1; ++i) {
306 QL_REQUIRE(gearings[i] > 0.0 || close_enough(gearings[i], 0.0),
307 "conversion reset gearing at " << QuantLib::io::iso_date(resetDatesPlusInf[i])
308 << " must be non-negative (got " << gearings[i] << ")");
309 QL_REQUIRE(floors[i] > 0.0 || close_enough(floors[i], 0.0),
310 "conversion reset floor at " << QuantLib::io::iso_date(resetDatesPlusInf[i])
311 << " must be non-negative (got " << floors[i]);
312 QL_REQUIRE(globalFloors[i] > 0.0 || close_enough(globalFloors[i], 0.0),
313 "conversion reset global floor at " << QuantLib::io::iso_date(resetDatesPlusInf[i])
314 << " must be non-negative (got " << globalFloors[i]);
316 if (references[i] == "InitialConversionPrice") {
317 referenceType = ConvertibleBond2::ConversionResetData::ReferenceType::InitialCP;
318 } else if (references[i] == "CurrentConversionPrice") {
319 referenceType = ConvertibleBond2::ConversionResetData::ReferenceType::CurrentCP;
320 } else {
321 QL_FAIL("invalid conversion reset reference type '"
322 << references[i] << "', expected InitialConversionPrice, CurrentConversionPrice");
323 }
324 result.push_back(ConvertibleBond2::ConversionResetData{referenceType, resetDatesPlusInf[i], thresholds[i],
325 gearings[i], floors[i], globalFloors[i]});
326 // on reset dates we need the equity fixing
327 requiredFixings.addFixingDate(fixingCalendar.adjust(resetDatesPlusInf[i], Preceding),
328 "EQ-" + equity->name());
329 if (fx != nullptr) {
330 requiredFixings.addFixingDate(fixingCalendar.adjust(resetDatesPlusInf[i], Preceding), fxIndexName);
331 }
332 }
333 }
334 return result;
335} // buildConversionResetData()
336
337std::vector<ConvertibleBond2::DividendProtectionData>
338buildDividendProtectionData(const ConvertibleBondData::DividendProtectionData& dividendProtectionData,
339 RequiredFixings& requiredFixings, const QuantLib::ext::shared_ptr<EquityIndex2>& equity,
340 const QuantLib::ext::shared_ptr<FxIndex>& fx, const std::string& fxIndexName,
341 const Date& openEndDateReplacement) {
342 std::vector<ConvertibleBond2::DividendProtectionData> result;
343 auto fixingCalendar = getEqFxFixingCalendar(equity, fx);
344 QL_REQUIRE(equity != nullptr || !dividendProtectionData.initialised(),
345 "no dividend protection data must be given if no equity underlying is given.");
346 if (dividendProtectionData.initialised()) {
347 Schedule schedule = makeSchedule(dividendProtectionData.dates(), openEndDateReplacement);
348 QL_REQUIRE(
349 schedule.dates().size() >= 2,
350 "dividend protection schedule must have at least two dates (effective dp start and first protection date)");
351 std::vector<Date> divDates(std::next(schedule.dates().begin(), 1), schedule.dates().end());
352 divDates.push_back(Date::maxDate());
353 auto styles = buildScheduledVectorNormalised<std::string>(dividendProtectionData.adjustmentStyles(),
354 dividendProtectionData.adjustmentStyleDates(),
355 divDates, "CrUpOnly", true);
356 auto types = buildScheduledVectorNormalised<std::string>(dividendProtectionData.dividendTypes(),
357 dividendProtectionData.dividendTypeDates(), divDates,
358 "Absolute", true);
359 auto thresholds = buildScheduledVectorNormalised<double>(
360 dividendProtectionData.thresholds(), dividendProtectionData.thresholdDates(), divDates, 0.0, true);
361 for (Size i = 0; i < divDates.size() - 1; ++i) {
364 if (styles[i] == "CrUpOnly") {
365 adjustmentStyle = ConvertibleBond2::DividendProtectionData::AdjustmentStyle::CrUpOnly;
366 } else if (styles[i] == "CrUpDown") {
367 adjustmentStyle = ConvertibleBond2::DividendProtectionData::AdjustmentStyle::CrUpDown;
368 } else if (styles[i] == "CrUpOnly2") {
369 adjustmentStyle = ConvertibleBond2::DividendProtectionData::AdjustmentStyle::CrUpOnly2;
370 QL_REQUIRE(
371 types[i] == "Absolute",
372 "divident prototection adjustment style 'CrUpOnly2' is only allowed with dividend type 'Absolute'");
373 } else if (styles[i] == "CrUpDown2") {
374 adjustmentStyle = ConvertibleBond2::DividendProtectionData::AdjustmentStyle::CrUpDown2;
375 QL_REQUIRE(
376 types[i] == "Absolute",
377 "dividend prototection adjustment style 'CrUpDown2' is only allowed with dividend type 'Absolute'");
378 } else if (styles[i] == "PassThroughUpOnly") {
379 adjustmentStyle = ConvertibleBond2::DividendProtectionData::AdjustmentStyle::PassThroughUpOnly;
380 } else if (styles[i] == "PassThroughUpDown") {
381 adjustmentStyle = ConvertibleBond2::DividendProtectionData::AdjustmentStyle::PassThroughUpDown;
382 } else {
383 QL_FAIL(
384 "invalid dividend protection adjustment style '"
385 << styles[i]
386 << "', expected CrUpOnly, CrUpDown, CrUpOnly2, CrUpDown2, PassThroughUpOnly, PassThroughUpDown");
387 }
388 if (types[i] == "Absolute") {
389 dividendType = ConvertibleBond2::DividendProtectionData::DividendType::Absolute;
390 } else if (types[i] == "Relative") {
391 dividendType = ConvertibleBond2::DividendProtectionData::DividendType::Relative;
392 } else {
393 QL_FAIL("invalid dividend protection dividend type '" << types[i] << "', expected Absolute, Relative");
394 }
395 result.push_back(
396 ConvertibleBond2::DividendProtectionData{i == 0 ? schedule.dates()[0] : divDates[i - 1] + 1,
397 divDates[i], adjustmentStyle, dividendType, thresholds[i]});
398 // on protection dates we need the equity fixing for CrUpOnly, CrUpDown adjustment styles
399 if (adjustmentStyle == ConvertibleBond2::DividendProtectionData::AdjustmentStyle::CrUpOnly ||
400 adjustmentStyle == ConvertibleBond2::DividendProtectionData::AdjustmentStyle::CrUpDown ||
401 adjustmentStyle == ConvertibleBond2::DividendProtectionData::AdjustmentStyle::CrUpOnly2 ||
402 adjustmentStyle == ConvertibleBond2::DividendProtectionData::AdjustmentStyle::CrUpDown2) {
403 requiredFixings.addFixingDate(fixingCalendar.adjust(schedule.dates()[i + 1], Preceding),
404 "EQ-" + equity->name());
405 if (fx != nullptr) {
406 requiredFixings.addFixingDate(fixingCalendar.adjust(schedule.dates()[i + 1], Preceding),
407 fxIndexName);
408 }
409 }
410 }
411 }
412 return result;
413} // buildDividendProtectionData()
414
415} // namespace
416
417void ConvertibleBond::build(const QuantLib::ext::shared_ptr<ore::data::EngineFactory>& engineFactory) {
418 DLOG("ConvertibleBond::build() called for trade " << id());
419
420 // ISDA taxonomy: not a derivative, but define the asset class at least
421 // so that we can determine a TRS asset class that has Convertible Bond underlyings
422 additionalData_["isdaAssetClass"] = string("Credit");
423 additionalData_["isdaBaseProduct"] = string("");
424 additionalData_["isdaSubProduct"] = string("");
425 additionalData_["isdaTransaction"] = string("");
426
427 auto builder = QuantLib::ext::dynamic_pointer_cast<ConvertibleBondEngineBuilder>(engineFactory->builder("ConvertibleBond"));
428 QL_REQUIRE(builder, "ConvertibleBond::build(): could not cast to ConvertibleBondBuilder, this is unexpected");
429
430 data_ = originalData_;
431 data_.populateFromBondReferenceData(engineFactory->referenceData());
432
433 // build converible underlying bond, add to required fixings
434
435 ore::data::Bond underlyingBond(Envelope(), data_.bondData());
436 underlyingBond.build(engineFactory);
437 requiredFixings_.addData(underlyingBond.requiredFixings());
438 auto qlUnderlyingBond = QuantLib::ext::dynamic_pointer_cast<QuantLib::Bond>(underlyingBond.instrument()->qlInstrument());
439 QL_REQUIRE(qlUnderlyingBond,
440 "ConvertibleBond::build(): internal error, could not cast underlying bond to QuantLib::Bond");
441 auto qlUnderlyingBondCoupons = qlUnderlyingBond->cashflows();
442 qlUnderlyingBondCoupons.erase(
443 std::remove_if(qlUnderlyingBondCoupons.begin(), qlUnderlyingBondCoupons.end(),
444 [](QuantLib::ext::shared_ptr<CashFlow> c) { return QuantLib::ext::dynamic_pointer_cast<Coupon>(c) == nullptr; }),
445 qlUnderlyingBondCoupons.end());
446
447 // get open end date replacement from vanilla builder to handle perpetuals
448
449 QuantLib::ext::shared_ptr<EngineBuilder> vanillaBuilder = engineFactory->builder("Bond");
450 QL_REQUIRE(builder, "ConvertibleBond::build(): internal error, vanilla builder is null");
451 std::string openEndDateStr = vanillaBuilder->modelParameter("OpenEndDateReplacement", {}, false, "");
452 Date openEndDateReplacement = getOpenEndDateReplacement(openEndDateStr, parseCalendar(data_.bondData().calendar()));
453
454 // check whether the underlying bond is set up as perpetual (i.e. without maturity date)
455
456 bool isPerpetual = false;
457 for (auto const& d : data_.bondData().coupons()) {
458 for (auto const& r : d.schedule().rules()) {
459 isPerpetual = isPerpetual || r.endDate().empty();
460 }
461 }
462
463 DLOG("isPerpetual=" << std::boolalpha << isPerpetual << ", openEndDateReplacement=" << openEndDateReplacement);
464
465 // get equity index and fx index
466
467 auto config = builder->configuration(MarketContext::pricing);
468
469 QuantLib::ext::shared_ptr<EquityIndex2> equity;
470 if (!data_.conversionData().equityUnderlying().name().empty()) {
471 equity = *engineFactory->market()->equityCurve(data_.conversionData().equityUnderlying().name(), config);
472 }
473
474 QL_REQUIRE((equity == nullptr && data_.conversionData().fixedAmountConversionData().initialised()) ||
475 (equity != nullptr && !data_.conversionData().fixedAmountConversionData().initialised()),
476 "ConvertibleBondEngineBuilder::build(): exactly one of equity underlying or fixed amount conversion "
477 "must be specified");
478
479 QuantLib::ext::shared_ptr<FxIndex> fx;
480 if (equity != nullptr && !equity->currency().empty()) {
481 if (equity->currency().code() != data_.bondData().currency()) {
482 QL_REQUIRE(!data_.conversionData().fxIndex().empty(),
483 "ConvertibleBondEngineBuilder::build(): FXIndex required in conversion data, since eq ccy ("
484 << equity->currency().code() << ") not equal bond ccy (" << data_.bondData().currency()
485 << ")");
486 fx = buildFxIndex(data_.conversionData().fxIndex(), data_.bondData().currency(), equity->currency().code(),
487 engineFactory->market(), config);
488 }
489 } else if (equity == nullptr && data_.conversionData().fixedAmountConversionData().initialised()) {
490 if (data_.conversionData().fixedAmountConversionData().currency() != data_.bondData().currency()) {
491 QL_REQUIRE(!data_.conversionData().fxIndex().empty(),
492 "ConvertibleBondEngineBuilder::build(): FXIndex required in conversion data, since fixed amount "
493 "conversion ccy ("
494 << data_.conversionData().fixedAmountConversionData().currency() << ") not equal bond ccy ("
495 << data_.bondData().currency() << ")");
496 fx = buildFxIndex(data_.conversionData().fxIndex(), data_.bondData().currency(),
497 data_.conversionData().fixedAmountConversionData().currency(), engineFactory->market(),
498 config);
499 }
500 }
501
502 // for cross currency, add required FX fixings for conversion and dividend history
503
504 if (fx != nullptr) {
505
506 Date d0 = qlUnderlyingBond->startDate();
507 Date d1 = qlUnderlyingBond->maturityDate();
508
509 // FIXME, the following only works, if we have the dividends loaded at this point...
510 if (equity != nullptr) {
511 auto div = equity->dividendFixings();
512 for (auto const& d : div) {
513 if (d.exDate >= d0) {
514 requiredFixings_.addFixingDate(fx->fixingCalendar().adjust(d.exDate, Preceding),
515 data_.conversionData().fxIndex());
516 }
517 }
518 }
519
520 Date today = Settings::instance().evaluationDate();
521 d0 = std::min(d0, today);
522
523 // ...as a workaround, we add all fx fixings from min(today, bond start date) to maturity
524 // -> this also covers the required fx fixings for conversion, so we don't have to add them separately
525 for (Date d = d0; d <= d1; ++d) {
526 requiredFixings_.addFixingDate(fx->fixingCalendar().adjust(d, Preceding), data_.conversionData().fxIndex(),
527 Date::maxDate(), false, false);
528 }
529 }
530
531 // the multiplier, basically the number of bonds and a sign for long / short positions
532
533 Real multiplier = data_.bondData().bondNotional() * (data_.bondData().isPayer() ? -1.0 : 1.0);
534
535 // build convertible data
536
537 ConvertibleBond2::ExchangeableData exchangeableData = buildExchangeableData(data_.conversionData());
538 std::vector<ConvertibleBond2::CallabilityData> callData =
539 buildCallabilityData(data_.callData(), openEndDateReplacement);
540 ConvertibleBond2::MakeWholeData makeWholeCrIncreaseData = buildMakeWholeData(data_.callData());
541 std::vector<ConvertibleBond2::CallabilityData> putData =
542 buildCallabilityData(data_.putData(), openEndDateReplacement);
543 // for fixed amounts the model will provide an equity with constant unit spot rate, so that
544 // we can treat the amount as a ratio
545 std::vector<ConvertibleBond2::ConversionRatioData> conversionRatioData =
546 equity != nullptr ? buildConversionRatioData(data_.conversionData())
547 : buildConversionFixedAmountData(data_.conversionData());
548 std::vector<ConvertibleBond2::ConversionData> conversionData = buildConversionData(
549 data_.conversionData(), requiredFixings_, equity, fx, data_.conversionData().fxIndex(), openEndDateReplacement);
550 std::vector<ConvertibleBond2::MandatoryConversionData> mandatoryConversionData =
551 buildMandatoryConversionData(data_.conversionData());
552 std::vector<ConvertibleBond2::ConversionResetData> conversionResetData = buildConversionResetData(
553 data_.conversionData(), requiredFixings_, equity, fx, data_.conversionData().fxIndex(), openEndDateReplacement);
554 std::vector<ConvertibleBond2::DividendProtectionData> dividendProtectionData =
555 buildDividendProtectionData(data_.dividendProtectionData(), requiredFixings_, equity, fx,
556 data_.conversionData().fxIndex(), openEndDateReplacement);
557
558 // build convertible bond instrument and attach pricing engine
559
560 // get the last relevant date of the convertible bond, this is used as the last calibration date for the model
561 Date lastDate = qlUnderlyingBond->maturityDate();
562 if (!dividendProtectionData.empty()) {
563 lastDate = std::max(lastDate, dividendProtectionData.back().protectionDate);
564 }
565
566 QL_REQUIRE(data_.conversionData().initialised(), "ConvertibleBond::build(): conversion data required");
567 auto qlConvertible = QuantLib::ext::make_shared<ConvertibleBond2>(
568 qlUnderlyingBond->settlementDays(), qlUnderlyingBond->calendar(), qlUnderlyingBond->issueDate(),
569 qlUnderlyingBondCoupons, exchangeableData, callData, makeWholeCrIncreaseData, putData, conversionRatioData,
570 conversionData, mandatoryConversionData, conversionResetData, dividendProtectionData,
571 data_.detachable().empty() ? false : parseBool(data_.detachable()), isPerpetual);
572 qlConvertible->setPricingEngine(builder->engine(
573 id(), data_.bondData().currency(), data_.bondData().creditCurveId(), data_.bondData().hasCreditRisk(),
574 data_.bondData().securityId(), data_.bondData().referenceCurveId(), exchangeableData.isExchangeable, equity, fx,
575 data_.conversionData().exchangeableData().equityCreditCurve(), qlUnderlyingBond->startDate(), lastDate));
576 setSensitivityTemplate(*builder);
577
578 // set up other trade member variables
579
580 instrument_ = QuantLib::ext::make_shared<VanillaInstrument>(qlConvertible, multiplier);
581 npvCurrency_ = notionalCurrency_ = data_.bondData().currency();
582 maturity_ = qlUnderlyingBond->maturityDate();
583 notional_ = qlUnderlyingBond->notional();
584 legs_ = {qlUnderlyingBond->cashflows()};
585 legCurrencies_ = {npvCurrency_};
586 legPayers_ = {data_.bondData().isPayer()};
587}
588
589void ConvertibleBond::fromXML(XMLNode* node) {
590 Trade::fromXML(node);
591 originalData_.fromXML(XMLUtils::getChildNode(node, "ConvertibleBondData"));
592 data_ = originalData_;
593}
594
595XMLNode* ConvertibleBond::toXML(XMLDocument& doc) const {
596 XMLNode* node = Trade::toXML(doc);
597 XMLUtils::appendNode(node, originalData_.toXML(doc));
598 return node;
599}
600
601void ConvertibleBondTrsUnderlyingBuilder::build(
602 const std::string& parentId, const QuantLib::ext::shared_ptr<Trade>& underlying, const std::vector<Date>& valuationDates,
603 const std::vector<Date>& paymentDates, const std::string& fundingCurrency,
604 const QuantLib::ext::shared_ptr<EngineFactory>& engineFactory, QuantLib::ext::shared_ptr<QuantLib::Index>& underlyingIndex,
605 Real& underlyingMultiplier, std::map<std::string, double>& indexQuantities,
606 std::map<std::string, QuantLib::ext::shared_ptr<QuantExt::FxIndex>>& fxIndices, Real& initialPrice,
607 std::string& assetCurrency, std::string& creditRiskCurrency,
608 std::map<std::string, SimmCreditQualifierMapping>& creditQualifierMapping, Date& maturity,
609 const std::function<QuantLib::ext::shared_ptr<QuantExt::FxIndex>(
610 const QuantLib::ext::shared_ptr<Market> market, const std::string& configuration, const std::string& domestic,
611 const std::string& foreign, std::map<std::string, QuantLib::ext::shared_ptr<QuantExt::FxIndex>>& fxIndices)>&
612 getFxIndex,
613 const std::string& underlyingDerivativeId, RequiredFixings& fixings, std::vector<Leg>& returnLegs) const {
614 auto t = QuantLib::ext::dynamic_pointer_cast<ore::data::ConvertibleBond>(underlying);
615 QL_REQUIRE(t, "could not cast to ore::data::Bond, this is unexpected");
616 auto qlBond = QuantLib::ext::dynamic_pointer_cast<QuantLib::Bond>(underlying->instrument()->qlInstrument());
617 QL_REQUIRE(qlBond, "expected QuantLib::Bond, could not cast");
618
619 BondIndexBuilder bondIndexBuilder(t->bondData(), true, false, NullCalendar(), true, engineFactory);
620 underlyingIndex = bondIndexBuilder.bondIndex();
621
622 underlyingIndex = bondIndexBuilder.bondIndex();
623 underlyingMultiplier = t->data().bondData().bondNotional();
624
625 indexQuantities[underlyingIndex->name()] = underlyingMultiplier;
626 if (initialPrice != Null<Real>())
627 initialPrice = qlBond->notional(valuationDates.front()) * bondIndexBuilder.priceAdjustment(initialPrice);
628
629 assetCurrency = t->data().bondData().currency();
630 auto fxIndex = getFxIndex(engineFactory->market(), engineFactory->configuration(MarketContext::pricing),
631 assetCurrency, fundingCurrency, fxIndices);
632 auto returnLeg =
633 makeBondTRSLeg(valuationDates, paymentDates, bondIndexBuilder, initialPrice, fxIndex);
634
635 // add required bond and fx fixings for bondIndex
636 returnLegs.push_back(returnLeg);
637 bondIndexBuilder.addRequiredFixings(fixings, returnLeg);
638
639 creditRiskCurrency = t->data().bondData().currency();
640 creditQualifierMapping[securitySpecificCreditCurveName(t->bondData().securityId(), t->bondData().creditCurveId())] =
641 SimmCreditQualifierMapping(t->data().bondData().securityId(), t->data().bondData().creditGroup());
642 creditQualifierMapping[t->bondData().creditCurveId()] =
643 SimmCreditQualifierMapping(t->data().bondData().securityId(), t->data().bondData().creditGroup());
644 // FIXME shouldn't we leave that empty and let TRS determine the maturity date based on valuation / funding dates?
645 maturity = qlBond->maturityDate();
646}
647
648void ConvertibleBondTrsUnderlyingBuilder::updateUnderlying(const QuantLib::ext::shared_ptr<ReferenceDataManager>& refData,
649 QuantLib::ext::shared_ptr<Trade>& underlying,
650 const std::string& parentId) const {
651
652 /* If the underlying is a bond, but the security id is actually pointing to reference data of a non-vanilla bond
653 flavour like a convertible bond, callable bond, etc., we change the underlying to that non-vanilla bond flavour
654 here on the fly. This way we can reference a bond from a TRS without knowing its flavour. */
655
656 if (underlying->tradeType() == "Bond") {
657 auto bond = QuantLib::ext::dynamic_pointer_cast<ore::data::Bond>(underlying);
658 QL_REQUIRE(bond, "TRS::build(): internal error, could not cast underlying trade to bond");
659 if (refData != nullptr &&
660 refData->hasData(ConvertibleBondReferenceDatum::TYPE, bond->bondData().securityId())) {
661 DLOG("Underlying trade type is bond, but security id '"
662 << bond->bondData().securityId()
663 << "' points to convertible bond in ref data, so we change the underlying trade type accordingly.");
664 underlying = QuantLib::ext::make_shared<ConvertibleBond>(Envelope(), ConvertibleBondData(bond->bondData()));
665 underlying->id() = parentId + "_underlying";
666 }
667 }
668}
669
670BondBuilder::Result ConvertibleBondBuilder::build(const QuantLib::ext::shared_ptr<EngineFactory>& engineFactory,
671 const QuantLib::ext::shared_ptr<ReferenceDataManager>& referenceData,
672 const std::string& securityId) const {
673 static long id = 0;
674 ConvertibleBondData data(BondData(securityId, 1.0));
675 data.populateFromBondReferenceData(referenceData);
677 bond.id() = "ConvertibleBondBuilder_" + securityId + "_" + std::to_string(id++);
678 bond.build(engineFactory);
679
680 QL_REQUIRE(bond.instrument(), "ConvertibleBondBuilder: constructed bond is null, this is unexpected");
681 auto qlBond = QuantLib::ext::dynamic_pointer_cast<QuantLib::Bond>(bond.instrument()->qlInstrument());
682
683 QL_REQUIRE(
684 bond.instrument() && bond.instrument()->qlInstrument(),
685 "ConvertibleBondBuilder: constructed bond trade does not provide a valid ql instrument, this is unexpected "
686 "(either the instrument wrapper or the ql instrument is null)");
687
689 res.bond = qlBond;
690 res.hasCreditRisk = data.bondData().hasCreditRisk() && !data.bondData().creditCurveId().empty();
691 res.currency = data.bondData().currency();
692 res.creditCurveId = data.bondData().creditCurveId();
693 res.securityId = data.bondData().securityId();
694 res.creditGroup = data.bondData().creditGroup();
695 res.priceQuoteMethod = data.bondData().priceQuoteMethod();
696 res.priceQuoteBaseValue = data.bondData().priceQuoteBaseValue();
697
698 auto builders = engineFactory->modelBuilders();
699 auto b = std::find_if(builders.begin(), builders.end(),
700 [&bond](const std::pair<const std::string&, const QuantLib::ext::shared_ptr<ModelBuilder>>& p) {
701 return bond.id() == p.first;
702 });
703 QL_REQUIRE(b != builders.end(), "ConvertibleBondBuilder: could not get model builder for bond '"
704 << bond.id() << "' from engine factory - this is an internal error.");
705 res.modelBuilder = b->second;
706
707 return res;
708}
709
710} // namespace data
711} // namespace ore
Interface for building a bond index.
bond utilities
Serializable Bond.
Definition: bond.hpp:153
virtual void build(const QuantLib::ext::shared_ptr< EngineFactory > &) override
Trade interface.
Definition: bond.cpp:228
QuantLib::Real priceAdjustment(QuantLib::Real price)
QuantLib::ext::shared_ptr< QuantExt::BondIndex > bondIndex() const
void addRequiredFixings(RequiredFixings &requiredFixings, Leg leg={})
const ExchangeableData & exchangeableData() const
std::map< AssetClass, std::set< std::string > > underlyingIndices(const QuantLib::ext::shared_ptr< ReferenceDataManager > &referenceDataManager=nullptr) const override
Add underlying Bond names.
Serializable object holding generic trade data, reporting dimensions.
Definition: envelope.hpp:51
const RequiredFixings & requiredFixings() const
Definition: trade.hpp:90
const QuantLib::ext::shared_ptr< InstrumentWrapper > & instrument() const
Definition: trade.hpp:141
Small XML Document wrapper class.
Definition: xmlutils.hpp:65
Convertible Bond trade data model and serialization.
Calendar parseCalendar(const string &s)
Convert text to QuantLib::Calendar.
Definition: parsers.cpp:157
Date parseDate(const string &s)
Convert std::string to QuantLib::Date.
Definition: parsers.cpp:51
bool parseBool(const string &s)
Convert text to bool.
Definition: parsers.cpp:144
Real parseReal(const string &s)
Convert text to Real.
Definition: parsers.cpp:112
@ data
Definition: log.hpp:77
#define DLOG(text)
Logging Macro (Level = Debug)
Definition: log.hpp:554
market data related utilties
Time maturity
Definition: utilities.cpp:66
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
Size size(const ValueType &v)
Definition: value.cpp:145
boost::bimap< std::string, TRS::FundingData::NotionalType > types
Definition: trs.cpp:783
Date getOpenEndDateReplacement(const std::string &replacementPeriodStr, const Calendar &calendar)
Definition: bondutils.cpp:118
QuantLib::ext::shared_ptr< QuantExt::FxIndex > buildFxIndex(const string &fxIndex, const string &domestic, const string &foreign, const QuantLib::ext::shared_ptr< Market > &market, const string &configuration, bool useXbsCurves)
Definition: marketdata.cpp:137
std::string securitySpecificCreditCurveName(const std::string &securityId, const std::string &creditCurveId)
Definition: marketdata.cpp:79
Schedule makeSchedule(const ScheduleDates &data)
Definition: schedule.cpp:263
Leg makeBondTRSLeg(const std::vector< Date > &valuationDates, const std::vector< Date > &paymentDates, const BondIndexBuilder &bondIndexBuilder, Real initialPrice, QuantLib::ext::shared_ptr< QuantExt::FxIndex > fxIndex)
Serializable Credit Default Swap.
Definition: namespaces.docs:23
Map text representations to QuantLib/QuantExt types.
Reference data model and serialization.
boost::optional< CrIncreaseData > crIncreaseData
QuantLib::ext::shared_ptr< QuantExt::ModelBuilder > modelBuilder
Definition: bond.hpp:186
QuantLib::ext::shared_ptr< QuantLib::Bond > bond
Definition: bond.hpp:185
QuantExt::BondIndex::PriceQuoteMethod priceQuoteMethod
Definition: bond.hpp:193