Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
cdo.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2016 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
22
27
35
36#include <algorithm>
37#include <iterator>
39#include <ql/cashflows/simplecashflow.hpp>
40#include <ql/instruments/compositeinstrument.hpp>
41#include <ql/math/interpolations/backwardflatinterpolation.hpp>
42#include <ql/math/interpolations/loginterpolation.hpp>
43#include <ql/math/optimization/costfunction.hpp>
44#include <ql/math/optimization/levenbergmarquardt.hpp>
45#include <ql/quotes/compositequote.hpp>
46#include <ql/termstructures/credit/flathazardrate.hpp>
54
55using namespace QuantLib;
56using namespace QuantExt;
57
58namespace {
59
60using std::string;
61
62// Check that weight, prior Weight or recovery is in [0, 1]
63void validateWeightRec(Real value, const string& name, const string& varName) {
64 QL_REQUIRE(value <= 1.0,
65 "The " << varName << " value (" << value << ") for name " << name << " should not be greater than 1.0.");
66 QL_REQUIRE(value >= 0.0,
67 "The " << varName << " value (" << value << ") for name " << name << " should not be less than 0.0.");
68}
69
70} // namespace
71
72namespace ore {
73namespace data {
74
75void SyntheticCDO::build(const QuantLib::ext::shared_ptr<EngineFactory>& engineFactory) {
76
77 DLOG("SyntheticCDO::build() called for trade " << id());
78
79 // ISDA taxonomy
80 additionalData_["isdaAssetClass"] = string("Credit");
81 additionalData_["isdaBaseProduct"] = string("Index Tranche");
82 QuantLib::ext::shared_ptr<ReferenceDataManager> refData = engineFactory->referenceData();
83 if (refData && refData->hasData("CreditIndex", qualifier_)) {
84 auto refDatum = refData->getData("CreditIndex", qualifier_);
85 QuantLib::ext::shared_ptr<CreditIndexReferenceDatum> creditIndexRefDatum =
86 QuantLib::ext::dynamic_pointer_cast<CreditIndexReferenceDatum>(refDatum);
87 additionalData_["isdaSubProduct"] = creditIndexRefDatum->indexFamily();
88 if (creditIndexRefDatum->indexFamily() == "") {
89 ALOG("IndexFamily is blank in credit index reference data for entity " << qualifier_);
90 }
91 } else {
92 ALOG("Credit index reference data missing for entity " << qualifier_ << ", isdaSubProduct left blank");
93 }
94 // skip the transaction level mapping for now
95 additionalData_["isdaTransaction"] = string("");
96
97 Date protectionStartDate = protectionStart_ == "" ? Date() : parseDate(protectionStart_);
98 Date upfrontDate = upfrontDate_ == "" ? Date() : parseDate(upfrontDate_);
99 Leg leg = makeFixedLeg(legData_);
100 Protection::Side side = legData_.isPayer() ? Protection::Buyer : Protection::Seller;
101 Schedule schedule = makeSchedule(legData_.schedule());
102 Real fixedRecovery = recoveryRate_;
103 auto fixedLegData = QuantLib::ext::dynamic_pointer_cast<FixedLegData>(legData_.concreteLegData());
104 QL_REQUIRE(fixedLegData, "Expected FixedLegData but got " << legData_.legType());
105 Real runningRate = fixedLegData->rates().front();
106 DayCounter dayCounter = parseDayCounter(legData_.dayCounter());
107 BusinessDayConvention bdc = parseBusinessDayConvention(legData_.paymentConvention());
108 Currency ccy = parseCurrency(legData_.currency());
109
110 // In general for CDS index trades, the standard day counter is Actual/360 and the final
111 // period coupon accrual includes the maturity date.
112 Actual360 standardDayCounter;
113 DayCounter lastPeriodDayCounter = dayCounter == standardDayCounter ? Actual360(true) : dayCounter;
114
115
116 // Set some trade variables
117 npvCurrency_ = legData_.currency();
118 maturity_ = leg.back()->date();
119 notionalCurrency_ = legData_.currency();
120 legs_ = {leg};
121 legPayers_ = {legData_.isPayer()};
122 legCurrencies_ = {legData_.currency()};
123
124 // Checks for upfront date and upfront fee
125 QL_REQUIRE(upfrontDate == Date() || upfrontFee_ != Null<Real>(),
126 "If upfront date is given (" << upfrontDate << "), upfront fee must be given.");
127 QL_REQUIRE(upfrontDate != Date() || upfrontFee_ == Null<Real>() || close_enough(upfrontFee_, 0.0),
128 "If no upfront date is given, no upfront fee should be given but got " << upfrontFee_ << ".");
129
130 // Get the original total notional using the contractual attachment point and detachment point and the contractual
131 // tranche notional. We will calculate the corresponding current amounts below from the basket or reference data.
132 QL_REQUIRE(attachmentPoint_ < detachmentPoint_, "Detachment point should be greater than attachment point.");
133 Real origTrancheNtl = legData_.notionals().front();
134 Real origTotalNtl = origTrancheNtl / (detachmentPoint_ - attachmentPoint_);
135 Real origEquityNtl = origTotalNtl * attachmentPoint_;
136 Real origSeniorNtl = origTotalNtl * (1.0 - detachmentPoint_);
137
138 DLOG("Original tranche notional: " << origTrancheNtl);
139 DLOG("Original equity notional: " << origEquityNtl);
140 DLOG("Original senior notional: " << origSeniorNtl);
141 DLOG("Original attachment point: " << attachmentPoint_);
142 DLOG("Original detachment point: " << detachmentPoint_);
143 DLOG("Original total notional: " << origTotalNtl);
144
145 // There may have been credit events up to the valuation date. Record the cumulative lost and recovered notional.
146 Real lostNotional = 0.0;
147 Real recoveredNotional = 0.0;
148 // clear the basketNotionals and credit names
149
150 vector<Real> basketNotionals;
151
152 vector<string> creditCurves;
153
154
155
156 if (basketData_.constituents().size() > 0) {
157
158 const auto& constituents = basketData_.constituents();
159 DLOG("Building constituents from basket data containing " << constituents.size() << " elements.");
160
161 Real totalRemainingNtl = 0.0;
162 Real totalPriorNtl = 0.0;
163
164 for (const auto& c : constituents) {
165 Real ntl = Null<Real>(), priorNotional = Null<Real>();
166 const auto& creditCurve = c.creditCurveId();
167 if (c.weightInsteadOfNotional()) {
168 ntl = c.weight() * origTotalNtl;
169 priorNotional = c.priorWeight();
170 if (priorNotional != Null<Real>()) {
171 priorNotional *= origTotalNtl;
172 }
173 } else {
174 ntl = c.notional();
175 priorNotional = c.priorNotional();
176 QL_REQUIRE(c.currency() == npvCurrency_, "The currency of basket constituent "
177 << creditCurve << " is " << c.currency()
178 << " and does not equal the trade leg currency "
179 << npvCurrency_);
180 }
181
182 if (!close(0.0, ntl) && ntl > 0.0) {
183 if (std::find(creditCurves.begin(), creditCurves.end(), creditCurve) == creditCurves.end()) {
184 DLOG("Adding underlying " << creditCurve << " with notional " << ntl);
185 creditCurves.push_back(creditCurve);
186 basketNotionals.push_back(ntl);
187 totalRemainingNtl += ntl;
188 } else {
189 StructuredTradeErrorMessage(id(), "Synthetic CDO", "Error building trade",
190 ("Invalid Basket: found a duplicate credit curve " + creditCurve +
191 ", skip it. Check the basket data for possible errors.")
192 .c_str())
193 .log();
194 }
195 } else {
196 DLOG("Underlying " << creditCurve << " notional is " << ntl << " so assuming a credit event occured.");
197 QL_REQUIRE(priorNotional != Null<Real>(),
198 "Expecting a valid prior notional for name " << creditCurve << ".");
199 auto recovery = c.recovery();
200 QL_REQUIRE(recovery != Null<Real>(), "Expecting a valid recovery for name " << creditCurve << ".");
201 validateWeightRec(recovery, creditCurve, "recovery");
202 lostNotional += (1.0 - recovery) * priorNotional;
203 recoveredNotional += recovery * priorNotional;
204 totalPriorNtl += priorNotional;
205 }
206 }
207
208 Real totalNtl = totalRemainingNtl + totalPriorNtl;
209 DLOG("All Underlyings added, total remaining notional = " << totalRemainingNtl);
210 DLOG("All Underlyings added, total prior notional = " << totalPriorNtl);
211 DLOG("All Underlyings added, total notional = " << totalNtl);
212
213 QL_REQUIRE(creditCurves.size() == basketNotionals.size(), "numbers of defaults curves ("
214 << creditCurves.size() << ") and notionals ("
215 << basketNotionals.size() << ") doesnt match");
216 Real notionalCorrectionFactor = origTotalNtl / totalNtl;
217 // Scaling to Notional if relative error is close less than 10^-4
218 if (!close(totalNtl, origTotalNtl) && (abs(notionalCorrectionFactor - 1.0) <= 1e-4)) {
219 ALOG("Trade " << id() << ", sum of notionals(" << totalNtl << ") is very close to total original notional ("
220 << origTotalNtl << "), will scale each notional by " << notionalCorrectionFactor
221 << ", check the basket data for possible errors.");
222 totalRemainingNtl = 0;
223 for (Size i = 0; i < basketNotionals.size(); i++) {
224 Real scaledNotional = basketNotionals[i] * notionalCorrectionFactor;
225 TLOG("Trade " << id() << ", Issuer" << creditCurves[i] << " unscaled Notional: " << basketNotionals[i]
226 << ", scaled Notional: " << scaledNotional);
227 basketNotionals[i] = scaledNotional;
228 totalRemainingNtl += scaledNotional;
229 }
230 lostNotional *= notionalCorrectionFactor;
231 recoveredNotional *= notionalCorrectionFactor;
232 totalNtl *= notionalCorrectionFactor;
233 }
234
235 if (!close(totalRemainingNtl, origTotalNtl) && totalRemainingNtl > origTotalNtl) {
236 StructuredTradeErrorMessage(id(), "Synthetic CDO", "Error building trade",
237 ("Total remaining notional (" + std::to_string(totalRemainingNtl) +
238 ") is greater than total original notional (" +
239 std::to_string(origTotalNtl) +
240 "), check the basket data for possible errors.")
241 .c_str())
242 .log();
243 }
244
245 if (!close(totalNtl, origTotalNtl)) {
246 StructuredTradeErrorMessage(id(), "Synthetic CDO", "Error building trade",
247 ("Expected the total notional (" + std::to_string(totalNtl) + " = " +
248 std::to_string(totalRemainingNtl) + " + " + std::to_string(totalPriorNtl) +
249 ") to equal the total original notional (" + std::to_string(origTotalNtl) +
250 "), check the basket data for possible errors.")
251 .c_str())
252 .log();
253 }
254
255 DLOG("Finished building constituents using basket data.");
256
257 } else {
258
259 DLOG("Building constituents using CreditIndexReferenceDatum for ID " << qualifier_);
260
261 QL_REQUIRE(engineFactory->referenceData(),
262 "Trade " << id() << " has no basket data and there is no reference data manager.");
263 QL_REQUIRE(engineFactory->referenceData()->hasData(CreditIndexReferenceDatum::TYPE, qualifier_),
264 "Trade " << id() << " needs credit index reference data for ID " << qualifier_);
265 auto crd = QuantLib::ext::dynamic_pointer_cast<CreditIndexReferenceDatum>(
266 engineFactory->referenceData()->getData(CreditIndexReferenceDatum::TYPE, qualifier_));
267
268 Real totalRemainingWeight = 0.0;
269 Real totalPriorWeight = 0.0;
270 for (const auto& c : crd->constituents()) {
271
272 const auto& name = c.name();
273 auto weight = c.weight();
274 validateWeightRec(weight, name, "weight");
275
276 if (!close(0.0, weight)) {
277 DLOG("Adding underlying " << name << " with weight " << weight);
278 creditCurves.push_back(name);
279 basketNotionals.push_back(weight * origTotalNtl);
280 totalRemainingWeight += weight;
281 } else {
282 DLOG("Underlying " << name << " has weight " << weight << " so assuming a credit event occured.");
283 auto priorWeight = c.priorWeight();
284 QL_REQUIRE(priorWeight != Null<Real>(), "Expecting a valid prior weight for name " << name << ".");
285 validateWeightRec(priorWeight, name, "prior weight");
286 auto recovery = c.recovery();
287 QL_REQUIRE(recovery != Null<Real>(), "Expecting a valid recovery for name " << name << ".");
288 validateWeightRec(recovery, name, "recovery");
289 lostNotional += (1.0 - recovery) * priorWeight * origTotalNtl;
290 recoveredNotional += recovery * priorWeight * origTotalNtl;
291 totalPriorWeight += priorWeight;
292 }
293 }
294
295 Real totalWeight = totalRemainingWeight + totalPriorWeight;
296 DLOG("All Underlyings added, total remaining weight = " << totalRemainingWeight);
297 DLOG("All Underlyings added, total prior weight = " << totalPriorWeight);
298 DLOG("All Underlyings added, total weight = " << totalWeight);
299
300 if (!close(totalRemainingWeight, 1.0) && totalRemainingWeight > 1.0) {
301 ALOG("Total remaining weight is greater than 1, possible error in CreditIndexReferenceDatum");
302 }
303
304 if (!close(totalWeight, 1.0)) {
305 ALOG("Expected the total weight (" << totalWeight << " = " << totalRemainingWeight << " + "
306 << totalPriorWeight
307 << ") to equal 1, possible error in CreditIndexReferenceDatum");
308 }
309
310 DLOG("Finished building constituents using CreditIndexReferenceDatum for ID " << qualifier_);
311 }
312
313 // Lost notional eats into junior tranches and recovered amount reduces senior tranches.
314 // Throw exception if tranche has been completely written down. Should be removed from portfolio or will be
315 // ignored if continue on error is true. Maybe better to have instrument that gives an NPV of 0.
316 Real currTotalNtl = accumulate(basketNotionals.begin(), basketNotionals.end(), 0.0);
317 QL_REQUIRE(!close(currTotalNtl, 0.0), "Trade " << id() << " has a current total notional of 0.0.");
318 Real currEquityNtl = max(origEquityNtl - lostNotional, 0.0);
319 Real currSeniorNtl = max(origSeniorNtl - recoveredNotional, 0.0);
320 Real currTrancheNtl = origTrancheNtl - max(min(recoveredNotional - origSeniorNtl, origTrancheNtl), 0.0) -
321 max(min(lostNotional - origEquityNtl, origTrancheNtl), 0.0);
322 QL_REQUIRE(!close(currTrancheNtl, 0.0), "Trade " << id() << " has a current tranche notional of 0.0.");
323 Real adjAttachPoint = currEquityNtl / currTotalNtl;
324 Real adjDetachPoint = (currEquityNtl + currTrancheNtl) / currTotalNtl;
325 notional_ = currTrancheNtl;
326
327 DLOG("Current tranche notional: " << currTrancheNtl);
328 DLOG("Current equity notional: " << currEquityNtl);
329 DLOG("Current senior notional: " << currSeniorNtl);
330 DLOG("Current attachment point: " << adjAttachPoint);
331 DLOG("Current detachment point: " << adjDetachPoint);
332 DLOG("Current total notional: " << currTotalNtl);
333
334
335 const auto& market = engineFactory->market();
336 auto cdoEngineBuilder = QuantLib::ext::dynamic_pointer_cast<CdoEngineBuilder>(engineFactory->builder("SyntheticCDO"));
337 QL_REQUIRE(cdoEngineBuilder, "Trade " << id() << " needs a valid CdoEngineBuilder.");
338 const string& config = cdoEngineBuilder->configuration(MarketContext::pricing);
339
340
341 std::vector<Handle<DefaultProbabilityTermStructure>> dpts;
342 vector<Real> recoveryRates;
343
344 if (fixedRecovery != Null<Real>()) {
345 LOG("Set all recovery rates to " << fixedRecovery);
346 }
347 for (Size i = 0; i < creditCurves.size(); ++i) {
348 const string& cc = creditCurves[i];
349 Real mktRecoveryRate = market->recoveryRate(cc, config)->value();
350 recoveryRates.push_back(fixedRecovery != Null<Real>() ? fixedRecovery : mktRecoveryRate);
351 auto originalCurve = market->defaultCurve(cc, config)->curve();
352 dpts.push_back(originalCurve);
353 }
354
355 // TODO check if underlying is index cds
356
357 // Calibrate the underlying constituent curves so that the index cds pricing with underlying curves matches the
358 // prices of the index cds with flat index curve.
359
360 QuantLib::ext::shared_ptr<SimpleQuote> calibrationFactor = QuantLib::ext::make_shared<SimpleQuote>(1.0);
361
362 bool calibrateConstiuentCurves = cdoEngineBuilder->calibrateConstituentCurve() && isIndexTranche();
363
364 if (calibrateConstiuentCurves) {
365 // Adjustment factor is a simplified version of the O'Kane's Forward Default Probability Multiplier
366 // O'Kane 2008 - Modelling Single-name and Multi-name Credit Derivatives
367 // Chapter 10.6
368 LOG("Use calibrated constiuent curves");
369 QuantLib::ext::shared_ptr<IndexCreditDefaultSwap> indexCDS;
370 Real cdsFairSpreads = 0.0;
371 Real cdsNpvs = 0.0;
372 try {
373 Handle<YieldTermStructure> yts =
374 market->discountCurve(ccy.code(), cdoEngineBuilder->configuration(MarketContext::pricing));
375
376 Date cdsStartDate = indexStartDateHint() == Date() ? schedule.dates().front() : indexStartDateHint();
377
378 Schedule cdsSchedule(cdsStartDate, schedule.dates().back(),
379 schedule.tenor(), schedule.calendar(), Following,
380 schedule.terminationDateBusinessDayConvention(), schedule.rule(), false);
381 indexCDS = QuantLib::ext::make_shared<QuantExt::IndexCreditDefaultSwap>(
382 side, currTotalNtl, basketNotionals, 0.0, runningRate, cdsSchedule, bdc, dayCounter, settlesAccrual_,
383 protectionPaymentTime_, Date(), Date(), QuantLib::ext::shared_ptr<Claim>(),
384 lastPeriodDayCounter, rebatesAccrual_);
385 Handle<DefaultProbabilityTermStructure> indexCreditCurve =
386 indexCdsDefaultCurve(market, creditCurveIdWithTerm(), config)->curve();
387 Handle<Quote> indexCdsRecovery = market->recoveryRate(creditCurveIdWithTerm(), config);
388 auto indexPricingEngine =
389 QuantLib::ext::make_shared<QuantExt::MidPointIndexCdsEngine>(indexCreditCurve, indexCdsRecovery->value(), yts);
390 indexCDS->setPricingEngine(indexPricingEngine);
391 } catch (const std::exception& e) {
392 indexCDS = nullptr;
393 WLOG("CDO constiuent calibration failed to build index cds. Got "
394 << e.what());
395 }
396 if (indexCDS) {
397 cdsFairSpreads = indexCDS->fairSpreadClean();
398 cdsNpvs = indexCDS->NPV();
399 }
400
401 auto it = engineFactory->engineData()->globalParameters().find("RunType");
402 if (it != engineFactory->engineData()->globalParameters().end() && it->second != "PortfolioAnalyser") {
403
404 Handle<YieldTermStructure> yts =
405 market->discountCurve(ccy.code(), cdoEngineBuilder->configuration(MarketContext::pricing));
406
407 std::vector<Handle<DefaultProbabilityTermStructure>> wrapperCurves;
408 DLOG("Building wrapper curves for calibration");
409 for (size_t i = 0; i < creditCurves.size(); ++i) {
410 try {
411 const string& cc = creditCurves[i];
412 auto originalCurve = market->defaultCurve(cc, config)->curve();
413 wrapperCurves.push_back(
414 buildCalibratedConstiuentCurve(originalCurve, calibrationFactor));
415 } catch (const std::exception& e) {
416
417 WLOG("CDO constiuent calibration failed during building wrapper curve for "
418 << creditCurves[i] << ", skip this curve. Got "
419 << e.what());
420 }
421 }
422
423 if (wrapperCurves.size() == dpts.size() && indexCDS) {
424 LOG("Start bootstraping of the calibration factors");
425 dpts.swap(wrapperCurves);
426
427 auto cdsPricingEngineUnderlyingCurves =
428 QuantLib::ext::make_shared<QuantExt::MidPointIndexCdsEngine>(dpts, recoveryRates, yts);
429
430 indexCDS->setPricingEngine(cdsPricingEngineUnderlyingCurves);
431
432 try {
433 auto targetFunction = [&cdsNpvs, &calibrationFactor, &indexCDS](const double& factor) {
434 calibrationFactor->setValue(factor);
435 return cdsNpvs - indexCDS->NPV();
436 };
437
438 Brent solver;
439 double indexAdjustmentForUnderlyingCurves =
440 solver.solve(targetFunction, 1e-8, cdsFairSpreads / indexCDS->fairSpreadClean(), 0.001, 2);
441
442 DLOG("Calibration of indexterm " << io::iso_date(indexCDS->maturity())
443 << "successful, found solution "
444 << indexAdjustmentForUnderlyingCurves);
445 calibrationFactor->setValue(indexAdjustmentForUnderlyingCurves);
446 } catch (const std::exception& e) {
447 WLOG("Calibration failed, at pillar " << io::iso_date(indexCDS->maturity())
448 << ", set calibration factor to 1 (uncalibrated), got "
449 << e.what());
450 calibrationFactor->setValue(1.0);
451 }
452
453 LOG("Calibration results for creditCurve:" << creditCurveIdWithTerm());
454 LOG("Expiry \t CalibrationFactor \t NpvIntrinsic \t NpvIndexCurve \t NpvError \t "
455 "FairSpreadIntrinsic "
456 "\t FairSpreadIndexCurve \t FairSreadError");
457
458 LOG(io::iso_date(indexCDS->maturity())
459 << "\t" << calibrationFactor->value() << "\t" << indexCDS->NPV() << "\t" << cdsNpvs << "\t"
460 << indexCDS->NPV() - cdsNpvs << "\t" << indexCDS->fairSpreadClean() << "\t" << cdsFairSpreads
461 << "\t" << indexCDS->fairSpreadClean() - cdsFairSpreads);
462 }
463 }
464 }
465
466 // Create the instruments.
467 auto pool = QuantLib::ext::make_shared<Pool>();
468
469 CreditPortfolioSensitivityDecomposition sensitivityDecomposition = cdoEngineBuilder->sensitivityDecomposition();
470 useSensitivitySimplification_ = sensitivityDecomposition != CreditPortfolioSensitivityDecomposition::Underlying;
471 Handle<DefaultProbabilityTermStructure> clientCurve;
472 Handle<DefaultProbabilityTermStructure> baseCurve;
473 vector<Time> baseCurveTimes;
474 vector<Real> expLoss;
475
476 if (cdoEngineBuilder->optimizedSensitivityCalculation()) {
478 }
479
480 for (Size i = 0; i < creditCurves.size(); ++i) {
481 const string& cc = creditCurves[i];
482 DefaultProbKey key = NorthAmericaCorpDefaultKey(ccy, SeniorSec, Period(), 1.0);
483 Real recoveryRate = recoveryRates[i];
484 auto defaultCurve = dpts[i];
485 expLoss.push_back((1 - recoveryRate) * defaultCurve->defaultProbability(maturity_, true) * basketNotionals[i]);
486 std::pair<DefaultProbKey, Handle<DefaultProbabilityTermStructure>> p(key, defaultCurve);
487 vector<pair<DefaultProbKey, Handle<DefaultProbabilityTermStructure>>> probabilities(1, p);
488 // Empty default set. Adjustments have been made above to account for existing credit events.
489 Issuer issuer(probabilities, DefaultEventSet());
490 pool->add(cc, issuer, key);
491 DLOG("Issuer " << cc << " added to the pool.");
492 }
493
494 // If we use the simplification, we need a list of all credit curves and their weight to the basket
495
496 if (sensitivityDecomposition == CreditPortfolioSensitivityDecomposition::LossWeighted) {
497 basketConstituents_.clear();
498 Real totalWeight = std::accumulate(expLoss.begin(), expLoss.end(), 0.0);
499 for (Size basketIdx = 0; basketIdx < creditCurves.size(); basketIdx++) {
500 string& creditCurve = creditCurves[basketIdx];
501 Real weight = expLoss[basketIdx];
502 if (basketConstituents_.count(creditCurve) == 0) {
503 basketConstituents_[creditCurve] = weight / totalWeight;
504 } else {
505 basketConstituents_[creditCurve] += weight / totalWeight;
506 }
507 }
508 } else if (sensitivityDecomposition == CreditPortfolioSensitivityDecomposition::NotionalWeighted) {
509 basketConstituents_.clear();
510 Real totalWeight = std::accumulate(basketNotionals.begin(), basketNotionals.end(), 0.0);
511 for (Size basketIdx = 0; basketIdx < creditCurves.size(); basketIdx++) {
512 string& creditCurve = creditCurves[basketIdx];
513 Real weight = basketNotionals[basketIdx];
514 if (basketConstituents_.count(creditCurve) == 0) {
515 basketConstituents_[creditCurve] = weight / totalWeight;
516 } else {
517 basketConstituents_[creditCurve] += weight / totalWeight;
518 }
519 }
520 } else if (sensitivityDecomposition == CreditPortfolioSensitivityDecomposition::DeltaWeighted) {
521 basketConstituents_.clear();
522 Real totalWeight = 0;
523 for (Size basketIdx = 0; basketIdx < creditCurves.size(); basketIdx++) {
524 string& creditCurve = creditCurves[basketIdx];
525 Real notional = basketNotionals[basketIdx];
526 auto defaultCurve = market->defaultCurve(creditCurve, config)->curve();
527 Real constituentSurvivalProb = defaultCurve->survivalProbability(maturity_);
528 Time t = defaultCurve->timeFromReference(maturity_);
529 Real CR01 = t * constituentSurvivalProb * notional;
530 if (basketConstituents_.find(creditCurve) == basketConstituents_.end())
531 basketConstituents_.emplace(creditCurve, CR01);
532 totalWeight += CR01;
533 }
534 // Normalize
535 for (auto& decompWeight : basketConstituents_) {
536 decompWeight.second /= totalWeight;
537 }
538 }
539
540 // We will use a homogeneous pool loss model below if all notionals and recoveries are the same.
541 bool homogeneous = all_of(basketNotionals.begin(), basketNotionals.end(),
542 [&basketNotionals](Real ntl) { return close_enough(ntl, basketNotionals[0]); });
543 homogeneous = homogeneous && all_of(recoveryRates.begin(), recoveryRates.end(),
544 [&recoveryRates](Real rr) { return close_enough(rr, recoveryRates[0]); });
545
546
547 // vanilla holds the representation of the CDO, i.e. 1 * [0, Detach] instrument - 1 * [0, Attach] instrument,
548 // without the upfront fee payment which is added below to create the full final instrument.
549 QuantLib::ext::shared_ptr<Instrument> vanilla;
550
551 // Tranche from 0 to detachment point.
552 // If detachment point is 1.0, build an index CDS, i.e. [0, 100%] tranche. Otherwise an actual tranche.
553 QuantLib::ext::shared_ptr<Instrument> cdoD;
554 if (!close_enough(adjDetachPoint, 1.0)) {
555 DLOG("Building detachment tranche [0," << adjDetachPoint << "].");
556 // Set up the basket loss model.
557 auto basket =
558 QuantLib::ext::make_shared<QuantExt::Basket>(schedule[0], creditCurves, basketNotionals, pool, 0.0, adjDetachPoint);
559 basket->setLossModel(
560 cdoEngineBuilder->lossModel(qualifier(), recoveryRates, adjDetachPoint,
561 maturity_, homogeneous));
562
563 auto cdoDetach =
564 QuantLib::ext::make_shared<QuantExt::SyntheticCDO>(basket, side, schedule, 0.0, runningRate, dayCounter, bdc,
565 settlesAccrual_, protectionPaymentTime_, protectionStartDate, parseDate(upfrontDate_), boost::none, Null<Real>(), lastPeriodDayCounter);
566
567 cdoDetach->setPricingEngine(
568 cdoEngineBuilder->engine(ccy, false, {}, calibrationFactor, fixedRecovery));
569 setSensitivityTemplate(*cdoEngineBuilder);
570 cdoD = cdoDetach;
571
572 DLOG("Detachment tranche [0," << adjDetachPoint << "] built.");
573
574 } else {
575 DLOG("Detachment point is 1.0 so building an index CDS for [0,1.0] 'tranche'.");
576
577 /*auto cdsBuilder = QuantLib::ext::dynamic_pointer_cast<IndexCreditDefaultSwapEngineBuilder>(
578 engineFactory->builder("IndexCreditDefaultSwap"));
579 QL_REQUIRE(cdsBuilder, "Trade " << id() << " needs a IndexCreditDefaultSwapEngineBuilder.");
580 */
581 auto cds = QuantLib::ext::make_shared<QuantExt::IndexCreditDefaultSwap>(
582 side, currTotalNtl, basketNotionals, 0.0, runningRate, schedule, bdc, dayCounter, settlesAccrual_,
583 protectionPaymentTime_, protectionStartDate, Date(), QuantLib::ext::shared_ptr<Claim>(),
584 lastPeriodDayCounter, rebatesAccrual_, protectionStartDate, 3);
585
586 cds->setPricingEngine(
587 cdoEngineBuilder->engine(ccy, true, creditCurves, calibrationFactor, fixedRecovery));
588 setSensitivityTemplate(*cdoEngineBuilder);
589 cdoD = cds;
590
591 DLOG("Index CDS for [0,1.0] 'tranche' built.");
592 }
593
594 // Tranche from 0 to attachment point.
595 // If attachment point is 0.0, the instrument is simply the 0 to detachment point CDO built above. If attachment
596 // point is greater than 0, we build the 0 to attachment point tranche and the value of the CDO is the
597 // difference between the detachment point CDO and attachment point CDO.
598 if (close_enough(adjAttachPoint, 0.0)) {
599 DLOG("Attachment point is 0 so the instrument is built.");
600 vanilla = cdoD;
601 } else {
602 DLOG("Building attachment tranche [0," << adjAttachPoint << "].");
603
604 // Set up the basket loss model.
605 auto basket =
606 QuantLib::ext::make_shared<QuantExt::Basket>(schedule[0], creditCurves, basketNotionals, pool, 0.0, adjAttachPoint);
607 basket->setLossModel(cdoEngineBuilder->lossModel(qualifier(), recoveryRates, adjAttachPoint,
608 maturity_, homogeneous));
609
610 auto cdoA =
611 QuantLib::ext::make_shared<QuantExt::SyntheticCDO>(basket, side, schedule, 0.0, runningRate, dayCounter, bdc, settlesAccrual_, protectionPaymentTime_,
612 protectionStartDate, parseDate(upfrontDate_), boost::none, fixedRecovery, lastPeriodDayCounter);
613
614 cdoA->setPricingEngine(
615 cdoEngineBuilder->engine(ccy, false, {}, calibrationFactor, fixedRecovery));
616 setSensitivityTemplate(*cdoEngineBuilder);
617
618 DLOG("Attachment tranche [0," << adjAttachPoint << "] built.");
619
620 DLOG("Building attachment and detachment composite instrument.");
621
622 auto composite = QuantLib::ext::make_shared<CompositeInstrument>();
623 composite->add(cdoD);
624 composite->subtract(cdoA);
625 vanilla = composite;
626
627 DLOG("Attachment and detachment composite instrument built.");
628 }
629
630 // Add the upfront fee payment here. Positive number indicates payment from buyer of protection to seller of
631 // protection and a negative value indicates a payment from seller of protection to buyer of protection. The
632 // upfront fee provided is interpreted as a fractional amount of the original tranche notional.
633 if (upfrontDate != Date()) {
634 vector<QuantLib::ext::shared_ptr<Instrument>> insts;
635 vector<Real> mults;
636 Real upfrontAmount = upfrontFee_ * origTrancheNtl;
637 string configuration = cdoEngineBuilder->configuration(MarketContext::pricing);
638 maturity_ =
639 std::max(maturity_, addPremiums(insts, mults, 1.0, PremiumData(upfrontAmount, ccy.code(), upfrontDate),
640 side == Protection::Buyer ? -1.0 : 1.0, ccy, engineFactory, configuration));
641
642 instrument_ = QuantLib::ext::make_shared<VanillaInstrument>(vanilla, 1.0, insts, mults);
643
644 } else {
645 // If no upfront payment, the instrument is simply the vanilla instrument.
646 instrument_ = QuantLib::ext::make_shared<VanillaInstrument>(vanilla);
647 }
648
649 additionalData_["originalNotional"] = origTrancheNtl;
650 additionalData_["currentNotional"] = currTrancheNtl;
651
652 DLOG("CDO instrument built");
653}
654
655void SyntheticCDO::fromXML(XMLNode* node) {
656 Trade::fromXML(node);
657 XMLNode* cdoNode = XMLUtils::getChildNode(node, "CdoData");
658 QL_REQUIRE(cdoNode, "No CdoData Node");
659 qualifier_ = XMLUtils::getChildValue(cdoNode, "Qualifier", true);
660 protectionStart_ = XMLUtils::getChildValue(cdoNode, "ProtectionStart", true);
661 upfrontDate_ = XMLUtils::getChildValue(cdoNode, "UpfrontDate", false);
662
663 // zero if empty or missing
664 upfrontFee_ = Null<Real>();
665 string strUpfrontFee = XMLUtils::getChildValue(cdoNode, "UpfrontFee", false);
666 if (!strUpfrontFee.empty()) {
667 upfrontFee_ = parseReal(strUpfrontFee);
668 }
669 settlesAccrual_ = XMLUtils::getChildValueAsBool(cdoNode, "SettlesAccrual", false); // default = Y
670 rebatesAccrual_ = XMLUtils::getChildValueAsBool(cdoNode, "RebatesAccrual", false); // default = Y
671 protectionPaymentTime_ = QuantExt::CreditDefaultSwap::ProtectionPaymentTime::atDefault; // set default
672
673 // Recovery rate is Null<Real>() on a standard CDO i.e. if "FixedRecoveryRate" field is not populated.
674 recoveryRate_ = Null<Real>();
675 string strRecoveryRate = XMLUtils::getChildValue(node, "FixedRecoveryRate", false);
676 if (!strRecoveryRate.empty()) {
677 recoveryRate_ = parseReal(strRecoveryRate);
678 }
679
680 // for backwards compatibility only
681 if (auto c = XMLUtils::getChildNode(cdoNode, "PaysAtDefaultTime"))
682 if (!parseBool(XMLUtils::getNodeValue(c)))
683 protectionPaymentTime_ = QuantExt::CreditDefaultSwap::ProtectionPaymentTime::atPeriodEnd;
684 // new node overrides deprecated one, if both should be given
685 if (auto c = XMLUtils::getChildNode(cdoNode, "ProtectionPaymentTime")) {
686 if (XMLUtils::getNodeValue(c) == "atDefault")
687 protectionPaymentTime_ = QuantExt::CreditDefaultSwap::ProtectionPaymentTime::atDefault;
688 else if (XMLUtils::getNodeValue(c) == "atPeriodEnd")
689 protectionPaymentTime_ = QuantExt::CreditDefaultSwap::ProtectionPaymentTime::atPeriodEnd;
690 else if (XMLUtils::getNodeValue(c) == "atMaturity")
691 protectionPaymentTime_ = QuantExt::CreditDefaultSwap::ProtectionPaymentTime::atMaturity;
692 else {
693 QL_FAIL("protection payment time '" << XMLUtils::getNodeValue(c)
694 << "' not known, expected atDefault, atPeriodEnd, atMaturity");
695 }
696 }
697 attachmentPoint_ = XMLUtils::getChildValueAsDouble(cdoNode, "AttachmentPoint", true);
698 detachmentPoint_ = XMLUtils::getChildValueAsDouble(cdoNode, "DetachmentPoint", true);
699 XMLNode* legNode = XMLUtils::getChildNode(cdoNode, "LegData");
700 legData_.fromXML(legNode);
701 XMLNode* basketNode = XMLUtils::getChildNode(cdoNode, "BasketData");
702 if (basketNode)
703 basketData_.fromXML(basketNode);
704}
705
706XMLNode* SyntheticCDO::toXML(XMLDocument& doc) const {
707 XMLNode* node = Trade::toXML(doc);
708 XMLNode* cdoNode = doc.allocNode("CdoData");
709 XMLUtils::appendNode(node, cdoNode);
710 XMLUtils::addChild(doc, cdoNode, "Qualifier", qualifier_);
711 XMLUtils::addChild(doc, cdoNode, "ProtectionStart", protectionStart_);
712 if (!upfrontDate_.empty()) {
713 XMLUtils::addChild(doc, cdoNode, "UpfrontDate", upfrontDate_);
714 }
715 if (upfrontFee_ != Null<Real>()) {
716 XMLUtils::addChild(doc, cdoNode, "UpfrontFee", upfrontFee_);
717 }
718 XMLUtils::addChild(doc, cdoNode, "SettlesAccrual", settlesAccrual_);
719 if (!rebatesAccrual_)
720 XMLUtils::addChild(doc, node, "RebatesAccrual", rebatesAccrual_);
721 if (protectionPaymentTime_ == QuantExt::CreditDefaultSwap::ProtectionPaymentTime::atDefault)
722 XMLUtils::addChild(doc, cdoNode, "ProtectionPaymentTime", "atDefault");
723 else if (protectionPaymentTime_ == QuantExt::CreditDefaultSwap::ProtectionPaymentTime::atPeriodEnd)
724 XMLUtils::addChild(doc, cdoNode, "ProtectionPaymentTime", "atPeriodEnd");
725 else if (protectionPaymentTime_ == QuantExt::CreditDefaultSwap::ProtectionPaymentTime::atMaturity)
726 XMLUtils::addChild(doc, cdoNode, "ProtectionPaymentTime", "atMaturity");
727 else {
728 QL_FAIL("CreditDefaultSwapData::toXML(): unexpected protectionPaymentTime_");
729 }
730 if (recoveryRate_ != Null<Real>())
731 XMLUtils::addChild(doc, node, "FixedRecoveryRate", recoveryRate_);
732 XMLUtils::addChild(doc, cdoNode, "AttachmentPoint", attachmentPoint_);
733 XMLUtils::addChild(doc, cdoNode, "DetachmentPoint", detachmentPoint_);
734 XMLUtils::appendNode(cdoNode, legData_.toXML(doc));
735 XMLUtils::appendNode(cdoNode, basketData_.toXML(doc));
736 return node;
737}
738vector<Time>
739SyntheticCDO::extractTimeGridDefaultCurve(const QuantLib::Handle<QuantLib::DefaultProbabilityTermStructure> &dpts) {
740 if (auto c = QuantLib::ext::dynamic_pointer_cast<SpreadedSurvivalProbabilityTermStructure>(*dpts)) {
741 return SyntheticCDO::extractTimeGridDefaultCurve(c->referenceCurve());
742 } else if (auto c = QuantLib::ext::dynamic_pointer_cast<InterpolatedSurvivalProbabilityCurve<LogLinear>>(*dpts)) {
743 return c->times();
744 } else if (auto c = QuantLib::ext::dynamic_pointer_cast<InterpolatedHazardRateCurve<BackwardFlat>>(*dpts)) {
745 return c->times();
746 } else if (auto c = QuantLib::ext::dynamic_pointer_cast<SurvivalProbabilityCurve<LogLinear>>(*dpts)) {
747 return c->times();
748 } else {
749 WLOG("Extraction of time points failed, unsupported default probability termstructure");
750 return vector<double>();
751 }
752}
753
754QuantLib::Handle<QuantLib::DefaultProbabilityTermStructure>
755SyntheticCDO::buildCalibratedConstiuentCurve(const QuantLib::Handle<QuantLib::DefaultProbabilityTermStructure>& curve,
756 const QuantLib::ext::shared_ptr<SimpleQuote>& calibrationFactor) {
757 if (!calibrationFactor) {
758 return curve;
759 }
760 auto curveTimes = extractTimeGridDefaultCurve(curve);
761 vector<Handle<Quote>> spreads;
762 for (size_t timeIdx = 0; timeIdx < curveTimes.size(); ++timeIdx) {
763 auto sp = curve->survivalProbability(curveTimes[timeIdx]);
764 auto compQuote = QuantLib::ext::make_shared<CompositeQuote<std::function<double(double, double)>>>(
765 Handle<Quote>(calibrationFactor),
766 Handle<Quote>(QuantLib::ext::make_shared<SimpleQuote>(sp)),
767 [](const double q1, const double q2) -> double { return std::exp(-(1 - q1) * std::log(q2)); });
768 spreads.push_back(Handle<Quote>(compQuote));
769 }
770 Handle<DefaultProbabilityTermStructure> targetCurve = Handle<DefaultProbabilityTermStructure>(
771 QuantLib::ext::make_shared<SpreadedSurvivalProbabilityTermStructure>(curve, curveTimes, spreads));
772 if (curve->allowsExtrapolation()) {
773 targetCurve->enableExtrapolation();
774 }
775 return targetCurve;
776}
777
778std::string SyntheticCDO::creditCurveIdWithTerm() const {
779 auto p = ore::data::splitCurveIdWithTenor(qualifier());
780 if (p.second != 0 * Days || !isIndexTranche())
781 return qualifier();
782
783 QuantLib::Schedule s = makeSchedule(leg().schedule());
784 if (s.dates().empty())
785 return p.first;
786 QuantLib::Period t = QuantExt::implyIndexTerm(
787 indexStartDateHint_ == Date() ? s.dates().front() : indexStartDateHint_, s.dates().back());
788 if (t != 0 * Days)
789 return p.first + "_" + ore::data::to_string(t);
790 return p.first;
791}
792
793
794} // namespace data
795} // namespace ore
Mid point CDO engines cached by currency.
QuantLib::CreditDefaultSwap::ProtectionPaymentTime protectionPaymentTime_
const boost::shared_ptr< QuantExt::Basket > & basket() const
Real recoveryRate() const
void log() const
generate Boost log record to pass to corresponding sinks
Definition: log.cpp:491
Serializable object holding premium data.
Definition: premiumdata.hpp:37
Utility class for Structured Trade errors, contains the Trade ID and Type.
virtual void build(const QuantLib::ext::shared_ptr< EngineFactory > &) override
Definition: cdo.cpp:75
Small XML Document wrapper class.
Definition: xmlutils.hpp:65
XMLNode * allocNode(const string &nodeName)
util functions that wrap rapidxml
Definition: xmlutils.cpp:132
SafeStack< ValueType > value
Date parseDate(const string &s)
Convert std::string to QuantLib::Date.
Definition: parsers.cpp:51
Currency parseCurrency(const string &s)
Convert text to QuantLib::Currency.
Definition: parsers.cpp:290
BusinessDayConvention parseBusinessDayConvention(const string &s)
Convert text to QuantLib::BusinessDayConvention.
Definition: parsers.cpp:173
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
DayCounter parseDayCounter(const string &s)
Convert text to QuantLib::DayCounter.
Definition: parsers.cpp:209
Map text representations to QuantLib/QuantExt types.
leg data model and serialization
Classes and functions for log message handling.
@ data
Definition: log.hpp:77
#define LOG(text)
Logging Macro (Level = Notice)
Definition: log.hpp:552
#define DLOG(text)
Logging Macro (Level = Debug)
Definition: log.hpp:554
#define ALOG(text)
Logging Macro (Level = Alert)
Definition: log.hpp:544
#define WLOG(text)
Logging Macro (Level = Warning)
Definition: log.hpp:550
#define TLOG(text)
Logging Macro (Level = Data)
Definition: log.hpp:556
market data related utilties
RandomVariable max(RandomVariable x, const RandomVariable &y)
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
RandomVariable abs(RandomVariable x)
QuantLib::Period implyIndexTerm(const Date &startDate, const Date &endDate)
RandomVariable min(RandomVariable x, const RandomVariable &y)
QuantLib::Handle< QuantExt::CreditCurve > indexCdsDefaultCurve(const QuantLib::ext::shared_ptr< Market > &market, const std::string &creditCurveId, const std::string &config)
Definition: marketdata.cpp:243
std::string to_string(const LocationInfo &l)
Definition: ast.cpp:28
std::vector< Handle< DefaultProbabilityTermStructure > > buildPerformanceOptimizedDefaultCurves(const std::vector< Handle< DefaultProbabilityTermStructure > > &curves)
Definition: cdo.cpp:40
CreditPortfolioSensitivityDecomposition
Enumeration CreditPortfolioSensitivityDecomposition.
Definition: parsers.hpp:568
std::pair< std::string, QuantLib::Period > splitCurveIdWithTenor(const std::string &creditCurveId)
Definition: marketdata.cpp:231
Leg makeFixedLeg(const LegData &data, const QuantLib::Date &openEndDateReplacement)
Definition: legdata.cpp:950
Schedule makeSchedule(const ScheduleDates &data)
Definition: schedule.cpp:263
Serializable Credit Default Swap.
Definition: namespaces.docs:23
Map text representations to QuantLib/QuantExt types.
Structured Trade Error class.
Swap trade data model and serialization.
string conversion utilities
string name