| 1 | /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
| 2 | |
| 3 | /* |
| 4 | Copyright (C) 2008 Roland Lichters |
| 5 | Copyright (C) 2009, 2014 Jose Aparicio |
| 6 | |
| 7 | This file is part of QuantLib, a free-software/open-source library |
| 8 | for financial quantitative analysts and developers - http://quantlib.org/ |
| 9 | |
| 10 | QuantLib is free software: you can redistribute it and/or modify it |
| 11 | under the terms of the QuantLib license. You should have received a |
| 12 | copy of the license along with this program; if not, please email |
| 13 | <quantlib-dev@lists.sf.net>. The license is also available online at |
| 14 | <http://quantlib.org/license.shtml>. |
| 15 | |
| 16 | This program is distributed in the hope that it will be useful, but WITHOUT |
| 17 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 18 | FOR A PARTICULAR PURPOSE. See the license for more details. |
| 19 | */ |
| 20 | |
| 21 | #include <ql/experimental/credit/basket.hpp> |
| 22 | #include <ql/experimental/credit/defaultlossmodel.hpp> |
| 23 | #include <ql/experimental/credit/loss.hpp> |
| 24 | #include <ql/time/daycounters/actualactual.hpp> |
| 25 | #include <algorithm> |
| 26 | #include <numeric> |
| 27 | #include <utility> |
| 28 | |
| 29 | using namespace std; |
| 30 | |
| 31 | namespace QuantLib { |
| 32 | |
| 33 | Basket::Basket(const Date& refDate, |
| 34 | const vector<string>& names, |
| 35 | vector<Real> notionals, |
| 36 | ext::shared_ptr<Pool> pool, |
| 37 | Real attachment, |
| 38 | Real detachment, |
| 39 | ext::shared_ptr<Claim> claim) |
| 40 | : notionals_(std::move(notionals)), pool_(std::move(pool)), claim_(std::move(claim)), |
| 41 | attachmentRatio_(attachment), detachmentRatio_(detachment), basketNotional_(0.0), |
| 42 | attachmentAmount_(0.0), detachmentAmount_(0.0), trancheNotional_(0.0), refDate_(refDate) { |
| 43 | QL_REQUIRE(!notionals_.empty(), "notionals empty" ); |
| 44 | QL_REQUIRE (attachmentRatio_ >= 0 && |
| 45 | attachmentRatio_ <= detachmentRatio_ && |
| 46 | detachmentRatio_ <= 1, |
| 47 | "invalid attachment/detachment ratio" ); |
| 48 | QL_REQUIRE(pool_, "Empty pool pointer." ); |
| 49 | QL_REQUIRE(notionals_.size() == pool_->size(), |
| 50 | "unmatched data entry sizes in basket" ); |
| 51 | |
| 52 | // registrations relevant to the loss status, not to the expected |
| 53 | // loss values; those are through models. |
| 54 | registerWith(h: Settings::instance().evaluationDate()); |
| 55 | registerWith(h: claim_); |
| 56 | |
| 57 | computeBasket(); |
| 58 | |
| 59 | // At this point Issuers in the pool might or might not have |
| 60 | // probability term structures for the defultKeys(eventType+ |
| 61 | // currency+seniority) entering in this basket. This is not |
| 62 | // necessarily a problem. |
| 63 | for (Real notional : notionals_) { |
| 64 | basketNotional_ += notional; |
| 65 | attachmentAmount_ += notional * attachmentRatio_; |
| 66 | detachmentAmount_ += notional * detachmentRatio_; |
| 67 | } |
| 68 | trancheNotional_ = detachmentAmount_ - attachmentAmount_; |
| 69 | } |
| 70 | |
| 71 | /*\todo Alternatively send a relinkable handle so it can be changed from |
| 72 | the outside. In that case reconsider the observability chain. |
| 73 | */ |
| 74 | void Basket::setLossModel( |
| 75 | const ext::shared_ptr<DefaultLossModel>& lossModel) { |
| 76 | |
| 77 | if (lossModel_ != nullptr) |
| 78 | unregisterWith(h: lossModel_); |
| 79 | lossModel_ = lossModel; |
| 80 | if (lossModel_ != nullptr) { |
| 81 | //recovery quotes, defaults(once Issuer is observable)etc might |
| 82 | // trigger us: |
| 83 | registerWith(h: lossModel_); |
| 84 | } |
| 85 | LazyObject::update(); //<- just set calc=false |
| 86 | } |
| 87 | |
| 88 | void Basket::performCalculations() const { |
| 89 | // Calculations for status |
| 90 | computeBasket();// or we might be called from an statistic member |
| 91 | // without being initialized yet (first called) |
| 92 | QL_REQUIRE(lossModel_, "Basket has no default loss model assigned." ); |
| 93 | |
| 94 | /* The model must notify us if the another basket calls it for |
| 95 | reasignment. The basket works as an argument to the deafult loss models |
| 96 | so, even if the models dont cache anything, they will be using the wrong |
| 97 | default TS. \todo: This has a possible optimization: the basket |
| 98 | incorporates trancheability and many models do their compuations |
| 99 | independently of that (some do but do it inefficiently when asked for |
| 100 | two tranches on the same basket; e,g, recursive model) so it might be |
| 101 | more efficient sending the pool only; however the modtionals and other |
| 102 | basket info are still used.*/ |
| 103 | lossModel_->setBasket(const_cast<Basket*>(this)); |
| 104 | } |
| 105 | |
| 106 | Real Basket::notional() const { |
| 107 | return std::accumulate(first: notionals_.begin(), last: notionals_.end(), init: Real(0.0)); |
| 108 | } |
| 109 | |
| 110 | vector<Real> Basket::probabilities(const Date& d) const { |
| 111 | vector<Real> prob(size()); |
| 112 | vector<DefaultProbKey> defKeys = defaultKeys(); |
| 113 | for (Size j = 0; j < size(); j++) |
| 114 | prob[j] = pool_->get(name: pool_->names()[j]).defaultProbability( |
| 115 | key: defKeys[j])->defaultProbability(d); |
| 116 | return prob; |
| 117 | } |
| 118 | |
| 119 | Real Basket::cumulatedLoss(const Date& endDate) const { |
| 120 | QL_REQUIRE(endDate >= refDate_, |
| 121 | "Target date lies before basket inception" ); |
| 122 | Real loss = 0.0; |
| 123 | for (Size i = 0; i < size(); i++) { |
| 124 | ext::shared_ptr<DefaultEvent> credEvent = |
| 125 | pool_->get(name: pool_->names()[i]).defaultedBetween(start: refDate_, |
| 126 | end: endDate, key: pool_->defaultKeys()[i]); |
| 127 | if (credEvent != nullptr) { |
| 128 | /* \todo If the event has not settled one would need to |
| 129 | introduce some model recovery rate (independently of a loss |
| 130 | model) This remains to be done. |
| 131 | */ |
| 132 | if(credEvent->hasSettled()) |
| 133 | loss += claim_->amount(defaultDate: credEvent->date(), |
| 134 | // notionals_[i], |
| 135 | notional: exposure(name: pool_->names()[i], credEvent->date()), |
| 136 | recoveryRate: credEvent->settlement().recoveryRate( |
| 137 | sen: pool_->defaultKeys()[i].seniority())); |
| 138 | } |
| 139 | } |
| 140 | return loss; |
| 141 | } |
| 142 | |
| 143 | Real Basket::settledLoss(const Date& endDate) const { |
| 144 | QL_REQUIRE(endDate >= refDate_, |
| 145 | "Target date lies before basket inception" ); |
| 146 | |
| 147 | Real loss = 0.0; |
| 148 | for (Size i = 0; i < size(); i++) { |
| 149 | ext::shared_ptr<DefaultEvent> credEvent = |
| 150 | pool_->get(name: pool_->names()[i]).defaultedBetween(start: refDate_, |
| 151 | end: endDate, key: pool_->defaultKeys()[i]); |
| 152 | if (credEvent != nullptr) { |
| 153 | if(credEvent->hasSettled()) { |
| 154 | loss += claim_->amount(defaultDate: credEvent->date(), |
| 155 | //notionals_[i], |
| 156 | notional: exposure(name: pool_->names()[i], credEvent->date()), |
| 157 | //NOtice I am requesting an exposure in the past... |
| 158 | /* also the seniority does not belong to the |
| 159 | counterparty anymore but to the position.....*/ |
| 160 | recoveryRate: credEvent->settlement().recoveryRate( |
| 161 | sen: pool_->defaultKeys()[i].seniority())); |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | return loss; |
| 166 | } |
| 167 | |
| 168 | Real Basket::remainingNotional() const { |
| 169 | return evalDateRemainingNot_; |
| 170 | } |
| 171 | |
| 172 | std::vector<Size> Basket::liveList(const Date& endDate) const { |
| 173 | std::vector<Size> calcBufferLiveList; |
| 174 | for (Size i = 0; i < size(); i++) |
| 175 | if (!pool_->get(name: pool_->names()[i]).defaultedBetween( |
| 176 | start: refDate_, |
| 177 | end: endDate, |
| 178 | key: pool_->defaultKeys()[i])) |
| 179 | calcBufferLiveList.push_back(x: i); |
| 180 | |
| 181 | return calcBufferLiveList; |
| 182 | } |
| 183 | |
| 184 | Real Basket::remainingNotional(const Date& endDate) const { |
| 185 | Real notional = 0; |
| 186 | vector<DefaultProbKey> defKeys = defaultKeys(); |
| 187 | for (Size i = 0; i < size(); i++) { |
| 188 | if (!pool_->get(name: pool_->names()[i]).defaultedBetween(start: refDate_, |
| 189 | end: endDate, |
| 190 | key: defKeys[i])) |
| 191 | notional += notionals_[i]; |
| 192 | } |
| 193 | return notional; |
| 194 | } |
| 195 | |
| 196 | vector<Real> Basket::remainingNotionals(const Date& endDate) const |
| 197 | { |
| 198 | QL_REQUIRE(endDate >= refDate_, |
| 199 | "Target date lies before basket inception" ); |
| 200 | |
| 201 | std::vector<Real> calcBufferNotionals; |
| 202 | const std::vector<Size>& alive = liveList(endDate); |
| 203 | for(Size i=0; i<alive.size(); i++) |
| 204 | calcBufferNotionals.push_back( |
| 205 | x: exposure(name: pool_->names()[i], endDate) |
| 206 | );// some better way to trim it? |
| 207 | return calcBufferNotionals; |
| 208 | } |
| 209 | |
| 210 | std::vector<Probability> Basket::remainingProbabilities(const Date& d) const |
| 211 | { |
| 212 | QL_REQUIRE(d >= refDate_, "Target date lies before basket inception" ); |
| 213 | vector<Real> prob; |
| 214 | const std::vector<Size>& alive = liveList(); |
| 215 | |
| 216 | for(Size i=0; i<alive.size(); i++) |
| 217 | prob.push_back(x: pool_->get(name: pool_->names()[i]).defaultProbability( |
| 218 | key: pool_->defaultKeys()[i])->defaultProbability(d, extrapolate: true)); |
| 219 | return prob; |
| 220 | } |
| 221 | |
| 222 | /* It is supossed to return the addition of ALL notionals from the |
| 223 | requested ctpty......*/ |
| 224 | Real Basket::exposure(const std::string& name, const Date& d) const { |
| 225 | //'this->names_' contains duplicates, contrary to 'pool->names' |
| 226 | auto match = std::find(first: pool_->names().begin(), last: pool_->names().end(), val: name); |
| 227 | QL_REQUIRE(match != pool_->names().end(), "Name not in basket." ); |
| 228 | Real totalNotional = 0.; |
| 229 | do{ |
| 230 | totalNotional += |
| 231 | // NOT IMPLEMENTED YET: |
| 232 | //positions_[std::distance(names_.begin(), match)]->expectedExposure(d); |
| 233 | notionals_[std::distance(first: pool_->names().begin(), last: match)]; |
| 234 | ++match; |
| 235 | match = std::find(first: match, last: pool_->names().end(), val: name); |
| 236 | }while(match != pool_->names().end()); |
| 237 | |
| 238 | return totalNotional; |
| 239 | //Size position = std::distance(poolNames.begin(), |
| 240 | // std::find(poolNames.begin(), poolNames.end(), name)); |
| 241 | //QL_REQUIRE(position < pool_->size(), "Name not in pool list"); |
| 242 | |
| 243 | //return positions_[position]->expectedExposure(d); |
| 244 | } |
| 245 | |
| 246 | std::vector<std::string> Basket::remainingNames(const Date& endDate) const |
| 247 | { |
| 248 | // maybe return zero directly instead?: |
| 249 | QL_REQUIRE(endDate >= refDate_, |
| 250 | "Target date lies before basket inception" ); |
| 251 | |
| 252 | const std::vector<Size>& alive = liveList(endDate); |
| 253 | std::vector<std::string> calcBufferNames; |
| 254 | calcBufferNames.reserve(n: alive.size()); |
| 255 | for (unsigned long i : alive) |
| 256 | calcBufferNames.push_back(x: pool_->names()[i]); |
| 257 | return calcBufferNames; |
| 258 | } |
| 259 | |
| 260 | vector<DefaultProbKey> Basket::remainingDefaultKeys(const Date& endDate) const |
| 261 | { |
| 262 | QL_REQUIRE(endDate >= refDate_, |
| 263 | "Target date lies before basket inception" ); |
| 264 | |
| 265 | const std::vector<Size>& alive = liveList(endDate); |
| 266 | vector<DefaultProbKey> defKeys; |
| 267 | defKeys.reserve(n: alive.size()); |
| 268 | for (unsigned long i : alive) |
| 269 | defKeys.push_back(x: pool_->defaultKeys()[i]); |
| 270 | return defKeys; |
| 271 | } |
| 272 | |
| 273 | Size Basket::remainingSize() const { |
| 274 | return evalDateLiveList_.size(); |
| 275 | } |
| 276 | |
| 277 | Size Basket::remainingSize(const Date& d) const { |
| 278 | return remainingDefaultKeys(endDate: d).size(); |
| 279 | } |
| 280 | |
| 281 | /* computed on the inception values, notice the positions might have |
| 282 | amortized or changed in value and the total outstanding notional might |
| 283 | differ from the inception one.*/ |
| 284 | Real Basket::remainingDetachmentAmount(const Date& endDate) const { |
| 285 | QL_REQUIRE(endDate >= refDate_, |
| 286 | "Target date lies before basket inception" ); |
| 287 | return detachmentAmount_; |
| 288 | } |
| 289 | |
| 290 | Real Basket::remainingAttachmentAmount(const Date& endDate) const { |
| 291 | // maybe return zero directly instead?: |
| 292 | QL_REQUIRE(endDate >= refDate_, |
| 293 | "Target date lies before basket inception" ); |
| 294 | Real loss = settledLoss(endDate); |
| 295 | return std::min(a: detachmentAmount_, b: attachmentAmount_ + |
| 296 | std::max(a: 0.0, b: loss - attachmentAmount_)); |
| 297 | } |
| 298 | |
| 299 | Probability Basket::probOverLoss(const Date& d, Real lossFraction) const { |
| 300 | // convert initial basket fraction to remaining basket fraction |
| 301 | calculate(); |
| 302 | // if eaten up all the tranche the prob of losing any amount is 1 |
| 303 | // (we have already lost it) |
| 304 | if(evalDateRemainingNot_ == 0.) return 1.; |
| 305 | |
| 306 | // Turn to live (remaining) tranche units to feed into the model request |
| 307 | Real xPtfl = attachmentAmount_ + |
| 308 | (detachmentAmount_-attachmentAmount_)*lossFraction; |
| 309 | Real xPrim = (xPtfl- evalDateAttachAmount_)/ |
| 310 | (detachmentAmount_-evalDateAttachAmount_); |
| 311 | // in live tranche fractional units |
| 312 | // if the level falls within realized losses the prob is 1. |
| 313 | if(xPtfl < 0.) return 1.; |
| 314 | |
| 315 | return lossModel_->probOverLoss(d, lossFraction: xPrim); |
| 316 | } |
| 317 | |
| 318 | Real Basket::percentile(const Date& d, Probability prob) const { |
| 319 | calculate(); |
| 320 | return lossModel_->percentile(d, percentile: prob); |
| 321 | } |
| 322 | |
| 323 | Real Basket::expectedTrancheLoss(const Date& d) const { |
| 324 | calculate(); |
| 325 | return cumulatedLoss() + lossModel_->expectedTrancheLoss(d); |
| 326 | } |
| 327 | |
| 328 | std::vector<Real> Basket::splitVaRLevel(const Date& date, Real loss) const { |
| 329 | calculate(); |
| 330 | return lossModel_->splitVaRLevel(d: date, loss); |
| 331 | } |
| 332 | |
| 333 | Real Basket::expectedShortfall(const Date& d, Probability prob) const { |
| 334 | calculate(); |
| 335 | return lossModel_->expectedShortfall(d, percentile: prob); |
| 336 | } |
| 337 | |
| 338 | std::map<Real, Probability> Basket::lossDistribution(const Date& d) const { |
| 339 | calculate(); |
| 340 | return lossModel_->lossDistribution(d); |
| 341 | } |
| 342 | |
| 343 | std::vector<Probability> |
| 344 | Basket::probsBeingNthEvent(Size n, const Date& d) const { |
| 345 | |
| 346 | Size alreadyDefaulted = pool_->size() - remainingNames().size(); |
| 347 | if(alreadyDefaulted >=n) |
| 348 | return std::vector<Probability>(remainingNames().size(), 0.); |
| 349 | |
| 350 | calculate(); |
| 351 | return lossModel_->probsBeingNthEvent(n: n-alreadyDefaulted, d); |
| 352 | } |
| 353 | |
| 354 | Real Basket::defaultCorrelation(const Date& d, Size iName, Size jName) const{ |
| 355 | calculate(); |
| 356 | return lossModel_->defaultCorrelation(d, iName, jName); |
| 357 | |
| 358 | } |
| 359 | |
| 360 | /*! Returns the probaility of having a given or larger number of |
| 361 | defaults in the basket portfolio at a given time. |
| 362 | */ |
| 363 | Probability Basket::probAtLeastNEvents(Size n, const Date& d) const{ |
| 364 | calculate(); |
| 365 | return lossModel_->probAtLeastNEvents(n, d); |
| 366 | |
| 367 | } |
| 368 | |
| 369 | Real Basket::recoveryRate(const Date& d, Size iName) const { |
| 370 | calculate(); |
| 371 | return |
| 372 | lossModel_->expectedRecovery(d, iName, pool_->defaultKeys()[iName]); |
| 373 | } |
| 374 | |
| 375 | } |
| 376 | |