Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
simmcalculator.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2018 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
24
25#include <boost/math/distributions/normal.hpp>
26#include <numeric>
31#include <ql/math/comparison.hpp>
32#include <ql/quote.hpp>
33
34using std::abs;
35using std::accumulate;
36using std::make_pair;
37using std::make_tuple;
38using std::map;
39using std::max;
40using std::min;
41using std::pair;
42using std::set;
43using std::sqrt;
44using std::string;
45
51using QuantLib::close_enough;
52using QuantLib::Real;
53
54namespace ore {
55namespace analytics {
56
57// Ease notation again
64
66 const QuantLib::ext::shared_ptr<SimmConfiguration>& simmConfiguration,
67 const string& calculationCcyCall, const string& calculationCcyPost,
68 const string& resultCcy, const QuantLib::ext::shared_ptr<Market> market,
69 const bool determineWinningRegulations, const bool enforceIMRegulations,
70 const bool quiet, const map<SimmSide, set<NettingSetDetails>>& hasSEC,
71 const map<SimmSide, set<NettingSetDetails>>& hasCFTC)
72 : simmConfiguration_(simmConfiguration), calculationCcyCall_(calculationCcyCall),
73 calculationCcyPost_(calculationCcyPost), resultCcy_(resultCcy.empty() ? calculationCcyCall_ : resultCcy),
74 market_(market), quiet_(quiet), hasSEC_(hasSEC), hasCFTC_(hasCFTC) {
75
76 QL_REQUIRE(checkCurrency(calculationCcyCall_), "SIMM Calculator: The Call side calculation currency ("
77 << calculationCcyCall_ << ") must be a valid ISO currency code");
78 QL_REQUIRE(checkCurrency(calculationCcyPost_), "SIMM Calculator: The Post side calculation currency ("
79 << calculationCcyPost_ << ") must be a valid ISO currency code");
80 QL_REQUIRE(checkCurrency(resultCcy_),
81 "SIMM Calculator: The result currency (" << resultCcy_ << ") must be a valid ISO currency code");
82
83 for (const CrifRecord& cr : crif) {
84 // Remove empty
85 if (cr.riskType == CrifRecord::RiskType::Empty) {
86 continue;
87 }
88 // Remove Schedule-only CRIF records
89 const bool isSchedule = cr.imModel == "Schedule";
90 if (isSchedule) {
91 if (!quiet_ && determineWinningRegulations) {
92 ore::data::StructuredTradeWarningMessage(cr.tradeId, cr.tradeType, "SIMM calculator", "Skipping over Schedule CRIF record").log();
93 }
94 continue;
95 }
96
97
98
99 // Check for each netting set whether post/collect regs are populated at all
100 if (collectRegsIsEmpty_.find(cr.nettingSetDetails) == collectRegsIsEmpty_.end()) {
101 collectRegsIsEmpty_[cr.nettingSetDetails] = cr.collectRegulations.empty();
102 } else if (collectRegsIsEmpty_.at(cr.nettingSetDetails) && !cr.collectRegulations.empty()) {
103 collectRegsIsEmpty_.at(cr.nettingSetDetails) = false;
104 }
105 if (postRegsIsEmpty_.find(cr.nettingSetDetails) == postRegsIsEmpty_.end()) {
106 postRegsIsEmpty_[cr.nettingSetDetails] = cr.postRegulations.empty();
107 } else if (postRegsIsEmpty_.at(cr.nettingSetDetails) && !cr.postRegulations.empty()) {
108 postRegsIsEmpty_.at(cr.nettingSetDetails) = false;
109 }
110
111 // Make sure we have CRIF amount denominated in the result ccy
112 CrifRecord newCrifRecord = cr;
113
114 if (cr.requiresAmountUsd() && resultCcy_ == "USD" && cr.hasAmountUsd()) {
115 newCrifRecord.amountResultCcy = newCrifRecord.amountUsd;
116 } else if(cr.requiresAmountUsd()) {
117 // ProductClassMultiplier and AddOnNotionalFactor don't have a currency and dont need to be converted,
118 // we use the amount
119 const Real fxSpot = market_->fxRate(newCrifRecord.amountCurrency + resultCcy_)->value();
120 newCrifRecord.amountResultCcy = fxSpot * newCrifRecord.amount;
121 }
122 newCrifRecord.resultCurrency = resultCcy_;
123
124 crif_.addRecord(newCrifRecord);
125 }
126
127 // If there are no CRIF records to process
128 if (crif_.empty())
129 return;
130
131 // Add CRIF records to each regulation under each netting set
132 if (!quiet_) {
133 LOG("SimmCalculator: Splitting up original CRIF records into their respective collect/post regulations");
134 }
135
136 splitCrifByRegulationsAndPortfolios(crif_, enforceIMRegulations);
137
138 // Some additional processing depending on the regulations applicable to each netting set
139 for (auto& [side, nettingsSetCrifMap] : regSensitivities_) {
140 for (auto& [nettingDetails, regulationCrifMap] : nettingsSetCrifMap) {
141 // Where there is SEC and CFTC in the portfolio, we add the CFTC trades to SEC,
142 // but still continue with CFTC calculations
143 const bool hasCFTCGlobal = hasCFTC_[side].find(nettingDetails) != hasCFTC_[side].end();
144 const bool hasSECGlobal = hasSEC_[side].find(nettingDetails) != hasSEC_[side].end();
145 const bool hasSECLocal = regulationCrifMap.find("SEC") != regulationCrifMap.end();
146 const bool hasCFTCLocal = regulationCrifMap.find("CFTC") != regulationCrifMap.end();
147
148 if ((hasSECLocal && hasCFTCLocal) || (hasCFTCGlobal && hasSECGlobal)) {
149 if (!hasSECLocal) {
150 if (!hasCFTCLocal) {
151 continue;
152 } else {
153 regulationCrifMap["SEC"] = Crif();
154 }
155 }
156
157 if (hasCFTCLocal) {
158 // At this point, we expect to have both SEC and CFTC sensitivities for the netting set
159 const auto& crifCFTC = regulationCrifMap["CFTC"];
160 const auto& crifSEC = regulationCrifMap["SEC"];
161 for (const auto& cr :crifCFTC) {
162 // Only add CFTC records to SEC if the record was not already in SEC,
163 // i.e. we skip over CRIF records with regulations specified as e.g. "..., CFTC, SEC, ..."
164 if (crifSEC.find(cr) == crifSEC.end()) {
165 if (!quiet_) {
166 DLOG("SimmCalculator: Inserting CRIF record with CFTC "
167 << nettingDetails << " regulation into SEC CRIF records: " << cr);
168 }
169 regulationCrifMap["SEC"].addRecord(cr);
170 }
171 }
172 }
173
174 }
175 // Aggreggate now all Crif Records
176 for (auto& [regulation, crif] : regulationCrifMap) {
177 crif = crif.aggregate();
178 }
179
180 // If netting set has "Unspecified" plus other regulations, the "Unspecified" sensis are to be excluded.
181 // If netting set only has "Unspecified", then no regulations were ever specified, so all trades are
182 // included.
183 if (regulationCrifMap.count("Unspecified") > 0 && regulationCrifMap.size() > 1)
184 regulationCrifMap.erase("Unspecified");
185 }
186 }
187
188 // Calculate SIMM call and post for each regulation under each netting set
189 for (const auto& [side, nettingSetRegulationCrifMap] : regSensitivities_) {
190 for (const auto& [nsd, regulationCrifMap] : nettingSetRegulationCrifMap) {
191 // Calculate SIMM for particular side-nettingSet-regulation combination
192 for (const auto& [regulation, crif] : regulationCrifMap) {
193 bool hasFixedAddOn = false;
194 for (const auto& sp : crif) {
195 if (sp.riskType == RiskType::AddOnFixedAmount) {
196 hasFixedAddOn = true;
197 break;
198 }
199 }
200 if (crif.hasCrifRecords() || hasFixedAddOn)
201 calculateRegulationSimm(crif, nsd, regulation, side);
202 }
203 }
204 }
205
206 // Determine winning call and post regulations
207 if (determineWinningRegulations) {
208 if (!quiet_) {
209 LOG("SimmCalculator: Determining winning regulations");
210 }
211
212 for (auto sv : simmResults_) {
213 const SimmSide side = sv.first;
214
215 // Determine winning (call and post) regulation for each netting set
216 for (const auto& kv : sv.second) {
217
218 // Collect margin amounts and determine the highest margin amount
219 Real winningMargin = std::numeric_limits<Real>::min();
220 map<string, Real> nettingSetMargins;
221 std::vector<Real> margins;
222 for (const auto& regSimmResults : kv.second) {
223 const Real& im = regSimmResults.second.get(ProductClass::All, RiskClass::All, MarginType::All, "All");
224 nettingSetMargins[regSimmResults.first] = im;
225 if (im > winningMargin)
226 winningMargin = im;
227 }
228
229 // Determine winning regulations, i.e. regulations under which we find the highest margin amount
230 std::vector<string> winningRegulations;
231 for (const auto& kv : nettingSetMargins) {
232 if (close_enough(kv.second, winningMargin))
233 winningRegulations.push_back(kv.first);
234 }
235
236 // In the case of multiple winning regulations, pick one based on the priority in the list
237 //const Regulation winningRegulation = oreplus::analytics::getWinningRegulation(winningRegulations);
238 string winningRegulation = winningRegulations.size() > 1
240 : winningRegulations.at(0);
241
242 // Populate internal list of winning regulators
243 winningRegulations_[side][kv.first] = to_string(winningRegulation);
244 }
245 }
246
248 }
249}
250
252 const NettingSetDetails& nettingSetDetails, const string& regulation,
253 const SimmSide& side) {
254
255 if (!quiet_) {
256 LOG("SimmCalculator: Calculating SIMM " << side << " for portfolio [" << nettingSetDetails << "], regulation "
257 << regulation);
258 }
259 // Loop over portfolios and product classes
260 for(const auto productClass : crif.ProductClassesByNettingSetDetails(nettingSetDetails)){
261 if (!quiet_) {
262 LOG("SimmCalculator: Calculating SIMM for product class " << productClass);
263 }
264
265 // Delta margin components
266 RiskClass rc = RiskClass::InterestRate;
267 MarginType mt = MarginType::Delta;
268 auto p = irDeltaMargin(nettingSetDetails, productClass, crif, side);
269 if (p.second)
270 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
271
272 rc = RiskClass::FX;
273 p = margin(nettingSetDetails, productClass, RiskType::FX, crif, side);
274 if (p.second)
275 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
276
277 rc = RiskClass::CreditQualifying;
278 p = margin(nettingSetDetails, productClass, RiskType::CreditQ, crif, side);
279 if (p.second)
280 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
281
282 rc = RiskClass::CreditNonQualifying;
283 p = margin(nettingSetDetails, productClass, RiskType::CreditNonQ, crif, side);
284 if (p.second)
285 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
286
287 rc = RiskClass::Equity;
288 p = margin(nettingSetDetails, productClass, RiskType::Equity, crif, side);
289 if (p.second)
290 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
291
292 rc = RiskClass::Commodity;
293 p = margin(nettingSetDetails, productClass, RiskType::Commodity, crif, side);
294 if (p.second)
295 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
296
297 // Vega margin components
298 mt = MarginType::Vega;
299 rc = RiskClass::InterestRate;
300 p = irVegaMargin(nettingSetDetails, productClass, crif, side);
301 if (p.second)
302 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
303
304 rc = RiskClass::FX;
305 p = margin(nettingSetDetails, productClass, RiskType::FXVol, crif, side);
306 if (p.second)
307 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
308
309 rc = RiskClass::CreditQualifying;
310 p = margin(nettingSetDetails, productClass, RiskType::CreditVol, crif, side);
311 if (p.second)
312 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
313
314 rc = RiskClass::CreditNonQualifying;
315 p = margin(nettingSetDetails, productClass, RiskType::CreditVolNonQ, crif, side);
316 if (p.second)
317 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
318
319 rc = RiskClass::Equity;
320 p = margin(nettingSetDetails, productClass, RiskType::EquityVol, crif, side);
321 if (p.second)
322 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
323
324 rc = RiskClass::Commodity;
325 p = margin(nettingSetDetails, productClass, RiskType::CommodityVol, crif, side);
326 if (p.second)
327 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
328
329 // Curvature margin components for sides call and post
330 mt = MarginType::Curvature;
331 rc = RiskClass::InterestRate;
332
333 p = irCurvatureMargin(nettingSetDetails, productClass, side, crif);
334 if (p.second)
335 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
336
337 rc = RiskClass::FX;
338 p = curvatureMargin(nettingSetDetails, productClass, RiskType::FXVol, side, crif, false);
339 if (p.second)
340 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
341
342 rc = RiskClass::CreditQualifying;
343 p = curvatureMargin(nettingSetDetails, productClass, RiskType::CreditVol, side, crif);
344 if (p.second)
345 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
346
347 rc = RiskClass::CreditNonQualifying;
348 p = curvatureMargin(nettingSetDetails, productClass, RiskType::CreditVolNonQ, side, crif);
349 if (p.second)
350 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
351
352 rc = RiskClass::Equity;
353 p = curvatureMargin(nettingSetDetails, productClass, RiskType::EquityVol, side, crif, false);
354 if (p.second)
355 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
356
357 rc = RiskClass::Commodity;
358 p = curvatureMargin(nettingSetDetails, productClass, RiskType::CommodityVol, side, crif, false);
359 if (p.second)
360 add(nettingSetDetails, regulation, productClass, rc, mt, p.first, side);
361
362 // Base correlation margin components. This risk type came later so need to check
363 // first if it is valid under the configuration
364 if (simmConfiguration_->isValidRiskType(RiskType::BaseCorr)) {
365 p = margin(nettingSetDetails, productClass, RiskType::BaseCorr, crif, side);
366 if (p.second)
367 add(nettingSetDetails, regulation, productClass, RiskClass::CreditQualifying, MarginType::BaseCorr,
368 p.first, side);
369 }
370 }
371
372 // Calculate the higher level margins
373 populateResults(side, nettingSetDetails, regulation);
374
375 // For each portfolio, calculate the additional margin
376 calcAddMargin(side, nettingSetDetails, regulation, crif);
377}
378
379const string& SimmCalculator::winningRegulations(const SimmSide& side, const NettingSetDetails& nettingSetDetails) const {
380 const auto& subWinningRegs = winningRegulations(side);
381 QL_REQUIRE(subWinningRegs.find(nettingSetDetails) != subWinningRegs.end(),
382 "SimmCalculator::winningRegulations(): Could not find netting set in the list of "
383 << side << " IM winning regulations: " << nettingSetDetails);
384 return subWinningRegs.at(nettingSetDetails);
385}
386
387const map<NettingSetDetails, string>& SimmCalculator::winningRegulations(const SimmSide& side) const {
388 QL_REQUIRE(winningRegulations_.find(side) != winningRegulations_.end(),
389 "SimmCalculator::winningRegulations(): Could not find list of" << side << " IM winning regulations");
390 return winningRegulations_.at(side);
391}
392
393const map<SimmSide, map<NettingSetDetails, string>>& SimmCalculator::winningRegulations() const {
394 return winningRegulations_;
395}
396
397const SimmResults& SimmCalculator::simmResults(const SimmSide& side, const NettingSetDetails& nettingSetDetails,
398 const string& regulation) const {
399 const auto& subResults = simmResults(side, nettingSetDetails);
400 QL_REQUIRE(subResults.find(regulation) != subResults.end(),
401 "SimmCalculator::simmResults(): Could not find regulation in the SIMM "
402 << side << " results for netting set [" << nettingSetDetails << "]: " << regulation);
403 return subResults.at(regulation);
404}
405
406const map<string, SimmResults>& SimmCalculator::simmResults(const SimmSide& side,
407 const NettingSetDetails& nettingSetDetails) const {
408 const auto& subResults = simmResults(side);
409 QL_REQUIRE(subResults.find(nettingSetDetails) != subResults.end(),
410 "SimmCalculator::simmResults(): Could not find netting set in the SIMM "
411 << side << " results: " << nettingSetDetails);
412 return subResults.at(nettingSetDetails);
413}
414
415const map<NettingSetDetails, map<string, SimmResults>>& SimmCalculator::simmResults(const SimmSide& side) const {
416 QL_REQUIRE(simmResults_.find(side) != simmResults_.end(),
417 "SimmCalculator::simmResults(): Could not find " << side << " IM in the SIMM results");
418 return simmResults_.at(side);
419}
420
421const map<SimmSide, map<NettingSetDetails, map<string, SimmResults>>>& SimmCalculator::simmResults() const {
422 return simmResults_;
423}
424
425const pair<string, SimmResults>& SimmCalculator::finalSimmResults(const SimmSide& side,
426 const NettingSetDetails& nettingSetDetails) const {
427 const auto& subResults = finalSimmResults(side);
428 QL_REQUIRE(subResults.find(nettingSetDetails) != subResults.end(),
429 "SimmCalculator::finalSimmResults(): Could not find netting set in the final SIMM "
430 << side << " results: " << nettingSetDetails);
431 return subResults.at(nettingSetDetails);
432}
433
434const map<NettingSetDetails, pair<string, SimmResults>>& SimmCalculator::finalSimmResults(const SimmSide& side) const {
435 QL_REQUIRE(finalSimmResults_.find(side) != finalSimmResults_.end(),
436 "SimmCalculator::finalSimmResults(): Could not find " << side << " IM in the final SIMM results");
437 return finalSimmResults_.at(side);
438}
439
440const map<SimmSide, map<NettingSetDetails, pair<string, SimmResults>>>& SimmCalculator::finalSimmResults() const {
441 return finalSimmResults_;
442}
443
444pair<map<string, Real>, bool> SimmCalculator::irDeltaMargin(const NettingSetDetails& nettingSetDetails,
445 const ProductClass& pc, const Crif& crif,
446 const SimmSide& side) const {
447 const string& calcCcy = side == SimmSide::Call ? calculationCcyCall_ : calculationCcyPost_;
448
449 // "Bucket" here referse to exposures under the CRIF qualifiers
450 map<string, Real> bucketMargins;
451
452 // Get alls IR qualifiers
453 set<string> qualifiers =
454 getQualifiers(crif, nettingSetDetails, pc, {RiskType::IRCurve, RiskType::XCcyBasis, RiskType::Inflation});
455
456 // If there are no qualifiers, return early and set bool to false to indicate margin does not apply
457 if (qualifiers.empty()) {
458 bucketMargins["All"] = 0.0;
459 return make_pair(bucketMargins, false);
460 }
461
462 // Hold the concentration risk for each qualifier i.e. $CR_b$ from SIMM docs
463 map<string, Real> concentrationRisk;
464 // The delta margin for each currency i.e. $K_b$ from SIMM docs
465 map<string, Real> deltaMargin;
466 // The sum of the weighted sensitivities for each currency i.e. $\sum_{i,k} WS_{k,i}$ from SIMM docs
467 map<string, Real> sumWeightedSensis;
468
469 // Loop over the qualifiers i.e. currencies
470 // Loop over the qualifiers i.e. currencies
471 for (const auto& qualifier : qualifiers) {
472 // Pair of iterators to start and end of IRCurve sensitivities with current qualifier
473 auto pIrQualifier =
474 crif.filterByQualifier(nettingSetDetails, pc, RiskType::IRCurve, qualifier);
475
476 // Iterator to Xccy basis element with current qualifier (expect zero or one element)
477 auto XccyCount = crif.countMatching(nettingSetDetails, pc, RiskType::XCcyBasis, qualifier);
478 QL_REQUIRE(XccyCount < 2, "SIMM Calcuator: Expected either 0 or 1 elements for risk type "
479 << RiskType::XCcyBasis << " and qualifier " << qualifier << " but got "
480 << XccyCount);
481 auto itXccy = crif.findBy(nettingSetDetails, pc, RiskType::XCcyBasis, qualifier);
482
483 // Iterator to inflation element with current qualifier (expect zero or one element)
484 auto inflationCount = crif.countMatching(nettingSetDetails, pc, RiskType::Inflation, qualifier);
485 QL_REQUIRE(inflationCount < 2, "SIMM Calculator: Expected either 0 or 1 elements for risk type "
486 << RiskType::Inflation << " and qualifier " << qualifier << " but got "
487 << inflationCount);
488 auto itInflation = crif.findBy(nettingSetDetails, pc, RiskType::Inflation, qualifier);
489
490 // One pass to get the concentration risk for this qualifier
491 // Note: XccyBasis is not included in the calculation of concentration risk and the XccyBasis sensitivity
492 // is not scaled by it
493 for (const auto& it : pIrQualifier) {
494 concentrationRisk[qualifier] += it.amountResultCcy;
495 }
496 // Add inflation sensitivity to the concentration risk
497 if (itInflation != crif.end()){
498 concentrationRisk[qualifier] += itInflation->amountResultCcy;
499 }
500 // Divide by the concentration risk threshold
501 Real concThreshold = simmConfiguration_->concentrationThreshold(RiskType::IRCurve, qualifier);
502 if (resultCcy_ != "USD")
503 concThreshold *= market_->fxRate("USD" + resultCcy_)->value();
504 concentrationRisk[qualifier] /= concThreshold;
505 // Final concentration risk amount
506 concentrationRisk[qualifier] = max(1.0, sqrt(std::abs(concentrationRisk[qualifier])));
507
508 // Calculate the delta margin piece for this qualifier i.e. $K_b$ from SIMM docs
509 for (auto itOuter = pIrQualifier.begin(); itOuter != pIrQualifier.end(); ++itOuter) {
510 // Risk weight i.e. $RW_k$ from SIMM docs
511 Real rwOuter = simmConfiguration_->weight(RiskType::IRCurve, qualifier, itOuter->label1);
512 // Weighted sensitivity i.e. $WS_{k,i}$ from SIMM docs
513 Real wsOuter = rwOuter * itOuter->amountResultCcy * concentrationRisk[qualifier];
514 // Update weighted sensitivity sum
515 sumWeightedSensis[qualifier] += wsOuter;
516 // Add diagonal element to delta margin
517 deltaMargin[qualifier] += wsOuter * wsOuter;
518 // Add the cross elements to the delta margin
519 for (auto itInner = pIrQualifier.begin(); itInner != itOuter; ++itInner) {
520 // Label2 level correlation i.e. $\phi_{i,j}$ from SIMM docs
521 Real subCurveCorr = simmConfiguration_->correlation(RiskType::IRCurve, qualifier, "", itOuter->label2,
522 RiskType::IRCurve, qualifier, "", itInner->label2);
523 // Label1 level correlation i.e. $\rho_{k,l}$ from SIMM docs
524 Real tenorCorr = simmConfiguration_->correlation(RiskType::IRCurve, qualifier, itOuter->label1, "",
525 RiskType::IRCurve, qualifier, itInner->label1, "");
526 // Add cross element to delta margin
527 Real rwInner = simmConfiguration_->weight(RiskType::IRCurve, qualifier, itInner->label1);
528 Real wsInner = rwInner * itInner->amountResultCcy * concentrationRisk[qualifier];
529 deltaMargin[qualifier] += 2 * subCurveCorr * tenorCorr * wsOuter * wsInner;
530 }
531 }
532
533 // Add the Inflation component, if any
534 Real wsInflation = 0.0;
535 if (itInflation != crif.end()) {
536 // Risk weight
537 Real rwInflation = simmConfiguration_->weight(RiskType::Inflation, qualifier, itInflation->label1);
538 // Weighted sensitivity
539 wsInflation = rwInflation * itInflation->amountResultCcy * concentrationRisk[qualifier];
540 // Update weighted sensitivity sum
541 sumWeightedSensis[qualifier] += wsInflation;
542 // Add diagonal element to delta margin
543 deltaMargin[qualifier] += wsInflation * wsInflation;
544 // Add the cross elements (Inflation with IRCurve tenors) to the delta margin
545 // Correlation (know that Label1 and Label2 do not matter)
546 Real corr = simmConfiguration_->correlation(RiskType::IRCurve, qualifier, "", "", RiskType::Inflation,
547 qualifier, "", "");
548 for (auto it = pIrQualifier.begin(); it != pIrQualifier.end(); ++it) {
549 // Add cross element to delta margin
550 Real rw = simmConfiguration_->weight(RiskType::IRCurve, qualifier, it->label1);
551 Real ws = rw * it->amountResultCcy * concentrationRisk[qualifier];
552 deltaMargin[qualifier] += 2 * corr * ws * wsInflation;
553 }
554 }
555
556 // Add the XccyBasis component, if any
557 if (itXccy != crif.end()) {
558 // Risk weight
559 Real rwXccy = simmConfiguration_->weight(RiskType::XCcyBasis, qualifier, itXccy->label1);
560 // Weighted sensitivity (no concentration risk here)
561 Real wsXccy = rwXccy * itXccy->amountResultCcy;
562 // Update weighted sensitivity sum
563 sumWeightedSensis[qualifier] += wsXccy;
564 // Add diagonal element to delta margin
565 deltaMargin[qualifier] += wsXccy * wsXccy;
566 // Add the cross elements (XccyBasis with IRCurve tenors) to the delta margin
567 // Correlation (know that Label1 and Label2 do not matter)
568 Real corr = simmConfiguration_->correlation(RiskType::IRCurve, qualifier, "", "", RiskType::XCcyBasis,
569 qualifier, "", "");
570 for (auto it = pIrQualifier.begin(); it != pIrQualifier.end(); ++it) {
571 // Add cross element to delta margin
572 Real rw = simmConfiguration_->weight(RiskType::IRCurve, qualifier, it->label1, calcCcy);
573 Real ws = rw * it->amountResultCcy * concentrationRisk[qualifier];
574 deltaMargin[qualifier] += 2 * corr * ws * wsXccy;
575 }
576
577 // Inflation vs. XccyBasis cross component if any
578 if (itInflation != crif.end()) {
579 // Correlation (know that Label1 and Label2 do not matter)
580 Real corr = simmConfiguration_->correlation(RiskType::Inflation, qualifier, "", "", RiskType::XCcyBasis,
581 qualifier, "", "");
582 deltaMargin[qualifier] += 2 * corr * wsInflation * wsXccy;
583 }
584 }
585
586 // Finally have the value of $K_b$
587 deltaMargin[qualifier] = sqrt(max(deltaMargin[qualifier], 0.0));
588 }
589
590 // Now calculate final IR delta margin by aggregating across currencies
591 Real margin = 0.0;
592 for (auto itOuter = qualifiers.begin(); itOuter != qualifiers.end(); ++itOuter) {
593 // Diagonal term
594 margin += deltaMargin.at(*itOuter) * deltaMargin.at(*itOuter);
595 // Cross terms
596 Real sOuter = max(min(sumWeightedSensis.at(*itOuter), deltaMargin.at(*itOuter)), -deltaMargin.at(*itOuter));
597 for (auto itInner = qualifiers.begin(); itInner != itOuter; ++itInner) {
598 Real sInner = max(min(sumWeightedSensis.at(*itInner), deltaMargin.at(*itInner)), -deltaMargin.at(*itInner));
599 Real g = min(concentrationRisk.at(*itOuter), concentrationRisk.at(*itInner)) /
600 max(concentrationRisk.at(*itOuter), concentrationRisk.at(*itInner));
601 Real corr = simmConfiguration_->correlation(RiskType::IRCurve, *itOuter, "", "", RiskType::IRCurve,
602 *itInner, "", "");
603 margin += 2.0 * sOuter * sInner * corr * g;
604 }
605 }
606 margin = sqrt(max(margin, 0.0));
607
608 for (const auto& m : deltaMargin)
609 bucketMargins[m.first] = m.second;
610 bucketMargins["All"] = margin;
611
612 return make_pair(bucketMargins, true);
613}
614
615pair<map<string, Real>, bool> SimmCalculator::irVegaMargin(const NettingSetDetails& nettingSetDetails,
616 const CrifRecord::ProductClass& pc, const Crif& crif,
617 const SimmSide& side) const {
618
619 const string& calcCcy = side == SimmSide::Call ? calculationCcyCall_ : calculationCcyPost_;
620
621 // "Bucket" here refers to exposures under the CRIF qualifiers
622 map<string, Real> bucketMargins;
623
624 // Find the set of qualifiers, i.e. currencies, in the Simm sensitivities
625 set<string> qualifiers = getQualifiers(crif, nettingSetDetails, pc, {RiskType::IRVol, RiskType::InflationVol});
626
627 // If there are no qualifiers, return early and set bool to false to indicate margin does not apply
628 if (qualifiers.empty()) {
629 bucketMargins["All"] = 0.0;
630 return make_pair(bucketMargins, false);
631 }
632
633 // Hold the concentration risk for each qualifier i.e. $VCR_b$ from SIMM docs
634 map<string, Real> concentrationRisk;
635 // The vega margin for each currency i.e. $K_b$ from SIMM docs
636 map<string, Real> vegaMargin;
637 // The sum of the weighted sensitivities for each currency i.e. $\sum_{k=1}^K VR_{k}$ from SIMM docs
638 map<string, Real> sumWeightedSensis;
639
640 // Loop over the qualifiers i.e. currencies
641 for (const auto& qualifier : qualifiers) {
642 // Pair of iterators to start and end of IRVol sensitivities with current qualifier
643 auto pIrQualifier = crif.filterByQualifier(nettingSetDetails, pc, RiskType::IRVol, qualifier);
644
645 // Pair of iterators to start and end of InflationVol sensitivities with current qualifier
646 auto pInfQualifier = crif.filterByQualifier(nettingSetDetails, pc, RiskType::InflationVol, qualifier);
647
648 // One pass to get the concentration risk for this qualifier
649 for (const auto& it : pIrQualifier) {
650 concentrationRisk[qualifier] += it.amountResultCcy;
651 }
652 for (const auto& it : pInfQualifier) {
653 concentrationRisk[qualifier] += it.amountResultCcy;
654 }
655 // Divide by the concentration risk threshold
656 Real concThreshold = simmConfiguration_->concentrationThreshold(RiskType::IRVol, qualifier);
657 if (resultCcy_ != "USD")
658 concThreshold *= market_->fxRate("USD" + resultCcy_)->value();
659 concentrationRisk[qualifier] /= concThreshold;
660
661 // Final concentration risk amount
662 concentrationRisk[qualifier] = max(1.0, sqrt(std::abs(concentrationRisk[qualifier])));
663
664 // Calculate the margin piece for this qualifier i.e. $K_b$ from SIMM docs
665 // Start with IRVol vs. IRVol components
666 for (auto itOuter = pIrQualifier.begin(); itOuter != pIrQualifier.end(); ++itOuter) {
667 // Risk weight i.e. $RW_k$ from SIMM docs
668 Real rwOuter = simmConfiguration_->weight(RiskType::IRVol, qualifier, itOuter->label1);
669 // Weighted sensitivity i.e. $WS_{k,i}$ from SIMM docs
670 Real wsOuter = rwOuter * itOuter->amountResultCcy * concentrationRisk[qualifier];
671 // Update weighted sensitivity sum
672 sumWeightedSensis[qualifier] += wsOuter;
673 // Add diagonal element to vega margin
674 vegaMargin[qualifier] += wsOuter * wsOuter;
675 // Add the cross elements to the vega margin
676 for (auto itInner = pIrQualifier.begin(); itInner != itOuter; ++itInner) {
677 // Label1 level correlation i.e. $\rho_{k,l}$ from SIMM docs
678 Real corr = simmConfiguration_->correlation(RiskType::IRVol, qualifier, itOuter->label1, "",
679 RiskType::IRVol, qualifier, itInner->label1, "");
680 // Add cross element to vega margin
681 Real rwInner = simmConfiguration_->weight(RiskType::IRVol, qualifier, itInner->label1);
682 Real wsInner = rwInner * itInner->amountResultCcy * concentrationRisk[qualifier];
683 vegaMargin[qualifier] += 2 * corr * wsOuter * wsInner;
684 }
685 }
686
687 // Now deal with inflation component
688 // To be generic/future-proof, assume that we don't know correlation structure. The way SIMM is
689 // currently, we could just sum over the InflationVol numbers within qualifier and use this.
690 for (auto itOuter = pInfQualifier.begin(); itOuter != pInfQualifier.end(); ++itOuter) {
691 // Risk weight i.e. $RW_k$ from SIMM docs
692 Real rwOuter = simmConfiguration_->weight(RiskType::InflationVol, qualifier, itOuter->label1);
693 // Weighted sensitivity i.e. $WS_{k,i}$ from SIMM docs
694 Real wsOuter = rwOuter * itOuter->amountResultCcy * concentrationRisk[qualifier];
695 // Update weighted sensitivity sum
696 sumWeightedSensis[qualifier] += wsOuter;
697 // Add diagonal element to vega margin
698 vegaMargin[qualifier] += wsOuter * wsOuter;
699 // Add the cross elements to the vega margin
700 // Firstly, against all IRVol components
701 for (auto itInner = pIrQualifier.begin(); itInner != pIrQualifier.end(); ++itInner) {
702 // Correlation i.e. $\rho_{k,l}$ from SIMM docs
703 Real corr = simmConfiguration_->correlation(RiskType::InflationVol, qualifier, itOuter->label1, "",
704 RiskType::IRVol, qualifier, itInner->label1, "");
705 // Add cross element to vega margin
706 Real rwInner = simmConfiguration_->weight(RiskType::IRVol, qualifier, itInner->label1);
707 Real wsInner = rwInner * itInner->amountResultCcy * concentrationRisk[qualifier];
708 vegaMargin[qualifier] += 2 * corr * wsOuter * wsInner;
709 }
710 // Secondly, against all previous InflationVol components
711 for (auto itInner = pInfQualifier.begin(); itInner != itOuter; ++itInner) {
712 // Correlation i.e. $\rho_{k,l}$ from SIMM docs
713 Real corr = simmConfiguration_->correlation(RiskType::InflationVol, qualifier, itOuter->label1, "",
714 RiskType::InflationVol, qualifier, itInner->label1, "");
715 // Add cross element to vega margin
716 Real rwInner = simmConfiguration_->weight(RiskType::InflationVol, qualifier, itInner->label1);
717 Real wsInner = rwInner * itInner->amountResultCcy * concentrationRisk[qualifier];
718 vegaMargin[qualifier] += 2 * corr * wsOuter * wsInner;
719 }
720 }
721
722 // Finally have the value of $K_b$
723 vegaMargin[qualifier] = sqrt(max(vegaMargin[qualifier], 0.0));
724 }
725
726 // Now calculate final vega margin by aggregating across currencies
727 Real margin = 0.0;
728 for (auto itOuter = qualifiers.begin(); itOuter != qualifiers.end(); ++itOuter) {
729 // Diagonal term
730 margin += vegaMargin.at(*itOuter) * vegaMargin.at(*itOuter);
731 // Cross terms
732 Real sOuter = max(min(sumWeightedSensis.at(*itOuter), vegaMargin.at(*itOuter)), -vegaMargin.at(*itOuter));
733 for (auto itInner = qualifiers.begin(); itInner != itOuter; ++itInner) {
734 Real sInner = max(min(sumWeightedSensis.at(*itInner), vegaMargin.at(*itInner)), -vegaMargin.at(*itInner));
735 Real g = min(concentrationRisk.at(*itOuter), concentrationRisk.at(*itInner)) /
736 max(concentrationRisk.at(*itOuter), concentrationRisk.at(*itInner));
737 Real corr = simmConfiguration_->correlation(RiskType::IRVol, *itOuter, "", "", RiskType::IRVol, *itInner,
738 "", "", calcCcy);
739 margin += 2.0 * sOuter * sInner * corr * g;
740 }
741 }
742 margin = sqrt(max(margin, 0.0));
743
744 for (const auto& m : vegaMargin)
745 bucketMargins[m.first] = m.second;
746 bucketMargins["All"] = margin;
747
748 return make_pair(bucketMargins, true);
749}
750
751pair<map<string, Real>, bool> SimmCalculator::irCurvatureMargin(const NettingSetDetails& nettingSetDetails,
752 const CrifRecord::ProductClass& pc,
753 const SimmSide& side, const Crif& crif) const {
754
755 // "Bucket" here refers to exposures under the CRIF qualifiers
756 map<string, Real> bucketMargins;
757
758 // Multiplier for sensitivities, -1 if SIMM side is Post
759 Real multiplier = side == SimmSide::Call ? 1.0 : -1.0;
760
761 // Find the set of qualifiers, i.e. currencies, in the Simm sensitivities
762 set<string> qualifiers = getQualifiers(crif, nettingSetDetails, pc, {RiskType::IRVol, RiskType::InflationVol});
763
764 // If there are no qualifiers, return early and set bool to false to indicate margin does not apply
765 if (qualifiers.empty()) {
766 bucketMargins["All"] = 0.0;
767 return make_pair(bucketMargins, false);
768 }
769
770 // The curvature margin for each currency i.e. $K_b$ from SIMM docs
771 map<string, Real> curvatureMargin;
772 // The sum of the weighted sensitivities for each currency i.e. $\sum_{k}^K CVR_{b,k}$ from SIMM docs
773 map<string, Real> sumWeightedSensis;
774 // The sum of all weighted sensitivities across currencies and risk factors
775 Real sumWs = 0.0;
776 // The sum of the absolute value of weighted sensitivities across currencies and risk factors
777 Real sumAbsWs = 0.0;
778
779 // Loop over the qualifiers i.e. currencies
780 for (const auto& qualifier : qualifiers) {
781 // Pair of iterators to start and end of IRVol sensitivities with current qualifier
782 auto pIrQualifier = crif.filterByQualifier(nettingSetDetails, pc, RiskType::IRVol, qualifier);
783
784 // Pair of iterators to start and end of InflationVol sensitivities with current qualifier
785 auto pInfQualifier =
786 crif.filterByQualifier(nettingSetDetails, pc, RiskType::InflationVol, qualifier);
787
788 // Calculate the margin piece for this qualifier i.e. $K_b$ from SIMM docs
789 // Start with IRVol vs. IRVol components
790 for (auto itOuter = pIrQualifier.begin(); itOuter != pIrQualifier.end(); ++itOuter) {
791 // Curvature weight i.e. $SF(t_{kj})$ from SIMM docs
792 Real sfOuter = simmConfiguration_->curvatureWeight(RiskType::IRVol, itOuter->label1);
793 // Curvature sensitivity i.e. $CVR_{ik}$ from SIMM docs
794 Real wsOuter = sfOuter * (itOuter->amountResultCcy * multiplier);
795 // Update weighted sensitivity sums
796 sumWeightedSensis[qualifier] += wsOuter;
797 sumWs += wsOuter;
798 sumAbsWs += std::abs(wsOuter);
799 // Add diagonal element to curvature margin
800 curvatureMargin[qualifier] += wsOuter * wsOuter;
801 // Add the cross elements to the curvature margin
802 for (auto itInner = pIrQualifier.begin(); itInner != itOuter; ++itInner) {
803 // Label1 level correlation i.e. $\rho_{k,l}$ from SIMM docs
804 Real corr = simmConfiguration_->correlation(RiskType::IRVol, qualifier, itOuter->label1, "",
805 RiskType::IRVol, qualifier, itInner->label1, "");
806 // Add cross element to curvature margin
807 Real sfInner = simmConfiguration_->curvatureWeight(RiskType::IRVol, itInner->label1);
808 Real wsInner = sfInner * (itInner->amountResultCcy * multiplier);
809 curvatureMargin[qualifier] += 2 * corr * corr * wsOuter * wsInner;
810 }
811 }
812
813 // Now deal with inflation component
814 const string simmVersion = simmConfiguration_->version();
815 SimmVersion thresholdVersion = SimmVersion::V1_0;
816 if (simmConfiguration_->isSimmConfigCalibration() || parseSimmVersion(simmVersion) > thresholdVersion) {
817 // Weighted sensitivity i.e. $WS_{k,i}$ from SIMM docs
818 Real infWs = 0.0;
819 for (auto infIt = pInfQualifier.begin(); infIt != pInfQualifier.end(); ++infIt) {
820 // Curvature weight i.e. $SF(t_{kj})$ from SIMM docs
821 Real infSf = simmConfiguration_->curvatureWeight(RiskType::InflationVol, infIt->label1);
822 infWs += infSf * (infIt->amountResultCcy * multiplier);
823 }
824 // Update weighted sensitivity sums
825 sumWeightedSensis[qualifier] += infWs;
826 sumWs += infWs;
827 sumAbsWs += std::abs(infWs);
828
829 // Add diagonal element to curvature margin - there is only one element for inflationVol
830 curvatureMargin[qualifier] += infWs * infWs;
831
832 // Add the cross elements to the curvature margin against IRVol components.
833 // There are no cross elements against InflationVol since we only have one element.
834 for (auto irIt = pIrQualifier.begin(); irIt != pIrQualifier.end(); ++irIt) {
835 // Correlation i.e. $\rho_{k,l}$ from SIMM docs
836 Real corr = simmConfiguration_->correlation(RiskType::InflationVol, qualifier, "", "", RiskType::IRVol,
837 qualifier, irIt->label1, "");
838 // Add cross element to curvature margin
839 Real irSf = simmConfiguration_->curvatureWeight(RiskType::IRVol, irIt->label1);
840 Real irWs = irSf * (irIt->amountResultCcy * multiplier);
841 curvatureMargin[qualifier] += 2 * corr * corr * infWs * irWs;
842 }
843 }
844
845 // Finally have the value of $K_b$
846 curvatureMargin[qualifier] = sqrt(max(curvatureMargin[qualifier], 0.0));
847 }
848
849 // If sum of absolute value of all individual curvature risks is zero, we can return 0.0
850 if (close_enough(sumAbsWs, 0.0)) {
851 bucketMargins["All"] = 0.0;
852 return make_pair(bucketMargins, true);
853 }
854
855 // Now calculate final curvature margin by aggregating across currencies
856 Real theta = min(sumWs / sumAbsWs, 0.0);
857
858 Real margin = 0.0;
859 for (auto itOuter = qualifiers.begin(); itOuter != qualifiers.end(); ++itOuter) {
860 // Diagonal term
861 margin += curvatureMargin.at(*itOuter) * curvatureMargin.at(*itOuter);
862 // Cross terms
863 Real sOuter =
864 max(min(sumWeightedSensis.at(*itOuter), curvatureMargin.at(*itOuter)), -curvatureMargin.at(*itOuter));
865 for (auto itInner = qualifiers.begin(); itInner != itOuter; ++itInner) {
866 Real sInner =
867 max(min(sumWeightedSensis.at(*itInner), curvatureMargin.at(*itInner)), -curvatureMargin.at(*itInner));
868 Real corr =
869 simmConfiguration_->correlation(RiskType::IRVol, *itOuter, "", "", RiskType::IRVol, *itInner, "", "");
870 margin += 2.0 * sOuter * sInner * corr * corr;
871 }
872 }
873 margin = sumWs + lambda(theta) * sqrt(max(margin, 0.0));
874
875 for (const auto& m : curvatureMargin)
876 bucketMargins[m.first] = m.second;
877
878 Real scaling = simmConfiguration_->curvatureMarginScaling();
879 Real totalCurvatureMargin = scaling * max(margin, 0.0);
880 // TODO: Review, should we return the pre-scaled value instead?
881 bucketMargins["All"] = totalCurvatureMargin;
882
883 return make_pair(bucketMargins, true);
884}
885
886pair<map<string, Real>, bool> SimmCalculator::margin(const NettingSetDetails& nettingSetDetails, const ProductClass& pc,
887 const RiskType& rt, const Crif& crif, const SimmSide& side) const {
888
889 const string& calcCcy = side == SimmSide::Call ? calculationCcyCall_ : calculationCcyPost_;
890
891 // "Bucket" here refers to exposures under the CRIF qualifiers for FX (and IR) risk class, and CRIF buckets for
892 // every other risk class.
893 // For FX Delta margin, this refers to WS_k in Section B. "Structure of the methodology", 8.(b).
894 // For FX Vega margin, this refers to VR_k in Section B., 10.(d).
895 // For other risk type, the bucket margin is K_b in the corresponding subsections.
896 map<string, Real> bucketMargins;
897
898 bool riskClassIsFX = rt == RiskType::FX || rt == RiskType::FXVol;
899
900 // precomputed
901 map<std::pair<std::string,std::string>, std::vector<CrifRecord>> crifByQualifierAndBucket;
902 map<std::string, std::vector<CrifRecord>> crifByBucket;
903
904 // Find the set of buckets and associated qualifiers for the netting set details, product class and risk type
905 map<string, set<string>> buckets;
906 for(const auto& it : crif.filterBy(nettingSetDetails, pc, rt)) {
907 buckets[it.bucket].insert(it.qualifier);
908 crifByQualifierAndBucket[std::make_pair(it.qualifier,it.bucket)].push_back(it);
909 crifByBucket[it.bucket].push_back(it);
910 }
911
912 // If there are no buckets, return early and set bool to false to indicate margin does not apply
913 if (buckets.empty()) {
914 bucketMargins["All"] = 0.0;
915 return make_pair(bucketMargins, false);
916 }
917
918 // The margin for each bucket i.e. $K_b$ from SIMM docs
919 map<string, Real> bucketMargin;
920 // The sum of the weighted sensitivities for each bucket i.e. $\sum_{k=1}^{K} WS_{k}$ from SIMM docs
921 map<string, Real> sumWeightedSensis;
922 // The historical volatility ratio for the risk type - will be 1.0 if not applicable
923 Real hvr = simmConfiguration_->historicalVolatilityRatio(rt);
924
925 // Loop over the buckets
926 for (const auto& kv : buckets) {
927 string bucket = kv.first;
928
929 // Initialise sumWeightedSensis here to ensure it is not empty in the later calculations
930 sumWeightedSensis[bucket] = 0.0;
931
932 // Get the concentration risk for each qualifier in current bucket i.e. $CR_k$ from SIMM docs
933 map<string, Real> concentrationRisk;
934
935 for (const auto& qualifier : kv.second) {
936
937 // Do not include Risk_FX components in the calculation currency in the SIMM calculation
938 if (rt == RiskType::FX && qualifier == calcCcy) {
939 if (!quiet_) {
940 DLOG("Not calculating concentration risk for qualifier "
941 << qualifier << " of risk type " << rt
942 << " since the qualifier equals the SIMM calculation currency " << calcCcy);
943 }
944 continue;
945 }
946
947 // Pair of iterators to start and end of sensitivities with current qualifier
948 auto pQualifier = crifByQualifierAndBucket[std::make_pair(qualifier,bucket)];
949
950 // One pass to get the concentration risk for this qualifier
951 for (auto it = pQualifier.begin(); it != pQualifier.end(); ++it) {
952 // Get the sigma value if applicable - returns 1.0 if not applicable
953 Real sigma = simmConfiguration_->sigma(rt, it->qualifier, it->label1, calcCcy);
954 concentrationRisk[qualifier] += it->amountResultCcy * sigma * hvr;
955 }
956 // Divide by the concentration risk threshold
957 Real concThreshold = simmConfiguration_->concentrationThreshold(rt, qualifier);
958 if (resultCcy_ != "USD")
959 concThreshold *= market_->fxRate("USD" + resultCcy_)->value();
960 concentrationRisk[qualifier] /= concThreshold;
961 // Final concentration risk amount
962 concentrationRisk[qualifier] = max(1.0, sqrt(std::abs(concentrationRisk[qualifier])));
963 }
964
965
966 // Calculate the margin component for the current bucket
967 // Pair of iterators to start and end of sensitivities within current bucket
968 auto pBucket = crifByBucket[bucket];
969 for (auto itOuter = pBucket.begin(); itOuter != pBucket.end(); ++itOuter) {
970 // Do not include Risk_FX components in the calculation currency in the SIMM calculation
971 if (rt == RiskType::FX && itOuter->qualifier == calcCcy) {
972 if (!quiet_) {
973 DLOG("Skipping qualifier " << itOuter->qualifier << " of risk type " << rt
974 << " since the qualifier equals the SIMM calculation currency "
975 << calcCcy);
976 }
977 continue;
978 }
979 // Risk weight i.e. $RW_k$ from SIMM docs
980 Real rwOuter = simmConfiguration_->weight(rt, itOuter->qualifier, itOuter->label1, calcCcy);
981 // Get the sigma value if applicable - returns 1.0 if not applicable
982 Real sigmaOuter = simmConfiguration_->sigma(rt, itOuter->qualifier, itOuter->label1, calcCcy);
983 // Weighted sensitivity i.e. $WS_{k}$ from SIMM docs
984 Real wsOuter =
985 rwOuter * (itOuter->amountResultCcy * sigmaOuter * hvr) * concentrationRisk[itOuter->qualifier];
986 // Get concentration risk for outer qualifier
987 Real outerConcentrationRisk = concentrationRisk.at(itOuter->qualifier);
988 // Update weighted sensitivity sum
989 sumWeightedSensis[bucket] += wsOuter;
990 // Add diagonal element to bucket margin
991 bucketMargin[bucket] += wsOuter * wsOuter;
992 // Add the cross elements to the bucket margin
993 for (auto itInner = pBucket.begin(); itInner != itOuter; ++itInner) {
994 // Do not include Risk_FX components in the calculation currency in the SIMM calculation
995 if (rt == RiskType::FX && itInner->qualifier == calcCcy) {
996 if (!quiet_) {
997 DLOG("Skipping qualifier " << itInner->qualifier << " of risk type " << rt
998 << " since the qualifier equals the SIMM calculation currency "
999 << calcCcy);
1000 }
1001 continue;
1002 }
1003 // Correlation, $\rho_{k,l}$ in the SIMM docs
1004 Real corr =
1005 simmConfiguration_->correlation(rt, itOuter->qualifier, itOuter->label1, itOuter->label2, rt,
1006 itInner->qualifier, itInner->label1, itInner->label2, calcCcy);
1007 // $f_{k,l}$ from the SIMM docs
1008 Real f = min(outerConcentrationRisk, concentrationRisk.at(itInner->qualifier)) /
1009 max(outerConcentrationRisk, concentrationRisk.at(itInner->qualifier));
1010 // Add cross element to delta margin
1011 Real sigmaInner = simmConfiguration_->sigma(rt, itInner->qualifier, itInner->label1, calcCcy);
1012 Real rwInner = simmConfiguration_->weight(rt, itInner->qualifier, itInner->label1, calcCcy);
1013 Real wsInner =
1014 rwInner * (itInner->amountResultCcy * sigmaInner * hvr) * concentrationRisk[itInner->qualifier];
1015 bucketMargin[bucket] += 2 * corr * f * wsOuter * wsInner;
1016 }
1017 // For FX risk class, results are broken down by qualifier, i.e. currency, instead of bucket, which is not used for Risk_FX
1018 if (riskClassIsFX)
1019 bucketMargins[itOuter->qualifier] += wsOuter;
1020 }
1021
1022 // Finally have the value of $K_b$
1023 bucketMargin[bucket] = sqrt(max(bucketMargin[bucket], 0.0));
1024 }
1025
1026 // If there is a "Residual" bucket entry store it separately
1027 // This is $K_{residual}$ from SIMM docs
1028 Real residualMargin = 0.0;
1029 if (bucketMargin.count("Residual") > 0) {
1030 residualMargin = bucketMargin.at("Residual");
1031 bucketMargin.erase("Residual");
1032 }
1033
1034 // Now calculate final margin by aggregating across non-residual buckets
1035 Real margin = 0.0;
1036 for (auto itOuter = bucketMargin.begin(); itOuter != bucketMargin.end(); ++itOuter) {
1037 string outerBucket = itOuter->first;
1038 // Diagonal term, $K_b^2$ from SIMM docs
1039 margin += itOuter->second * itOuter->second;
1040 // Cross terms
1041 // $S_b$ from SIMM docs
1042 Real sOuter = max(min(sumWeightedSensis.at(outerBucket), itOuter->second), -itOuter->second);
1043 for (auto itInner = bucketMargin.begin(); itInner != itOuter; ++itInner) {
1044 string innerBucket = itInner->first;
1045 // $S_c$ from SIMM docs
1046 Real sInner = max(min(sumWeightedSensis.at(innerBucket), itInner->second), -itInner->second);
1047 // $\gamma_{b,c}$ from SIMM docs
1048 // Interface to SimmConfiguration is on qualifiers => take any qualifier from each
1049 // of the respective (different) buckets to get the inter-bucket correlation
1050 string innerQualifier = *buckets.at(innerBucket).begin();
1051 string outerQualifier = *buckets.at(outerBucket).begin();
1052 Real corr = simmConfiguration_->correlation(rt, outerQualifier, "", "", rt, innerQualifier, "", "", calcCcy);
1053 margin += 2.0 * sOuter * sInner * corr;
1054 }
1055 }
1056 margin = sqrt(max(margin, 0.0));
1057
1058 // Now add the residual component back in
1059 margin += residualMargin;
1060 if (!close_enough(residualMargin, 0.0))
1061 bucketMargins["Residual"] = residualMargin;
1062
1063 // For non-FX risk class, results are broken down by buckets
1064 if (!riskClassIsFX)
1065 for (const auto& m : bucketMargin)
1066 bucketMargins[m.first] = m.second;
1067 else
1068 for (auto& m : bucketMargins)
1069 m.second = std::abs(m.second);
1070
1071 bucketMargins["All"] = margin;
1072 return make_pair(bucketMargins, true);
1073}
1074
1075pair<map<string, Real>, bool>
1076SimmCalculator::curvatureMargin(const NettingSetDetails& nettingSetDetails, const ProductClass& pc, const RiskType& rt,
1077 const SimmSide& side, const Crif& crif, bool rfLabels) const {
1078
1079 const string& calcCcy = side == SimmSide::Call ? calculationCcyCall_ : calculationCcyPost_;
1080
1081 // "Bucket" here refers to exposures under the CRIF qualifiers for FX (and IR) risk class, and CRIF buckets for
1082 // every other risk class
1083 // For FX Curvature marrgin, this refers to CVR_{b,k} in Section B. "Structure of the methodology", 11.(c).
1084 // For other risk types, the bucket margin is K_b in the corresponding subsection.
1085 map<string, Real> bucketMargins;
1086
1087 bool riskClassIsFX = rt == RiskType::FX || rt == RiskType::FXVol;
1088
1089 // Multiplier for sensitivities, -1 if SIMM side is Post
1090 Real multiplier = side == SimmSide::Call ? 1.0 : -1.0;
1091
1092 // Find the set of buckets and associated qualifiers for the netting set details, product class and risk type
1093 map<string, set<string>> buckets;
1094 for(const auto& it : crif.filterBy(nettingSetDetails, pc, rt)) {
1095 buckets[it.bucket].insert(it.qualifier);
1096 }
1097
1098 // If there are no buckets, return early and set bool to false to indicate margin does not apply
1099 if (buckets.empty()) {
1100 bucketMargins["All"] = 0.0;
1101 return make_pair(bucketMargins, false);
1102 }
1103
1104 // The curvature margin for each bucket i.e. $K_b$ from SIMM docs
1105 map<string, Real> curvatureMargin;
1106 // The sum of the weighted (and absolute weighted) sensitivities for each bucket
1107 // i.e. $\sum_{k}^K CVR_{b,k}$ from SIMM docs
1108 map<string, Real> sumWeightedSensis;
1109 map<string, map<string, Real>> sumAbsTemp;
1110 map<string, Real> sumAbsWeightedSensis;
1111
1112 // Loop over the buckets
1113 for (const auto& kv : buckets) {
1114 string bucket = kv.first;
1115 sumAbsTemp[bucket] = {};
1116
1117 // Calculate the margin component for the current bucket
1118 // Pair of iterators to start and end of sensitivities within current bucket
1119 auto pBucket = crif.filterByBucket(nettingSetDetails, pc, rt, bucket);
1120 for (auto itOuter = pBucket.begin(); itOuter != pBucket.end(); ++itOuter) {
1121 // Curvature weight i.e. $SF(t_{kj})$ from SIMM docs
1122 Real sfOuter = simmConfiguration_->curvatureWeight(rt, itOuter->label1);
1123 // Get the sigma value if applicable - returns 1.0 if not applicable
1124 Real sigmaOuter = simmConfiguration_->sigma(rt, itOuter->qualifier, itOuter->label1, calcCcy);
1125 // Weighted curvature i.e. $CVR_{ik}$ from SIMM docs
1126 // WARNING: The order of multiplication here is important because unit tests fail if for
1127 // example you use sfOuter * (itOuter->amountResultCcy * multiplier) * sigmaOuter;
1128 Real wsOuter = sfOuter * ((itOuter->amountResultCcy * multiplier) * sigmaOuter);
1129 // for ISDA SIMM 2.2 or higher, this $CVR_{ik}$ for EQ bucket 12 is zero
1130 const string simmVersion = simmConfiguration_->version();
1131 SimmVersion thresholdVersion = SimmVersion::V2_2;
1132 if ((simmConfiguration_->isSimmConfigCalibration() || parseSimmVersion(simmVersion) >= thresholdVersion) &&
1133 bucket == "12" && rt == RiskType::EquityVol) {
1134 wsOuter = 0.0;
1135 }
1136 // Update weighted sensitivity sum
1137 sumWeightedSensis[bucket] += wsOuter;
1138 sumAbsTemp[bucket][itOuter->qualifier] += rfLabels ? std::abs(wsOuter) : wsOuter;
1139 // Add diagonal element to curvature margin
1140 curvatureMargin[bucket] += wsOuter * wsOuter;
1141 // Add the cross elements to the curvature margin
1142 for (auto itInner = pBucket.begin(); itInner != itOuter; ++itInner) {
1143 // Correlation, $\rho_{k,l}$ in the SIMM docs
1144 Real corr = simmConfiguration_->correlation(rt, itOuter->qualifier, itOuter->label1, itOuter->label2,
1145 rt, itInner->qualifier, itInner->label1, itInner->label2,
1146 calcCcy);
1147 // Add cross element to delta margin
1148 Real sfInner = simmConfiguration_->curvatureWeight(rt, itInner->label1);
1149 Real sigmaInner = simmConfiguration_->sigma(rt, itInner->qualifier, itInner->label1, calcCcy);
1150 Real wsInner = sfInner * ((itInner->amountResultCcy * multiplier) * sigmaInner);
1151 curvatureMargin[bucket] += 2 * corr * corr * wsOuter * wsInner;
1152 }
1153 // For FX risk class, results are broken down by qualifier, i.e. currency, instead of bucket, which is not
1154 // used for Risk_FX
1155 if (riskClassIsFX)
1156 bucketMargins[itOuter->qualifier] += wsOuter;
1157 }
1158
1159 // Finally have the value of $K_b$
1160 Real bucketCurvatureMargin = sqrt(max(curvatureMargin[bucket], 0.0));
1161 curvatureMargin[bucket] = bucketCurvatureMargin;
1162
1163 // Bucket level absolute sensitivity
1164 for (const auto& kv : sumAbsTemp[bucket]) {
1165 sumAbsWeightedSensis[bucket] += std::abs(kv.second);
1166 }
1167 }
1168
1169 // If there is a "Residual" bucket entry store it separately
1170 // This is $K_{residual}$ from SIMM docs
1171 Real residualMargin = 0.0;
1172 Real residualSum = 0.0;
1173 Real residualAbsSum = 0.0;
1174 if (curvatureMargin.count("Residual") > 0) {
1175 residualMargin = curvatureMargin.at("Residual");
1176 residualSum = sumWeightedSensis.at("Residual");
1177 residualAbsSum = sumAbsWeightedSensis.at("Residual");
1178 // Remove the entries for "Residual" bucket
1179 curvatureMargin.erase("Residual");
1180 sumWeightedSensis.erase("Residual");
1181 sumAbsWeightedSensis.erase("Residual");
1182 }
1183
1184 // Now calculate final margin
1185 Real margin = 0.0;
1186
1187 // First, aggregating across non-residual buckets
1188 auto acc = [](const Real p, const pair<const string, Real>& kv) { return p + kv.second; };
1189 Real sumSensis = accumulate(sumWeightedSensis.begin(), sumWeightedSensis.end(), 0.0, acc);
1190 Real sumAbsSensis = accumulate(sumAbsWeightedSensis.begin(), sumAbsWeightedSensis.end(), 0.0, acc);
1191
1192 if (!close_enough(sumAbsSensis, 0.0)) {
1193 Real theta = min(sumSensis / sumAbsSensis, 0.0);
1194 for (auto itOuter = curvatureMargin.begin(); itOuter != curvatureMargin.end(); ++itOuter) {
1195 string outerBucket = itOuter->first;
1196 // Diagonal term
1197 margin += itOuter->second * itOuter->second;
1198 // Cross terms
1199 // $S_b$ from SIMM docs
1200 Real sOuter = max(min(sumWeightedSensis.at(outerBucket), itOuter->second), -itOuter->second);
1201 for (auto itInner = curvatureMargin.begin(); itInner != itOuter; ++itInner) {
1202 string innerBucket = itInner->first;
1203 // $S_c$ from SIMM docs
1204 Real sInner = max(min(sumWeightedSensis.at(innerBucket), itInner->second), -itInner->second);
1205 // $\gamma_{b,c}$ from SIMM docs
1206 // Interface to SimmConfiguration is on qualifiers => take any qualifier from each
1207 // of the respective (different) buckets to get the inter-bucket correlation
1208 string innerQualifier = *buckets.at(innerBucket).begin();
1209 string outerQualifier = *buckets.at(outerBucket).begin();
1210 Real corr = simmConfiguration_->correlation(rt, outerQualifier, "", "", rt, innerQualifier, "", "", calcCcy);
1211 margin += 2.0 * sOuter * sInner * corr * corr;
1212 }
1213 }
1214 margin = max(sumSensis + lambda(theta) * sqrt(max(margin, 0.0)), 0.0);
1215 }
1216
1217 // Second, the residual bucket if necessary, and add "Residual" bucket back in to be added to the SIMM results
1218 if (!close_enough(residualAbsSum, 0.0)) {
1219 Real theta = min(residualSum / residualAbsSum, 0.0);
1220 curvatureMargin["Residual"] = max(residualSum + lambda(theta) * residualMargin, 0.0);
1221 margin += curvatureMargin["Residual"];
1222 }
1223
1224 // For non-FX risk class, results are broken down by buckets
1225 if (!riskClassIsFX)
1226 for (const auto& m : curvatureMargin)
1227 bucketMargins[m.first] = m.second;
1228 else
1229 for (auto& m : bucketMargins)
1230 m.second = std::abs(m.second);
1231
1232 bucketMargins["All"] = margin;
1233 return make_pair(bucketMargins, true);
1234}
1235
1236void SimmCalculator::calcAddMargin(const SimmSide& side, const NettingSetDetails& nettingSetDetails,
1237 const string& regulation, const Crif& crif) {
1238
1239 // Reference to SIMM results for this portfolio
1240 auto& results = simmResults_[side][nettingSetDetails][regulation];
1241
1242 const bool overwrite = false;
1243
1244 if (!quiet_) {
1245 DLOG("Calculating additional margin for portfolio [" << nettingSetDetails << "], regulation " << regulation
1246 << " and SIMM side " << side);
1247 }
1248
1249 // First, add scaled additional margin, using "ProductClassMultiplier"
1250 // risk type, for the portfolio
1251 auto pc = ProductClass::Empty;
1252 auto rt = RiskType::ProductClassMultiplier;
1253 auto pIt = crif.filterBy(nettingSetDetails, pc, rt);
1254
1255 for(const auto& it : pIt) {
1256 // Qualifier should be a product class string
1257 auto qpc = parseProductClass(it.qualifier);
1258 if (results.has(qpc, RiskClass::All, MarginType::All, "All")) {
1259 Real im = results.get(qpc, RiskClass::All, MarginType::All, "All");
1260 Real factor = it.amount;
1261 QL_REQUIRE(factor >= 0.0, "SIMM Calculator: Amount for risk type "
1262 << rt << " must be greater than or equal to 0 but we got " << factor);
1263 Real pcmMargin = (factor - 1.0) * im;
1264 add(nettingSetDetails, regulation, qpc, RiskClass::All, MarginType::AdditionalIM, "All", pcmMargin, side,
1265 overwrite);
1266
1267 // Add to aggregation at margin type level
1268 add(nettingSetDetails, regulation, qpc, RiskClass::All, MarginType::All, "All", pcmMargin, side, overwrite);
1269 // Add to aggregation at product class level
1270 add(nettingSetDetails, regulation, ProductClass::All, RiskClass::All, MarginType::AdditionalIM, "All", pcmMargin,
1271 side, overwrite);
1272 // Add to aggregation at portfolio level
1273 add(nettingSetDetails, regulation, ProductClass::All, RiskClass::All, MarginType::All, "All", pcmMargin, side,
1274 overwrite);
1275 CrifRecord spRecord = it;
1276 if (side == SimmSide::Call)
1277 spRecord.collectRegulations = regulation;
1278 else
1279 spRecord.postRegulations = regulation;
1280 simmParameters_.addRecord(spRecord);
1281 }
1282 }
1283
1284 // Second, add fixed amounts IM, using "AddOnFixedAmount" risk type, for the portfolio
1285 pIt = crif.filterBy(nettingSetDetails, pc, RiskType::AddOnFixedAmount);
1286 for(const auto& it : pIt){
1287 Real fixedMargin = it.amountResultCcy;
1288 add(nettingSetDetails, regulation, ProductClass::AddOnFixedAmount, RiskClass::All, MarginType::AdditionalIM,
1289 "All", fixedMargin, side, overwrite);
1290
1291 // Add to aggregation at margin type level
1292 add(nettingSetDetails, regulation, ProductClass::AddOnFixedAmount, RiskClass::All, MarginType::All, "All",
1293 fixedMargin,
1294 side, overwrite);
1295 // Add to aggregation at product class level
1296 add(nettingSetDetails, regulation, ProductClass::All, RiskClass::All, MarginType::AdditionalIM, "All",
1297 fixedMargin, side, overwrite);
1298 // Add to aggregation at portfolio level
1299 add(nettingSetDetails, regulation, ProductClass::All, RiskClass::All, MarginType::All, "All", fixedMargin, side,
1300 overwrite);
1301 CrifRecord spRecord = it;
1302 if (side == SimmSide::Call)
1303 spRecord.collectRegulations = regulation;
1304 else
1305 spRecord.postRegulations = regulation;
1306 simmParameters_.addRecord(spRecord);
1307 }
1308
1309 // Third, add percentage of notional amounts IM, using "AddOnNotionalFactor"
1310 // and "Notional" risk types, for the portfolio.
1311 pIt = crif.filterBy(nettingSetDetails, pc, RiskType::AddOnNotionalFactor);
1312 for(const auto& it : pIt){
1313 // We should have a single corresponding CrifRecord with risk type
1314 // "Notional" and the same qualifier. Search for it.
1315 auto pQualifierIt = crif.filterByQualifier(nettingSetDetails, pc, RiskType::Notional, it.qualifier);
1316 const auto count = pQualifierIt.size();
1317 QL_REQUIRE(count < 2, "Expected either 0 or 1 elements for risk type "
1318 << RiskType::Notional << " and qualifier " << it.qualifier
1319 << " but got " << count);
1320
1321 // If we have found a corresponding notional, update the additional margin
1322 if (count == 1) {
1323 Real notional = pQualifierIt.front().amountResultCcy;
1324 Real factor = it.amount;
1325 Real notionalFactorMargin = notional * factor / 100.0;
1326
1327 add(nettingSetDetails, regulation, ProductClass::AddOnNotionalFactor, RiskClass::All,
1328 MarginType::AdditionalIM, "All", notionalFactorMargin, side, overwrite);
1329
1330 // Add to aggregation at margin type level
1331 add(nettingSetDetails, regulation, ProductClass::AddOnNotionalFactor, RiskClass::All, MarginType::All,
1332 "All",
1333 notionalFactorMargin, side, overwrite);
1334 // Add to aggregation at product class level
1335 add(nettingSetDetails, regulation, ProductClass::All, RiskClass::All, MarginType::AdditionalIM, "All",
1336 notionalFactorMargin, side, overwrite);
1337 // Add to aggregation at portfolio level
1338 add(nettingSetDetails, regulation, ProductClass::All, RiskClass::All, MarginType::All, "All",
1339 notionalFactorMargin,
1340 side, overwrite);
1341 CrifRecord spRecord = it;
1342 if (side == SimmSide::Call)
1343 spRecord.collectRegulations = regulation;
1344 else
1345 spRecord.postRegulations = regulation;
1346 simmParameters_.addRecord(spRecord);
1347 }
1348 }
1349}
1350
1351void SimmCalculator::populateResults(const SimmSide& side, const NettingSetDetails& nettingSetDetails,
1352 const string& regulation) {
1353
1354 if (!quiet_) {
1355 LOG("SimmCalculator: Populating higher level results")
1356 }
1357
1358 // Sets of classes (excluding 'All')
1359 auto pcs = simmConfiguration_->productClasses(false);
1360 auto rcs = simmConfiguration_->riskClasses(false);
1361 auto mts = simmConfiguration_->marginTypes(false);
1362
1363 // Populate netting set level results for each portfolio
1364
1365 // Reference to SIMM results for this portfolio
1366 auto& results = simmResults_[side][nettingSetDetails][regulation];
1367
1368 // Fill in the margin within each (product class, risk class) combination
1369 for (const auto& pc : pcs) {
1370 for (const auto& rc : rcs) {
1371 // Margin for a risk class is just sum over margin for each margin type
1372 // within that risk class
1373 Real riskClassMargin = 0.0;
1374 bool hasRiskClass = false;
1375 for (const auto& mt : mts) {
1376 if (results.has(pc, rc, mt, "All")) {
1377 riskClassMargin += results.get(pc, rc, mt, "All");
1378 if (!hasRiskClass)
1379 hasRiskClass = true;
1380 }
1381 }
1382
1383 // Add the margin to the results if it was calculated
1384 if (hasRiskClass) {
1385 add(nettingSetDetails, regulation, pc, rc, MarginType::All, "All", riskClassMargin, side);
1386 }
1387 }
1388 }
1389
1390 // Fill in the margin within each product class by aggregating across risk classes
1391 for (const auto& pc : pcs) {
1392 Real productClassMargin = 0.0;
1393 bool hasProductClass = false;
1394
1395 // IM within product class across risk classes requires correlation
1396 // o suffix => outer and i suffix => inner here
1397 for (auto ito = rcs.begin(); ito != rcs.end(); ++ito) {
1398 // Skip to next if no results for current risk class
1399 if (!results.has(pc, *ito, MarginType::All, "All"))
1400 continue;
1401 // If we get to here, we have the current product class
1402 if (!hasProductClass)
1403 hasProductClass = true;
1404
1405 Real imo = results.get(pc, *ito, MarginType::All, "All");
1406 // Diagonal term
1407 productClassMargin += imo * imo;
1408
1409 // Now, cross terms
1410 for (auto iti = rcs.begin(); iti != ito; ++iti) {
1411 if (!results.has(pc, *iti, MarginType::All, "All"))
1412 continue;
1413 Real imi = results.get(pc, *iti, MarginType::All, "All");
1414 // Get the correlation between risk classes
1415 Real corr = simmConfiguration_->correlationRiskClasses(*ito, *iti);
1416 productClassMargin += 2.0 * corr * imo * imi;
1417 }
1418 }
1419
1420 // Add the margin to the results if it was calculated
1421 if (hasProductClass) {
1422 productClassMargin = sqrt(max(productClassMargin, 0.0));
1423 add(nettingSetDetails, regulation, pc, RiskClass::All, MarginType::All, "All", productClassMargin, side);
1424 }
1425 }
1426
1427 // Overall initial margin for the portfolio is the sum of the initial margin in
1428 // each of the product classes. Could have done it in the last loop but cleaner here.
1429 Real im = 0.0;
1430 for (const auto& pc : pcs) {
1431 if (results.has(pc, RiskClass::All, MarginType::All, "All")) {
1432 im += results.get(pc, RiskClass::All, MarginType::All, "All");
1433 }
1434 }
1435 add(nettingSetDetails, regulation, ProductClass::All, RiskClass::All, MarginType::All, "All", im, side);
1436
1437 // Combinations outside of the natural SIMM hierarchy
1438
1439 // Across risk class, for each product class and margin type
1440 for (const auto& pc : pcs) {
1441 for (const auto& mt : mts) {
1442 Real margin = 0.0;
1443 bool hasPcMt = false;
1444
1445 // IM within product class and margin type across risk classes
1446 // requires correlation. o suffix => outer and i suffix => inner here
1447 for (auto ito = rcs.begin(); ito != rcs.end(); ++ito) {
1448 // Skip to next if no results for current risk class
1449 if (!results.has(pc, *ito, mt, "All"))
1450 continue;
1451 // If we get to here, we have the current product class & margin type
1452 if (!hasPcMt)
1453 hasPcMt = true;
1454
1455 Real imo = results.get(pc, *ito, mt, "All");
1456 // Diagonal term
1457 margin += imo * imo;
1458
1459 // Now, cross terms
1460 for (auto iti = rcs.begin(); iti != ito; ++iti) {
1461 if (!results.has(pc, *iti, mt, "All"))
1462 continue;
1463 Real imi = results.get(pc, *iti, mt, "All");
1464 // Get the correlation between risk classes
1465 Real corr = simmConfiguration_->correlationRiskClasses(*ito, *iti);
1466 margin += 2.0 * corr * imo * imi;
1467 }
1468 }
1469
1470 // Add the margin to the results if it was calculated
1471 if (hasPcMt) {
1472 margin = sqrt(max(margin, 0.0));
1473 add(nettingSetDetails, regulation, pc, RiskClass::All, mt, "All", margin, side);
1474 }
1475 }
1476 }
1477
1478 // Across product class, for each risk class and margin type
1479 for (const auto& rc : rcs) {
1480 for (const auto& mt : mts) {
1481 Real margin = 0.0;
1482 bool hasRcMt = false;
1483
1484 // Can just sum across product class
1485 for (const auto& pc : pcs) {
1486 // Skip to next if no results for current product class
1487 if (!results.has(pc, rc, mt, "All"))
1488 continue;
1489 // If we get to here, we have the current risk class & margin type
1490 if (!hasRcMt)
1491 hasRcMt = true;
1492
1493 margin += results.get(pc, rc, mt, "All");
1494 }
1495
1496 // Add the margin to the results if it was calculated
1497 if (hasRcMt) {
1498 add(nettingSetDetails, regulation, ProductClass::All, rc, mt, "All", margin, side);
1499 }
1500 }
1501 }
1502
1503 // Across product class and margin type for each risk class
1504 // Have already computed MarginType::All results above so just need
1505 // to sum over product class for each risk class here
1506 for (const auto& rc : rcs) {
1507 Real margin = 0.0;
1508 bool hasRc = false;
1509
1510 for (const auto& pc : pcs) {
1511 // Skip to next if no results for current product class
1512 if (!results.has(pc, rc, MarginType::All, "All"))
1513 continue;
1514 // If we get to here, we have the current risk class
1515 if (!hasRc)
1516 hasRc = true;
1517
1518 margin += results.get(pc, rc, MarginType::All, "All");
1519 }
1520
1521 // Add the margin to the results if it was calculated
1522 if (hasRc) {
1523 add(nettingSetDetails, regulation, ProductClass::All, rc, MarginType::All, "All", margin, side);
1524 }
1525 }
1526
1527 // Across product class and risk class for each margin type
1528 // Have already computed RiskClass::All results above so just need
1529 // to sum over product class for each margin type here
1530 for (const auto& mt : mts) {
1531 Real margin = 0.0;
1532 bool hasMt = false;
1533
1534 for (const auto& pc : pcs) {
1535 // Skip to next if no results for current product class
1536 if (!results.has(pc, RiskClass::All, mt, "All"))
1537 continue;
1538 // If we get to here, we have the current risk class
1539 if (!hasMt)
1540 hasMt = true;
1541
1542 margin += results.get(pc, RiskClass::All, mt, "All");
1543 }
1544
1545 // Add the margin to the results if it was calculated
1546 if (hasMt) {
1547 add(nettingSetDetails, regulation, ProductClass::All, RiskClass::All, mt, "All", margin, side);
1548 }
1549 }
1550}
1551
1552void SimmCalculator::populateFinalResults(const map<SimmSide, map<NettingSetDetails, string>>& winningRegs) {
1553
1554 if (!quiet_) {
1555 LOG("SimmCalculator: Populating final winning regulators' IM");
1556 }
1557 winningRegulations_ = winningRegs;
1558
1559 // Populate list of trade IDs of final trades used for SIMM winning reg
1560 for (auto& tids : finalTradeIds_)
1561 tids.second.clear();
1562 for (const auto& sv : winningRegs) {
1563 SimmSide side = sv.first;
1564 finalTradeIds_.insert(std::make_pair(side, std::set<string>()));
1565
1566 for (const auto& nv : sv.second) {
1567 NettingSetDetails nsd = nv.first;
1568 string winningReg = nv.second;
1569
1570 if (tradeIds_.count(side) > 0)
1571 if (tradeIds_.at(side).count(nsd) > 0)
1572 if (tradeIds_.at(side).at(nsd).count(winningReg) > 0)
1573 for (const string& tid : tradeIds_.at(side).at(nsd).at(winningReg))
1574 finalTradeIds_.at(side).insert(tid);
1575 }
1576 }
1577
1578 // Populate final SIMM results
1579 for (const auto& sv : simmResults_) {
1580 const SimmSide side = sv.first;
1581
1582 for (const auto& nv : sv.second) {
1583 const NettingSetDetails& nsd = nv.first;
1584
1585 const string& reg = winningRegulations(side, nsd);
1586 // If no results found for winning regulator, i.e IM is Schedule IM only, use empty SIMM results
1587 const SimmResults simmResults = nv.second.find(reg) == nv.second.end()
1588 ? SimmResults(resultCcy_)
1589 : nv.second.at(reg);
1590 finalSimmResults_[side][nsd] = make_pair(reg, simmResults);
1591 }
1592 }
1593}
1594
1597}
1598
1599void SimmCalculator::add(const NettingSetDetails& nettingSetDetails, const string& regulation, const ProductClass& pc,
1600 const RiskClass& rc, const MarginType& mt, const string& b, Real margin, SimmSide side,
1601 const bool overwrite) {
1602 if (!quiet_) {
1603 DLOG("Calculated " << side << " margin for [netting set details, product class, risk class, margin type] = ["
1604 << "[" << NettingSetDetails(nettingSetDetails) << "]"
1605 << ", " << pc << ", " << rc << ", " << mt << "] of " << margin);
1606 }
1607
1608 const string& calculationCcy = side == SimmSide::Call ? calculationCcyCall_ : calculationCcyPost_;
1609 simmResults_[side][nettingSetDetails][regulation].add(pc, rc, mt, b, margin, resultCcy_, calculationCcy, overwrite);
1610}
1611
1612void SimmCalculator::add(const NettingSetDetails& nettingSetDetails, const string& regulation, const ProductClass& pc,
1613 const RiskClass& rc, const MarginType& mt, const map<string, Real>& margins, SimmSide side,
1614 const bool overwrite) {
1615
1616 for (const auto& kv : margins)
1617 add(nettingSetDetails, regulation, pc, rc, mt, kv.first, kv.second, side, overwrite);
1618}
1619
1620void SimmCalculator::splitCrifByRegulationsAndPortfolios(const Crif& crif, const bool enforceIMRegulations) {
1621 for (const auto& crifRecord : crif) {
1622 for (const auto& side : {SimmSide::Call, SimmSide::Post}) {
1623 const NettingSetDetails& nettingSetDetails = crifRecord.nettingSetDetails;
1624
1625 bool collectRegsIsEmpty = false;
1626 bool postRegsIsEmpty = false;
1627 if (collectRegsIsEmpty_.find(crifRecord.nettingSetDetails) != collectRegsIsEmpty_.end())
1628 collectRegsIsEmpty = collectRegsIsEmpty_.at(crifRecord.nettingSetDetails);
1629 if (postRegsIsEmpty_.find(crifRecord.nettingSetDetails) != postRegsIsEmpty_.end())
1630 postRegsIsEmpty = postRegsIsEmpty_.at(crifRecord.nettingSetDetails);
1631
1632 string regsString;
1633 if (enforceIMRegulations)
1634 regsString = side == SimmSide::Call ? crifRecord.collectRegulations : crifRecord.postRegulations;
1635 set<string> regs = parseRegulationString(regsString);
1636
1637 auto newCrifRecord = crifRecord;
1638 newCrifRecord.collectRegulations.clear();
1639 newCrifRecord.postRegulations.clear();
1640 for (const string& r : regs) {
1641 if (r == "Excluded" ||
1642 (r == "Unspecified" && enforceIMRegulations && !(collectRegsIsEmpty && postRegsIsEmpty))) {
1643 continue;
1644 } else if (r != "Excluded") {
1645 // Keep a record of trade IDs for each regulation
1646 if (!newCrifRecord.isSimmParameter())
1647 tradeIds_[side][nettingSetDetails][r].insert(newCrifRecord.tradeId);
1648 // We make sure to ignore amountCcy when aggregating the records, since we will only be using
1649 // amountResultCcy, and we may have CRIF records that are equal everywhere except for the amountCcy,
1650 // and this will fail in the case of Risk_XCcyBasis and Risk_Inflation.
1651 const bool onDiffAmountCcy = true;
1652 regSensitivities_[side][nettingSetDetails][r].addRecord(newCrifRecord, onDiffAmountCcy);
1653 }
1654 }
1655 }
1656 }
1657}
1658
1659Real SimmCalculator::lambda(Real theta) const {
1660 // Use boost inverse normal here as opposed to QL. Using QL inverse normal
1661 // will cause the ISDA SIMM unit tests to fail
1662 static Real q = boost::math::quantile(boost::math::normal(), 0.995);
1663
1664 return (q * q - 1.0) * (1.0 + theta) - theta;
1665}
1666
1667std::set<std::string> SimmCalculator::getQualifiers(const Crif& crif,
1668 const ore::data::NettingSetDetails& nettingSetDetails,
1669 const CrifRecord::ProductClass& pc,
1670 const std::vector<CrifRecord::RiskType>& riskTypes) const {
1671 std::set<std::string> qualifiers;
1672 for (auto& rt : riskTypes) {
1673 auto qualis = crif.qualifiersBy(nettingSetDetails, pc, rt);
1674 qualifiers.insert(qualis.begin(), qualis.end());
1675 }
1676 return qualifiers;
1677}
1678
1679
1680} // namespace analytics
1681} // namespace ore
Crif aggregate() const
Aggregate all existing records.
Definition: crif.cpp:160
std::set< std::string > qualifiersBy(const NettingSetDetails nsd, CrifRecord::ProductClass pc, const CrifRecord::RiskType rt) const
Definition: crif.cpp:227
std::vector< CrifRecord > filterByQualifier(const NettingSetDetails &nsd, const CrifRecord::ProductClass pc, const CrifRecord::RiskType rt, const std::string &qualifier) const
Definition: crif.cpp:247
std::vector< CrifRecord > filterByBucket(const NettingSetDetails &nsd, const CrifRecord::ProductClass pc, const CrifRecord::RiskType rt, const std::string &bucket) const
Definition: crif.cpp:257
std::set< CrifRecord::ProductClass > ProductClassesByNettingSetDetails(const NettingSetDetails nsd) const
Definition: crif.cpp:324
bool empty() const
Definition: crif.hpp:55
std::set< CrifRecord >::const_iterator end() const
Definition: crif.hpp:48
void addRecord(const CrifRecord &record, bool aggregateDifferentAmountCurrencies=false, bool sortFxVolQualifer=true)
Definition: crif.cpp:41
size_t countMatching(const NettingSetDetails &nsd, const CrifRecord::ProductClass pc, const CrifRecord::RiskType rt, const std::string &qualifier) const
Definition: crif.cpp:334
std::set< CrifRecord >::const_iterator findBy(const NettingSetDetails nsd, CrifRecord::ProductClass pc, const CrifRecord::RiskType rt, const std::string &qualifier) const
Find first element.
Definition: crif.cpp:196
std::vector< CrifRecord > filterBy(const NettingSetDetails &nsd, const CrifRecord::ProductClass pc, const CrifRecord::RiskType rt) const
Definition: crif.cpp:266
const std::map< SimmSide, std::map< ore::data::NettingSetDetails, std::map< std::string, SimmResults > > > & simmResults() const
std::pair< std::map< std::string, QuantLib::Real >, bool > margin(const ore::data::NettingSetDetails &nettingSetDetails, const CrifRecord::ProductClass &pc, const CrifRecord::RiskType &rt, const ore::analytics::Crif &netRecords, const SimmSide &side) const
std::set< std::string > getQualifiers(const Crif &crif, const ore::data::NettingSetDetails &nettingSetDetails, const CrifRecord::ProductClass &pc, const std::vector< CrifRecord::RiskType > &riskTypes) const
std::map< SimmSide, std::set< NettingSetDetails > > hasCFTC_
std::map< SimmSide, std::set< NettingSetDetails > > hasSEC_
std::pair< std::map< std::string, QuantLib::Real >, bool > irDeltaMargin(const ore::data::NettingSetDetails &nettingSetDetails, const CrifRecord::ProductClass &pc, const ore::analytics::Crif &netRecords, const SimmSide &side) const
Calculate the Interest Rate delta margin component for the given portfolio and product class.
std::string calculationCcyCall_
The SIMM exposure calculation currency i.e. the currency for which FX delta risk is ignored.
QuantLib::Real lambda(QuantLib::Real theta) const
Give the used in the curvature margin calculation.
std::pair< std::map< std::string, QuantLib::Real >, bool > irVegaMargin(const ore::data::NettingSetDetails &nettingSetDetails, const CrifRecord::ProductClass &pc, const ore::analytics::Crif &netRecords, const SimmSide &side) const
Calculate the Interest Rate vega margin component for the given portfolio and product class.
void calcAddMargin(const SimmSide &side, const ore::data::NettingSetDetails &nsd, const string &regulation, const ore::analytics::Crif &netRecords)
Calculate the additional initial margin for the portfolio ID and regulation.
std::map< SimmSide, std::map< ore::data::NettingSetDetails, std::string > > winningRegulations_
Regulation with highest initial margin for each given netting set.
std::pair< std::map< std::string, QuantLib::Real >, bool > irCurvatureMargin(const ore::data::NettingSetDetails &nettingSetDetails, const CrifRecord::ProductClass &pc, const SimmSide &side, const ore::analytics::Crif &crif) const
Calculate the Interest Rate curvature margin component for the given portfolio and product class.
std::map< ore::data::NettingSetDetails, bool > collectRegsIsEmpty_
For each netting set, whether all CRIF records' collect regulations are empty.
std::map< SimmSide, std::map< ore::data::NettingSetDetails, std::map< std::string, SimmResults > > > simmResults_
std::map< ore::data::NettingSetDetails, bool > postRegsIsEmpty_
For each netting set, whether all CRIF records' post regulations are empty.
SimmCalculator(const ore::analytics::Crif &crif, const QuantLib::ext::shared_ptr< SimmConfiguration > &simmConfiguration, const std::string &calculationCcyCall="USD", const std::string &calculationCcyPost="USD", const std::string &resultCcy="", const QuantLib::ext::shared_ptr< ore::data::Market > market=nullptr, const bool determineWinningRegulations=true, const bool enforceIMRegulations=false, const bool quiet=false, const std::map< SimmSide, std::set< NettingSetDetails > > &hasSEC=std::map< SimmSide, std::set< NettingSetDetails > >(), const std::map< SimmSide, std::set< NettingSetDetails > > &hasCFTC=std::map< SimmSide, std::set< NettingSetDetails > >())
const std::map< SimmSide, std::map< ore::data::NettingSetDetails, string > > & winningRegulations() const
QuantLib::ext::shared_ptr< SimmConfiguration > simmConfiguration_
The SIMM configuration governing the calculation.
ore::analytics::Crif simmParameters_
Record of SIMM parameters that were used in the calculation.
std::pair< std::map< std::string, QuantLib::Real >, bool > curvatureMargin(const ore::data::NettingSetDetails &nettingSetDetails, const CrifRecord::ProductClass &pc, const CrifRecord::RiskType &rt, const SimmSide &side, const ore::analytics::Crif &netRecords, bool rfLabels=true) const
const std::map< SimmSide, std::map< ore::data::NettingSetDetails, std::pair< std::string, SimmResults > > > & finalSimmResults() const
QuantLib::ext::shared_ptr< ore::data::Market > market_
Market data for FX rates to use for converting amounts to USD.
bool quiet_
If true, no logging is written out.
std::map< SimmSide, std::map< ore::data::NettingSetDetails, std::pair< std::string, SimmResults > > > finalSimmResults_
std::map< SimmSide, std::map< ore::data::NettingSetDetails, std::map< std::string, Crif > > > regSensitivities_
Net sentivities at the regulation level within each netting set.
void add(const ore::data::NettingSetDetails &nettingSetDetails, const string &regulation, const CrifRecord::ProductClass &pc, const SimmConfiguration::RiskClass &rc, const SimmConfiguration::MarginType &mt, const std::string &b, QuantLib::Real margin, SimmSide side, const bool overwrite=true)
ore::analytics::Crif crif_
All the net sensitivities passed in for the calculation.
void populateResults(const SimmSide &side, const ore::data::NettingSetDetails &nsd, const string &regulation)
std::map< SimmSide, set< string > > finalTradeIds_
void splitCrifByRegulationsAndPortfolios(const Crif &crif, const bool enforceIMRegulations)
Add CRIF record to the CRIF records container that correspondsd to the given regulation/s and portfol...
std::map< SimmSide, std::map< ore::data::NettingSetDetails, std::map< std::string, set< string > > > > tradeIds_
Container for keeping track of what trade IDs belong to each regulation.
std::string resultCcy_
The SIMM result currency i.e. the currency in which the main SIMM results are denominated.
const void calculateRegulationSimm(const ore::analytics::Crif &crif, const ore::data::NettingSetDetails &nsd, const string &regulation, const SimmSide &side)
Calculates SIMM for a given regulation under a given netting set.
SimmConfiguration::SimmSide SimmSide
SimmSide
Enum indicating the relevant side of the SIMM calculation.
Class for loading CRIF records.
Struct for holding a CRIF record.
bool parseBool(const string &s)
bool checkCurrency(const string &code)
#define LOG(text)
#define DLOG(text)
RandomVariable max(RandomVariable x, const RandomVariable &y)
RandomVariable sqrt(RandomVariable x)
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
RandomVariable min(RandomVariable x, const RandomVariable &y)
SimmConfiguration::RiskClass RiskClass
SimmConfiguration::Regulation getWinningRegulation(const std::vector< string > &winningRegulations)
From a vector of regulations, determine the winning regulation based on order of priority.
set< string > parseRegulationString(const string &regsString, const set< string > &valueIfEmpty)
Reads a string containing regulations applicable for a given CRIF record.
SimmConfiguration::MarginType MarginType
CrifRecord::RiskType RiskType
Definition: crifloader.cpp:92
CrifRecord::ProductClass ProductClass
SimmVersion
Ordered SIMM versions.
Definition: utilities.hpp:43
SimmVersion parseSimmVersion(const string &version)
Definition: utilities.cpp:162
SimmConfiguration::Regulation Regulation
CrifRecord::ProductClass parseProductClass(const string &pc)
Definition: crifrecord.cpp:127
SimmConfiguration::SimmSide SimmSide
std::string to_string(const LocationInfo &l)
std::size_t count
Class for calculating SIMM.
Base SIMM configuration class.
QuantLib::Real amountResultCcy
Definition: crifrecord.hpp:148
supporting utilities