Coverage Report

Created: 2026-01-25 06:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/experimental/credit/basket.cpp
Line
Count
Source
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
 <https://www.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
0
    : notionals_(std::move(notionals)), pool_(std::move(pool)), claim_(std::move(claim)),
41
0
      attachmentRatio_(attachment), detachmentRatio_(detachment), basketNotional_(0.0),
42
0
      attachmentAmount_(0.0), detachmentAmount_(0.0), trancheNotional_(0.0), refDate_(refDate) {
43
0
        QL_REQUIRE(!notionals_.empty(), "notionals empty");
44
0
        QL_REQUIRE (attachmentRatio_ >= 0 &&
45
0
                    attachmentRatio_ <= detachmentRatio_ &&
46
0
                    detachmentRatio_ <= 1,
47
0
                    "invalid attachment/detachment ratio");
48
0
        QL_REQUIRE(pool_, "Empty pool pointer.");
49
0
        QL_REQUIRE(notionals_.size() == pool_->size(), 
50
0
                   "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
0
        registerWith(Settings::instance().evaluationDate());
55
0
        registerWith(claim_);
56
57
0
        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
0
        for (Real notional : notionals_) {
64
0
            basketNotional_ += notional;
65
0
            attachmentAmount_ += notional * attachmentRatio_;
66
0
            detachmentAmount_ += notional * detachmentRatio_;
67
0
        }
68
0
        trancheNotional_ = detachmentAmount_ - attachmentAmount_;
69
0
    }
Unexecuted instantiation: QuantLib::Basket::Basket(QuantLib::Date const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::vector<double, std::__1::allocator<double> >, boost::shared_ptr<QuantLib::Pool>, double, double, boost::shared_ptr<QuantLib::Claim>)
Unexecuted instantiation: QuantLib::Basket::Basket(QuantLib::Date const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::vector<double, std::__1::allocator<double> >, boost::shared_ptr<QuantLib::Pool>, double, double, boost::shared_ptr<QuantLib::Claim>)
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
0
        const ext::shared_ptr<DefaultLossModel>& lossModel) {
76
77
0
        if (lossModel_ != nullptr)
78
0
            unregisterWith(lossModel_);
79
0
        lossModel_ = lossModel;
80
0
        if (lossModel_ != nullptr) {
81
            //recovery quotes, defaults(once Issuer is observable)etc might 
82
            //  trigger us:
83
0
            registerWith(lossModel_);
84
0
        }
85
0
        LazyObject::update(); //<- just set calc=false
86
0
    }
87
88
0
    void Basket::performCalculations() const {
89
        // Calculations for status
90
0
        computeBasket();// or we might be called from an statistic member 
91
                        // without being initialized yet (first called)
92
0
        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
0
        lossModel_->setBasket(const_cast<Basket*>(this));
104
0
    }
105
106
0
    Real Basket::notional() const {
107
0
        return std::accumulate(notionals_.begin(), notionals_.end(), Real(0.0));
108
0
    }
109
110
0
    vector<Real> Basket::probabilities(const Date& d) const {
111
0
        vector<Real> prob(size());
112
0
        vector<DefaultProbKey> defKeys = defaultKeys();
113
0
        for (Size j = 0; j < size(); j++)
114
0
            prob[j] = pool_->get(pool_->names()[j]).defaultProbability(
115
0
                defKeys[j])->defaultProbability(d);
116
0
        return prob;
117
0
    }
118
119
0
    Real Basket::cumulatedLoss(const Date& endDate) const {
120
0
        QL_REQUIRE(endDate >= refDate_, 
121
0
            "Target date lies before basket inception");
122
0
        Real loss = 0.0;
123
0
        for (Size i = 0; i < size(); i++) {
124
0
            ext::shared_ptr<DefaultEvent> credEvent =
125
0
                pool_->get(pool_->names()[i]).defaultedBetween(refDate_,
126
0
                    endDate, pool_->defaultKeys()[i]);
127
0
            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
0
                if(credEvent->hasSettled())
133
0
                    loss += claim_->amount(credEvent->date(),
134
                            // notionals_[i],
135
0
                            exposure(pool_->names()[i], credEvent->date()),
136
0
                            credEvent->settlement().recoveryRate(
137
0
                                pool_->defaultKeys()[i].seniority()));
138
0
            }
139
0
        }
140
0
        return loss;
141
0
    }
142
143
0
    Real Basket::settledLoss(const Date& endDate) const {
144
0
        QL_REQUIRE(endDate >= refDate_, 
145
0
            "Target date lies before basket inception");
146
        
147
0
        Real loss = 0.0;
148
0
        for (Size i = 0; i < size(); i++) {
149
0
            ext::shared_ptr<DefaultEvent> credEvent =
150
0
                pool_->get(pool_->names()[i]).defaultedBetween(refDate_,
151
0
                    endDate, pool_->defaultKeys()[i]);
152
0
            if (credEvent != nullptr) {
153
0
                if(credEvent->hasSettled()) {
154
0
                    loss += claim_->amount(credEvent->date(),
155
                            //notionals_[i],
156
0
                            exposure(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
0
                            credEvent->settlement().recoveryRate(
161
0
                                pool_->defaultKeys()[i].seniority()));
162
0
                }
163
0
            }
164
0
        }
165
0
        return loss;
166
0
    }
167
168
0
    Real Basket::remainingNotional() const {
169
0
        return evalDateRemainingNot_;
170
0
    }
171
172
0
    std::vector<Size> Basket::liveList(const Date& endDate) const {
173
0
        std::vector<Size> calcBufferLiveList;
174
0
        for (Size i = 0; i < size(); i++)
175
0
            if (!pool_->get(pool_->names()[i]).defaultedBetween(
176
0
                    refDate_,
177
0
                    endDate,
178
0
                    pool_->defaultKeys()[i]))
179
0
                calcBufferLiveList.push_back(i);
180
181
0
        return calcBufferLiveList;
182
0
    }
183
184
0
    Real Basket::remainingNotional(const Date& endDate) const {
185
0
        Real notional = 0;
186
0
        vector<DefaultProbKey> defKeys = defaultKeys();
187
0
        for (Size i = 0; i < size(); i++) {
188
0
            if (!pool_->get(pool_->names()[i]).defaultedBetween(refDate_,
189
0
                                                        endDate,
190
0
                                                        defKeys[i]))
191
0
                notional += notionals_[i];
192
0
        }
193
0
        return notional;
194
0
    }
195
196
    vector<Real> Basket::remainingNotionals(const Date& endDate) const 
197
0
    {
198
0
        QL_REQUIRE(endDate >= refDate_, 
199
0
            "Target date lies before basket inception");
200
201
0
        std::vector<Real> calcBufferNotionals;
202
0
        const std::vector<Size>& alive = liveList(endDate);
203
0
        calcBufferNotionals.reserve(alive.size());
204
0
        for(Size i=0; i<alive.size(); i++)
205
0
            calcBufferNotionals.push_back(
206
0
                exposure(pool_->names()[i], endDate)
207
0
                );// some better way to trim it? 
208
0
        return calcBufferNotionals;
209
0
    }
210
211
    std::vector<Probability> Basket::remainingProbabilities(const Date& d) const 
212
0
    {
213
0
        QL_REQUIRE(d >= refDate_, "Target date lies before basket inception");
214
0
        vector<Real> prob;
215
0
        const std::vector<Size>& alive = liveList();
216
217
0
        prob.reserve(alive.size());
218
0
        for(Size i=0; i<alive.size(); i++)
219
0
            prob.push_back(pool_->get(pool_->names()[i]).defaultProbability(
220
0
                pool_->defaultKeys()[i])->defaultProbability(d, true));
221
0
        return prob;
222
0
    }
223
224
    /* It is supossed to return the addition of ALL notionals from the 
225
    requested ctpty......*/
226
0
    Real Basket::exposure(const std::string& name, const Date& d) const {
227
        //'this->names_' contains duplicates, contrary to 'pool->names'
228
0
        auto match = std::find(pool_->names().begin(), pool_->names().end(), name);
229
0
        QL_REQUIRE(match != pool_->names().end(), "Name not in basket.");
230
0
        Real totalNotional = 0.;
231
0
        do{
232
0
            totalNotional += 
233
             // NOT IMPLEMENTED YET:
234
    //positions_[std::distance(names_.begin(), match)]->expectedExposure(d);
235
0
                notionals_[std::distance(pool_->names().begin(), match)];
236
0
            ++match;
237
0
            match = std::find(match, pool_->names().end(), name);
238
0
        }while(match != pool_->names().end());
239
240
0
        return totalNotional;
241
        //Size position = std::distance(poolNames.begin(), 
242
        //    std::find(poolNames.begin(), poolNames.end(), name));
243
        //QL_REQUIRE(position < pool_->size(), "Name not in pool list");
244
245
        //return positions_[position]->expectedExposure(d);
246
0
    }
247
248
    std::vector<std::string> Basket::remainingNames(const Date& endDate) const 
249
0
    {
250
        // maybe return zero directly instead?:
251
0
        QL_REQUIRE(endDate >= refDate_, 
252
0
            "Target date lies before basket inception");
253
254
0
        const std::vector<Size>& alive = liveList(endDate);
255
0
        std::vector<std::string> calcBufferNames;
256
0
        calcBufferNames.reserve(alive.size());
257
0
        for (unsigned long i : alive)
258
0
            calcBufferNames.push_back(pool_->names()[i]);
259
0
        return calcBufferNames;
260
0
    }
261
262
    vector<DefaultProbKey> Basket::remainingDefaultKeys(const Date& endDate) const 
263
0
    {
264
0
        QL_REQUIRE(endDate >= refDate_,
265
0
            "Target date lies before basket inception");
266
267
0
        const std::vector<Size>& alive = liveList(endDate);
268
0
        vector<DefaultProbKey> defKeys;
269
0
        defKeys.reserve(alive.size());
270
0
        for (unsigned long i : alive)
271
0
            defKeys.push_back(pool_->defaultKeys()[i]);
272
0
        return defKeys;
273
0
    }
274
275
0
    Size Basket::remainingSize() const {
276
0
        return evalDateLiveList_.size();
277
0
    }
278
279
0
    Size Basket::remainingSize(const Date& d) const {
280
0
        return remainingDefaultKeys(d).size();
281
0
    }
282
283
    /* computed on the inception values, notice the positions might have 
284
    amortized or changed in value and the total outstanding notional might 
285
    differ from the inception one.*/
286
0
    Real Basket::remainingDetachmentAmount(const Date& endDate) const {
287
0
        QL_REQUIRE(endDate >= refDate_, 
288
0
            "Target date lies before basket inception");
289
0
        return detachmentAmount_;
290
0
    }
291
292
0
    Real Basket::remainingAttachmentAmount(const Date& endDate) const {
293
        // maybe return zero directly instead?:
294
0
        QL_REQUIRE(endDate >= refDate_, 
295
0
            "Target date lies before basket inception");
296
0
        Real loss = settledLoss(endDate);
297
0
        return std::min(detachmentAmount_, attachmentAmount_ + 
298
0
            std::max(0.0, loss - attachmentAmount_));
299
0
    }
300
301
0
    Probability Basket::probOverLoss(const Date& d, Real lossFraction) const {
302
        // convert initial basket fraction to remaining basket fraction
303
0
        calculate();
304
        // if eaten up all the tranche the prob of losing any amount is 1 
305
        //  (we have already lost it)
306
0
        if(evalDateRemainingNot_ == 0.) return 1.;
307
308
        // Turn to live (remaining) tranche units to feed into the model request
309
0
        Real xPtfl = attachmentAmount_ + 
310
0
            (detachmentAmount_-attachmentAmount_)*lossFraction;
311
0
        Real xPrim = (xPtfl- evalDateAttachAmount_)/
312
0
            (detachmentAmount_-evalDateAttachAmount_);
313
        // in live tranche fractional units
314
        // if the level falls within realized losses the prob is 1.
315
0
        if(xPtfl < 0.) return 1.;
316
317
0
        return lossModel_->probOverLoss(d, xPrim);
318
0
    }
319
320
0
    Real Basket::percentile(const Date& d, Probability prob) const {
321
0
        calculate();
322
0
        return lossModel_->percentile(d, prob);
323
0
    }
324
325
0
    Real Basket::expectedTrancheLoss(const Date& d) const {
326
0
        calculate();
327
0
        return cumulatedLoss() + lossModel_->expectedTrancheLoss(d);
328
0
    }
329
330
0
    std::vector<Real> Basket::splitVaRLevel(const Date& date, Real loss) const {
331
0
        calculate();
332
0
        return lossModel_->splitVaRLevel(date, loss);
333
0
    }
334
335
0
    Real Basket::expectedShortfall(const Date& d, Probability prob) const {
336
0
        calculate();
337
0
        return lossModel_->expectedShortfall(d, prob);
338
0
    }
339
340
0
    std::map<Real, Probability> Basket::lossDistribution(const Date& d) const {
341
0
        calculate();
342
0
        return lossModel_->lossDistribution(d);
343
0
    }
344
345
    std::vector<Probability> 
346
0
        Basket::probsBeingNthEvent(Size n, const Date& d) const {
347
348
0
        Size alreadyDefaulted = pool_->size() - remainingNames().size();
349
0
        if(alreadyDefaulted >=n) 
350
0
            return std::vector<Probability>(remainingNames().size(), 0.);
351
352
0
        calculate();
353
0
        return lossModel_->probsBeingNthEvent(n-alreadyDefaulted, d);
354
0
    }
355
356
0
    Real Basket::defaultCorrelation(const Date& d, Size iName, Size jName) const{
357
0
        calculate();
358
0
        return lossModel_->defaultCorrelation(d, iName, jName);
359
360
0
    }
361
362
    /*! Returns the probaility of having a given or larger number of 
363
    defaults in the basket portfolio at a given time.
364
    */
365
0
    Probability Basket::probAtLeastNEvents(Size n, const Date& d) const{
366
0
        calculate();
367
0
        return lossModel_->probAtLeastNEvents(n, d);
368
369
0
    }
370
371
0
    Real Basket::recoveryRate(const Date& d, Size iName) const {
372
0
        calculate();
373
0
        return 
374
0
            lossModel_->expectedRecovery(d, iName, pool_->defaultKeys()[iName]);
375
0
    }
376
377
}