Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
basecorrelationcurve.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2017 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
25
26#include <ql/math/comparison.hpp>
27#include <ql/math/matrix.hpp>
28
32
33using namespace QuantLib;
34using namespace std;
35
36namespace {
37
38using std::string;
39
40// Check that weight, prior Weight or recovery is in [0, 1]
41void validateWeightRec(Real value, const string& name, const string& varName) {
42 QL_REQUIRE(value <= 1.0, "The " << varName << " value (" << value << ") for name " << name <<
43 " should not be greater than 1.0.");
44 QL_REQUIRE(value >= 0.0, "The " << varName << " value (" << value << ") for name " << name <<
45 " should not be less than 0.0.");
46}
47
48}
49
50namespace ore {
51namespace data {
52
54 Date asof,
55 BaseCorrelationCurveSpec spec,
56 const Loader& loader,
57 const CurveConfigurations& curveConfigs,
58 const QuantLib::ext::shared_ptr<ReferenceDataManager>& referenceData)
59 : spec_(spec), referenceData_(referenceData) {
60
61 DLOG("BaseCorrelationCurve: start building base correlation structure with ID " << spec_.curveConfigID());
62
63 try {
64
65 const auto& config = *curveConfigs.baseCorrelationCurveConfig(spec_.curveConfigID());
66
67 // The base correlation surface is of the form term x detachment point. We need at least two detachment points
68 // and at least one term. The list of terms may be explicit or contain a single wildcard character '*'.
69 // Similarly, the list of detachment points may be explicit or contain a single wildcard character '*'.
70
71 // Terms
72 const vector<string>& termStrs = config.terms();
73 QL_REQUIRE(!termStrs.empty(), "BaseCorrelationCurve: need at least one term.");
74 set<Period> terms;
75 bool termWc = find(termStrs.begin(), termStrs.end(), "*") != termStrs.end();
76 if (termWc) {
77 QL_REQUIRE(termStrs.size() == 1, "BaseCorrelationCurve: only one wild card term can be specified.");
78 DLOG("Have term wildcard pattern " << termStrs[0]);
79 } else {
80 for (const string& termStr : termStrs) {
81 terms.insert(parsePeriod(termStr));
82 }
83 DLOG("Parsed " << terms.size() << " unique configured term(s).");
84 }
85
86 // Detachment points
87
88 // Not advised to use set/map with floating point keys but should be ok here.
89 auto cmp = [](Real dp_1, Real dp_2) { return !close_enough(dp_1, dp_2) && dp_1 < dp_2; };
90 set<Real, decltype(cmp)> dps(cmp);
91
92 const vector<string>& dpStrs = config.detachmentPoints();
93 bool dpsWc = find(dpStrs.begin(), dpStrs.end(), "*") != dpStrs.end();
94 if (dpsWc) {
95 QL_REQUIRE(dpStrs.size() == 1, "BaseCorrelationCurve: only one wild card "
96 << "detachment point can be specified.");
97 DLOG("Have detachment point wildcard pattern " << dpStrs[0]);
98 } else {
99 for (const string& dpStr : dpStrs) {
100 dps.insert(parseReal(dpStr));
101 }
102 DLOG("Parsed " << dps.size() << " unique configured detachment points.");
103 QL_REQUIRE(dps.size() > 1, "BaseCorrelationCurve: need at least 2 unique detachment points.");
104 }
105
106 // Read in quotes relevant for the base correlation surface. The points that will be used are stored in data
107 // where the key is the term, detachment point pair and value is the base correlation quote.
108 auto mpCmp = [](pair<Period, Real> k_1, pair<Period, Real> k_2) {
109 if (k_1.first != k_2.first)
110 return k_1.first < k_2.first;
111 else
112 return !close_enough(k_1.second, k_2.second) && k_1.second < k_2.second;
113 };
114 map<pair<Period, Real>, Handle<Quote>, decltype(mpCmp)> data(mpCmp);
115
116 std::ostringstream ss;
117 ss << MarketDatum::InstrumentType::CDS_INDEX << "/" << MarketDatum::QuoteType::BASE_CORRELATION << "/*";
118 Wildcard w(ss.str());
119 for (const auto& md : loader.get(w, asof)) {
120
121 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
122
123 auto q = QuantLib::ext::dynamic_pointer_cast<BaseCorrelationQuote>(md);
124 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to BaseCorrelationQuote");
125
126 // Go to next quote if index name in the quote does not match the cds vol configuration name.
127 if (config.quoteName() != q->cdsIndexName())
128 continue;
129
130 TLOG("Processing quote " << q->name() << ": (" << q->term() << "," << fixed << setprecision(9)
131 << q->detachmentPoint() << "," << q->quote()->value() << ")");
132
133 // If we have been given a list of explicit terms, check that the quote matches one of them.
134 // Move to the next quote if it does not.
135 if (!termWc) {
136 auto it = terms.find(q->term());
137 if (it == terms.end())
138 continue;
139 }
140
141 // If we have been given a list of explicit detachment points, check that the quote matches one of them.
142 // Move to the next quote if it does not.
143 if (!dpsWc) {
144 auto it = dps.find(q->detachmentPoint());
145 if (it == dps.end())
146 continue;
147 }
148
149 // Skip if we have already added a quote for the given term and detachment point.
150 auto key = make_pair(q->term(), q->detachmentPoint());
151 if (data.find(key) != data.end()) {
152 DLOG("Already added base correlation with term " << q->term() << " and detachment point " << fixed
153 << setprecision(9) << q->detachmentPoint()
154 << " so skipping quote " << q->name());
155 continue;
156 }
157
158 // If we have wildcards, update the set so that dps and terms are populated.
159 if (termWc)
160 terms.insert(q->term());
161 if (dpsWc)
162 dps.insert(q->detachmentPoint());
163
164 // Add to the data that we will use.
165 data[key] = q->quote();
166
167 TLOG("Added quote " << q->name() << ": (" << q->term() << "," << fixed << setprecision(9)
168 << q->detachmentPoint() << "," << q->quote()->value() << ")");
169 }
170
171 DLOG("After processing the quotes, we have " << terms.size() << " unique term(s), " << dps.size()
172 << " unique detachment points and " << data.size() << " quotes.");
173 QL_REQUIRE(dps.size() > 1, "BaseCorrelationCurve: need at least 2 unique detachment points.");
174 QL_REQUIRE(dps.size() * terms.size() == data.size(),
175 "BaseCorrelationCurve: number of quotes ("
176 << data.size() << ") should equal number of detachment points (" << dps.size()
177 << ") times the number of terms (" << terms.size() << ").");
178
179
180 vector<vector<Handle<Quote>>> quotes;
181
182 // Need a vector of terms and detachment points for ctor below
183 vector<Period> tmpTerms(terms.begin(), terms.end());
184 vector<Real> tmpDps(dps.begin(), dps.end());
185
186 if (config.indexTerm() != 0 * Days) {
187 vector<Time> termTimes;
188 for (auto const& h : tmpTerms) {
189 termTimes.push_back(QuantExt::periodToTime(h));
190 }
191
192 Real t = QuantExt::periodToTime(config.indexTerm());
193 Size termIndex_m, termIndex_p;
194 Real alpha;
195 std::tie(termIndex_m, termIndex_p, alpha) = QuantExt::interpolationIndices(termTimes, t);
196
197 for (const auto& dp : dps) {
198 quotes.push_back({});
199 for (const auto& term : terms) {
200 auto key_m = make_pair(tmpTerms[termIndex_m], dp);
201 auto it_m = data.find(key_m);
202 auto key_p = make_pair(tmpTerms[termIndex_p], dp);
203 auto it_p = data.find(key_p);
204 QL_REQUIRE(it_m != data.end() && it_p != data.end(),
205 "BaseCorrelationCurve: do not have a quote for term "
206 << term << " and detachment point " << fixed << setprecision(9) << dp << ".");
207 quotes.back().push_back(Handle<Quote>(QuantLib::ext::make_shared<SimpleQuote>(
208 alpha * it_m->second->value() + (1 - alpha) * it_p->second->value())));
209 }
210 }
211 } else {
212
213 // Need to now fill _completely_ the (number of dps) x (number of terms) quotes surface.
214
215 for (const auto& dp : dps) {
216 quotes.push_back({});
217 for (const auto& term : terms) {
218 auto key = make_pair(term, dp);
219 auto it = data.find(key);
220 QL_REQUIRE(it != data.end(), "BaseCorrelationCurve: do not have a quote for term "
221 << term << " and detachment point " << fixed << setprecision(9)
222 << dp << ".");
223 quotes.back().push_back(it->second);
224 }
225 }
226 }
227
228 if (config.adjustForLosses()) {
229 DLOG("Adjust for losses is true for base correlation " << spec_.curveConfigID());
230 DLOG("Detachment points before: " << Array(tmpDps.begin(), tmpDps.end()));
231 tmpDps = adjustForLosses(tmpDps);
232 DLOG("Detachment points after: " << Array(tmpDps.begin(), tmpDps.end()));
233 }
234
235 // The QuantLib interpolator expects at least two terms, so add a column, copying the last
236 QL_REQUIRE(!tmpTerms.empty(), "No terms found");
237 tmpTerms.push_back(tmpTerms.back() + 1 * tmpTerms.back().units());
238 for (Size i = 0; i < tmpDps.size(); ++i)
239 quotes[i].push_back(quotes[i][tmpTerms.size() - 2]);
240
241 baseCorrelation_ = QuantLib::ext::make_shared<QuantExt::InterpolatedBaseCorrelationTermStructure<QuantExt::BilinearFlat>>(
242 config.settlementDays(), config.calendar(), config.businessDayConvention(), tmpTerms, tmpDps, quotes,
243 config.dayCounter(), config.startDate(), config.rule());
244
245 baseCorrelation_->enableExtrapolation(config.extrapolate());
246
247
248 } catch (std::exception& e) {
249 QL_FAIL("BaseCorrelationCurve: curve building failed :" << e.what());
250 } catch (...) {
251 QL_FAIL("BaseCorrelationCurve: curve building failed: unknown error");
252 }
253
254 DLOG("BaseCorrelationCurve: finished building base correlation structure with ID " << spec_.curveConfigID());
255}
256
257vector<Real> BaseCorrelationCurve::adjustForLosses(const vector<Real>& detachPoints) const {
258
259 const auto& cId = spec_.curveConfigID();
260
261 const auto& [qualifier, period] = splitCurveIdWithTenor(cId);
262
263 DLOG("BaseCorrelationCurve::adjustForLosses: start adjusting for losses for base correlation " << qualifier);
264
265 if (!referenceData_) {
266 DLOG("Reference data manager is null so cannot adjust for losses.");
267 return detachPoints;
268 }
269
270 if (!referenceData_->hasData(CreditIndexReferenceDatum::TYPE, qualifier)) {
271 DLOG("Reference data manager does not have index credit data for " << qualifier
272 << " so cannot adjust for losses.");
273 return detachPoints;
274 }
275
276 auto crd = QuantLib::ext::dynamic_pointer_cast<CreditIndexReferenceDatum>(
277 referenceData_->getData(CreditIndexReferenceDatum::TYPE, qualifier));
278 if (!crd) {
279 DLOG("Index credit data for " << qualifier << " is not of correct type so cannot adjust for losses.");
280 return detachPoints;
281 }
282
283 // Process the credit index reference data
284 Real totalRemainingWeight = 0.0;
285 Real totalPriorWeight = 0.0;
286 Real lost = 0.0;
287 Real recovered = 0.0;
288
289 for (const auto& c : crd->constituents()) {
290
291 const auto& name = c.name();
292 auto weight = c.weight();
293 validateWeightRec(weight, name, "weight");
294
295 if (!close(0.0, weight)) {
296 totalRemainingWeight += weight;
297 } else {
298 auto priorWeight = c.priorWeight();
299 QL_REQUIRE(priorWeight != Null<Real>(), "Expecting a valid prior weight for name " << name << ".");
300 validateWeightRec(priorWeight, name, "prior weight");
301 auto recovery = c.recovery();
302 QL_REQUIRE(recovery != Null<Real>(), "Expecting a valid recovery for name " << name << ".");
303 validateWeightRec(recovery, name, "recovery");
304 lost += (1.0 - recovery) * priorWeight;
305 recovered += recovery * priorWeight;
306 totalPriorWeight += priorWeight;
307 }
308 }
309
310 Real totalWeight = totalRemainingWeight + totalPriorWeight;
311 TLOG("Total remaining weight = " << totalRemainingWeight);
312 TLOG("Total prior weight = " << totalPriorWeight);
313 TLOG("Total weight = " << totalWeight);
314
315 if (!close(totalRemainingWeight, 1.0) && totalRemainingWeight > 1.0) {
316 ALOG("Total remaining weight is greater than 1, possible error in CreditIndexReferenceDatum for " << qualifier);
317 }
318
319 if (!close(totalWeight, 1.0)) {
320 ALOG("Expected the total weight (" << totalWeight << " = " << totalRemainingWeight << " + " << totalPriorWeight
321 << ") to equal 1, possible error in CreditIndexReferenceDatum for "
322 << qualifier);
323 }
324
325 if (close(totalRemainingWeight, 0.0)) {
326 ALOG("The total remaining weight is 0 so cannot adjust for losses.");
327 return detachPoints;
328 }
329
330 if (close(totalRemainingWeight, 1.0)) {
331 DLOG("Index factor for " << qualifier << " is 1 so adjustment for losses not required.");
332 return detachPoints;
333 }
334
335 // Index factor is less than 1 so need to adjust each of the detachment points.
336 vector<Real> result;
337 for (Size i = 0; i < detachPoints.size(); ++i) {
338
339 // Amounts below, for tranche and above for original (quoted) attachment and detachment points.
340 Real below = i == 0 ? 0.0 : detachPoints[i-1];
341 Real tranche = detachPoints[i] - below;
342 Real above = 1.0 - detachPoints[i];
343 Real newBelow = max(below - lost, 0.0);
344 Real newTranche = tranche - max(min(recovered - above, tranche), 0.0) - max(min(lost - below, tranche), 0.0);
345 Real newDetach = (newBelow + newTranche) / totalRemainingWeight;
346
347 TLOG("Quoted detachment point " << detachPoints[i] << " adjusted to " << newDetach << ".");
348
349 if (i > 0 && (newDetach < result.back() || close(newDetach, result.back()))) {
350 ALOG("The " << io::ordinal(i + 1) << " adjusted detachment point is not greater than the previous " <<
351 "adjusted detachment point so cannot adjust for losses.");
352 return detachPoints;
353 }
354
355 result.push_back(newDetach);
356 }
357
358 DLOG("BaseCorrelationCurve::adjustForLosses: finished adjusting for losses for base correlation " << qualifier);
359
360 return result;
361}
362
363} // namespace data
364} // namespace ore
Wrapper class for building base correlation structures.
SafeStack< ValueType > value
Period parsePeriod(const string &s)
Convert text to QuantLib::Period.
Definition: parsers.cpp:171
Real parseReal(const string &s)
Convert text to Real.
Definition: parsers.cpp:112
leg data model and serialization
Classes and functions for log message handling.
@ data
Definition: log.hpp:77
#define DLOG(text)
Logging Macro (Level = Debug)
Definition: log.hpp:554
#define ALOG(text)
Logging Macro (Level = Alert)
Definition: log.hpp:544
#define TLOG(text)
Logging Macro (Level = Data)
Definition: log.hpp:556
market data related utilties
RandomVariable max(RandomVariable x, const RandomVariable &y)
std::tuple< Size, Size, Real > interpolationIndices(const T &x, const Real v)
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
Real periodToTime(const Period &p)
RandomVariable min(RandomVariable x, const RandomVariable &y)
std::pair< std::string, QuantLib::Period > splitCurveIdWithTenor(const std::string &creditCurveId)
Definition: marketdata.cpp:231
Serializable Credit Default Swap.
Definition: namespaces.docs:23
Map text representations to QuantLib/QuantExt types.
Reference data model and serialization.
vector< string > curveConfigs
string name