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
29using namespace std;
30
31namespace 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

source code of quantlib/ql/experimental/credit/basket.cpp