Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
simmconfigurationcalibration.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2023 Quaternion Risk Management Ltd.
3 All rights reserved.
4*/
5
9#include <ql/math/matrix.hpp>
10
11#include <boost/algorithm/string/predicate.hpp>
12#include <boost/make_shared.hpp>
13
14using QuantLib::InterestRateIndex;
15using QuantLib::Matrix;
16using QuantLib::Real;
17using std::string;
18using std::vector;
19
20namespace ore {
21namespace analytics {
22
24
25string SimmConfigurationCalibration::group(const string& qualifier,
26 const std::map<string, std::set<string>>& categories) const {
27 string result;
28 for (const auto& kv : categories) {
29 if (kv.second.empty()) {
30 result = kv.first;
31 } else {
32 if (kv.second.count(qualifier) > 0) {
33 // Found qualifier in category so return it
34 return kv.first;
35 }
36 }
37 }
38
39 // If we get here, result should hold category with empty set
40 return result;
41}
42
43QuantLib::Real SimmConfigurationCalibration::weight(const RiskType& rt, boost::optional<string> qualifier,
44 boost::optional<string> label_1,
45 const string& calculationCurrency) const {
46
47 if (rt == RiskType::FX) {
48 QL_REQUIRE(calculationCurrency != "", "no calculation currency provided weight");
49 QL_REQUIRE(qualifier, "need a qualifier to return a risk weight for the risk type FX");
50
51 const string label1 = group(calculationCurrency, ccyGroups_);
52 const string label2 = group(*qualifier, ccyGroups_);
53 auto label12Key = makeKey("", label1, label2);
54 return rwBucket_.at(RiskType::FX).at(label12Key);
55 }
56
57 return SimmConfigurationBase::weight(rt, qualifier, label_1);
58}
59
60QuantLib::Real SimmConfigurationCalibration::correlation(const RiskType& firstRt, const string& firstQualifier,
61 const string& firstLabel_1, const string& firstLabel_2,
62 const RiskType& secondRt, const string& secondQualifier,
63 const string& secondLabel_1, const string& secondLabel_2,
64 const string& calculationCurrency) const {
65
66 if (firstRt == RiskType::FX && secondRt == RiskType::FX) {
67 QL_REQUIRE(calculationCurrency != "", "no calculation currency provided corr");
68 const string bucket = group(calculationCurrency, ccyGroups_);
69 const string label1 = group(firstQualifier, ccyGroups_);
70 const string label2 = group(secondQualifier, ccyGroups_);
71 auto key = makeKey(bucket, label1, label2);
72
73 if (intraBucketCorrelation_.at(RiskType::FX).find(key) != intraBucketCorrelation_.at(RiskType::FX).end()) {
74 return intraBucketCorrelation_.at(RiskType::FX).at(key);
75 } else {
76 QL_FAIL("Could not find FX intrabucket correlation, calculation currency '"
77 << calculationCurrency << "', firstQualifier '" << firstQualifier << "', secondQualifier '"
78 << secondQualifier << "'.");
79 }
80 }
81
82 return SimmConfigurationBase::correlation(firstRt, firstQualifier, firstLabel_1, firstLabel_2, secondRt,
83 secondQualifier, secondLabel_1, secondLabel_2);
84}
85
86SimmConfigurationCalibration::SimmConfigurationCalibration(const QuantLib::ext::shared_ptr<SimmBucketMapper>& simmBucketMapper,
87 const QuantLib::ext::shared_ptr<SimmCalibration>& simmCalibration,
88 const QuantLib::Size& mporDays, const string& name)
89 : SimmConfigurationBase(simmBucketMapper, name, "", mporDays) {
90
91 version_ = simmCalibration->version();
92
93 // The differences in methodology for 1 Day horizon is described in
94 // Standard Initial Margin Model: Technical Paper, ISDA SIMM Governance Forum, Version 10:
95 // Section I - Calibration with one-day horizon
96 QL_REQUIRE(mporDays_ == 10 || mporDays_ == 1, "SIMM only supports MPOR 10-day or 1-day");
97
98 // Set up the correct concentration threshold getter
99 if (mporDays == 10) {
100 simmConcentration_ = QuantLib::ext::make_shared<SimmConcentrationCalibration>(simmCalibration, simmBucketMapper_);
101 } else {
102 // SIMM:Technical Paper, Section I.4: "The Concentration Risk feature is disabled"
103 simmConcentration_ = QuantLib::ext::make_shared<SimmConcentrationBase>();
104 }
105
106 for (const auto& [riskClass, rcData] : simmCalibration->riskClassData()) {
107
108 // Risk weights
109 const auto& riskWeights = rcData->riskWeights();
110
111 // Currency lists for FX
112 // Populate CCY groups that are used for FX correlations and risk weights
113 // The groups consists of High Vol Currencies & regular vol currencies
114 if (riskClass == RiskClass::FX) {
115 const auto& fxRiskWeights = QuantLib::ext::dynamic_pointer_cast<SimmCalibration::RiskClassData::FXRiskWeights>(riskWeights);
116 QL_REQUIRE(fxRiskWeights, "Cannot cast RiskWeights to FXRiskWeights");
117 const auto& ccyLists = fxRiskWeights->currencyLists();
118
119 for (const auto& [ccyKey, ccyList] : ccyLists) {
120 const string& bucket = std::get<0>(ccyKey);
121 for (const string& ccy : ccyList) {
122 if (ccy == "Other") {
123 ccyGroups_[bucket].clear();
124 } else {
125 ccyGroups_[bucket].insert(ccy);
126 }
127 }
128 }
129 }
130
131 const auto& [deltaRiskType, vegaRiskType] = SimmConfiguration::riskClassToRiskType(riskClass);
132
133 // Risk weights unique to each risk class
134 auto singleRiskWeights = riskWeights->uniqueRiskWeights();
135 for (const auto& [riskType, rw] : singleRiskWeights) {
136 rwRiskType_[riskType] = ore::data::parseReal(rw.at(mporDays_)->value());
137 }
138
139 // Delta risk weights
140 const auto& deltaRiskWeights = riskWeights->delta().at(mporDays_);
141 const auto& vegaRiskWeights = riskWeights->vega().at(mporDays_);
142
143 if (deltaRiskWeights.size() == 1) {
144 rwRiskType_[deltaRiskType] = ore::data::parseReal(deltaRiskWeights.begin()->second);
145 } else {
146 // IR risk weights
147 if (riskClass == RiskClass::InterestRate) {
148 for (const auto& [rwKey, weight] : deltaRiskWeights) {
149 rwLabel_1_[deltaRiskType][rwKey] = ore::data::parseReal(weight);
150 }
151 } else {
152 for (const auto& [rwKey, weight] : deltaRiskWeights) {
153 rwBucket_[deltaRiskType][rwKey] = ore::data::parseReal(weight);
154 }
155 }
156 }
157
158 // Vega risk weights
159 if (vegaRiskWeights.size() == 1) {
160 rwRiskType_[vegaRiskType] = ore::data::parseReal(vegaRiskWeights.begin()->second);
161 if (vegaRiskType == RiskType::IRVol)
162 rwRiskType_[RiskType::InflationVol] = rwRiskType_[RiskType::IRVol];
163 } else {
164 for (const auto& [rwKey, weight] : vegaRiskWeights) {
165 rwBucket_[vegaRiskType][rwKey] = ore::data::parseReal(weight);
166 }
167 }
168
169 // Historical volatility ratio
170 // IR
171 if (riskClass == RiskClass::InterestRate) {
172 hvr_ir_ = ore::data::parseReal(riskWeights->historicalVolatilityRatio().at(mporDays)->value());
173 }
174 // EQ, COMM, FX
175 if (riskClass == RiskClass::Equity || riskClass == RiskClass::Commodity || riskClass == RiskClass::FX)
176 historicalVolatilityRatios_[vegaRiskType] =
177 ore::data::parseReal(riskWeights->historicalVolatilityRatio().at(mporDays_)->value());
178
179 // Correlations
180 const auto& correlations = rcData->correlations();
181
182 // Interbucket and Intrabucket correlations
183 for (const auto& [corrKey, corr] : rcData->correlations()->intraBucketCorrelations()) {
184 if (riskClass == RiskClass::CreditQualifying || riskClass == RiskClass::CreditNonQualifying) {
185 const string& label1 = std::get<1>(corrKey);
186 const string& label2 = std::get<2>(corrKey);
188
189 if (riskClass == RiskClass::CreditQualifying) {
190 if (label1 == "aggregate") {
191 if (label2 == "same") {
193 } else {
195 }
196 } else {
198 }
199 } else {
200 if (label1 == "aggregate") {
201 if (label2 == "same") {
203 } else {
205 }
206 } else {
208 }
209 }
210 } else {
211 intraBucketCorrelation_[deltaRiskType][corrKey] = ore::data::parseReal(corr);
212 }
213 }
214 for (const auto& [corrKey, corr] : rcData->correlations()->interBucketCorrelations()) {
215 interBucketCorrelation_[deltaRiskType][corrKey] = ore::data::parseReal(corr);
216 }
217
218 // Correlations unique to other risk class
219 if (riskClass == RiskClass::InterestRate) {
220 const auto& irCorrelations = QuantLib::ext::dynamic_pointer_cast<SimmCalibration::RiskClassData::IRCorrelations>(correlations);
221 irSubCurveCorr_ = ore::data::parseReal(irCorrelations->subCurves()->value());
222 infCorr_ = ore::data::parseReal(irCorrelations->inflation()->value());
223 infVolCorr_ = ore::data::parseReal(irCorrelations->inflation()->value());
224 xccyCorr_ = ore::data::parseReal(irCorrelations->xCcyBasis()->value());
225 irInterCurrencyCorr_ = ore::data::parseReal(irCorrelations->outer()->value());
226 } else if (riskClass == RiskClass::FX) {
227 const auto& fxCorrelations =
228 QuantLib::ext::dynamic_pointer_cast<SimmCalibration::RiskClassData::FXCorrelations>(correlations);
229 fxCorr_ = ore::data::parseReal(fxCorrelations->volatility()->value());
230 } else if (riskClass == RiskClass::CreditQualifying) {
231 const auto& creditQCorrelations =
232 QuantLib::ext::dynamic_pointer_cast<SimmCalibration::RiskClassData::CreditQCorrelations>(correlations);
233 basecorrCorr_ = ore::data::parseReal(creditQCorrelations->baseCorrelation()->value());
234 } else if (riskClass == RiskClass::CreditNonQualifying) {
235 crnqInterCorr_ = ore::data::parseReal(correlations->interBucketCorrelations().begin()->second);
236 }
237 }
238
239 mapBuckets_ = {{RiskType::IRCurve, {"1", "2", "3"}},
240 {RiskType::CreditQ, {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "Residual"}},
241 {RiskType::CreditVol, {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "Residual"}},
242 {RiskType::CreditNonQ, {"1", "2", "Residual"}},
243 {RiskType::CreditVolNonQ, {"1", "2", "Residual"}},
244 {RiskType::Equity, {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "Residual"}},
245 {RiskType::EquityVol, {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "Residual"}},
246 {RiskType::Commodity,
247 {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17"}},
248 {RiskType::CommodityVol,
249 {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17"}}};
250
251 mapLabels_1_ = {
252 {RiskType::IRCurve, {"2w", "1m", "3m", "6m", "1y", "2y", "3y", "5y", "10y", "15y", "20y", "30y"}},
253 {RiskType::CreditQ, {"1y", "2y", "3y", "5y", "10y"}},
254 {RiskType::CreditNonQ, {"1y", "2y", "3y", "5y", "10y"}},
255 {RiskType::IRVol, {"2w", "1m", "3m", "6m", "1y", "2y", "3y", "5y", "10y", "15y", "20y", "30y"}},
256 {RiskType::InflationVol, {"2w", "1m", "3m", "6m", "1y", "2y", "3y", "5y", "10y", "15y", "20y", "30y"}},
257 {RiskType::CreditVol, {"1y", "2y", "3y", "5y", "10y"}},
258 {RiskType::CreditVolNonQ, {"1y", "2y", "3y", "5y", "10y"}},
259 {RiskType::EquityVol, {"2w", "1m", "3m", "6m", "1y", "2y", "3y", "5y", "10y", "15y", "20y", "30y"}},
260 {RiskType::CommodityVol, {"2w", "1m", "3m", "6m", "1y", "2y", "3y", "5y", "10y", "15y", "20y", "30y"}},
261 {RiskType::FXVol, {"2w", "1m", "3m", "6m", "1y", "2y", "3y", "5y", "10y", "15y", "20y", "30y"}}};
262
263 mapLabels_2_ = {{RiskType::IRCurve, {"OIS", "Libor1m", "Libor3m", "Libor6m", "Libor12m", "Prime", "Municipal"}},
264 {RiskType::CreditQ, {"", "Sec"}}};
265
266 // Valid risk types
267 validRiskTypes_ = {RiskType::Commodity,
268 RiskType::CommodityVol,
269 RiskType::CreditNonQ,
270 RiskType::CreditQ,
271 RiskType::CreditVol,
272 RiskType::CreditVolNonQ,
273 RiskType::Equity,
274 RiskType::EquityVol,
275 RiskType::FX,
276 RiskType::FXVol,
277 RiskType::Inflation,
278 RiskType::IRCurve,
279 RiskType::IRVol,
280 RiskType::InflationVol,
281 RiskType::BaseCorr,
282 RiskType::XCcyBasis,
283 RiskType::ProductClassMultiplier,
284 RiskType::AddOnNotionalFactor,
285 RiskType::PV,
286 RiskType::Notional,
287 RiskType::AddOnFixedAmount};
288
289 // Curvature weights
290 // Hardcoded weights - doesn't seem to change much across versions
291 // clang-format off
292 if (mporDays_ == 10) {
294 { RiskType::IRVol, { 0.5,
295 0.5 * 14.0 / (365.0 / 12.0),
296 0.5 * 14.0 / (3.0 * 365.0 / 12.0),
297 0.5 * 14.0 / (6.0 * 365.0 / 12.0),
298 0.5 * 14.0 / 365.0,
299 0.5 * 14.0 / (2.0 * 365.0),
300 0.5 * 14.0 / (3.0 * 365.0),
301 0.5 * 14.0 / (5.0 * 365.0),
302 0.5 * 14.0 / (10.0 * 365.0),
303 0.5 * 14.0 / (15.0 * 365.0),
304 0.5 * 14.0 / (20.0 * 365.0),
305 0.5 * 14.0 / (30.0 * 365.0) }
306 },
307 { RiskType::CreditVol, { 0.5 * 14.0 / 365.0,
308 0.5 * 14.0 / (2.0 * 365.0),
309 0.5 * 14.0 / (3.0 * 365.0),
310 0.5 * 14.0 / (5.0 * 365.0),
311 0.5 * 14.0 / (10.0 * 365.0) }
312 }
313 };
314 } else {
315 //SIMM:Technical Paper, Section I.3, this 10-day formula for curvature weights is modified
317 { RiskType::IRVol, { 0.5 / 10.0,
318 0.5 * 1.40 / (365.0 / 12.0),
319 0.5 * 1.40 / (3.0 * 365.0 / 12.0),
320 0.5 * 1.40 / (6.0 * 365.0 / 12.0),
321 0.5 * 1.40 / 365.0,
322 0.5 * 1.40 / (2.0 * 365.0),
323 0.5 * 1.40 / (3.0 * 365.0),
324 0.5 * 1.40 / (5.0 * 365.0),
325 0.5 * 1.40 / (10.0 * 365.0),
326 0.5 * 1.40 / (15.0 * 365.0),
327 0.5 * 1.40 / (20.0 * 365.0),
328 0.5 * 1.40 / (30.0 * 365.0) }
329 },
330 { RiskType::CreditVol, { 0.5 * 1.40 / 365.0,
331 0.5 * 1.40 / (2.0 * 365.0),
332 0.5 * 1.40 / (3.0 * 365.0),
333 0.5 * 1.40 / (5.0 * 365.0),
334 0.5 * 1.40 / (10.0 * 365.0) }
335 }
336 };
337 }
338 // clang-format on
339 curvatureWeights_[RiskType::InflationVol] = curvatureWeights_[RiskType::IRVol];
340 curvatureWeights_[RiskType::EquityVol] = curvatureWeights_[RiskType::IRVol];
341 curvatureWeights_[RiskType::CommodityVol] = curvatureWeights_[RiskType::IRVol];
342 curvatureWeights_[RiskType::FXVol] = curvatureWeights_[RiskType::IRVol];
343 curvatureWeights_[RiskType::CreditVolNonQ] = curvatureWeights_[RiskType::CreditVol];
344
345 // Risk class correlation matrix
346 const auto& rcCorrelations = simmCalibration->riskClassCorrelations();
347 for (const auto& [rcCorrKey, rcCorr] : rcCorrelations) {
348 riskClassCorrelation_[rcCorrKey] = ore::data::parseReal(rcCorr);
349 }
350}
351
352/* The CurvatureMargin must be multiplied by a scale factor of HVR(IR)^{-2}, where HVR(IR)
353is the historical volatility ratio for the interest-rate risk class (see page 8 section 11(d)
354of the ISDA-SIMM-v2.6 documentation).
355*/
356QuantLib::Real SimmConfigurationCalibration::curvatureMarginScaling() const { return pow(hvr_ir_, -2.0); }
357
358void SimmConfigurationCalibration::addLabels2(const RiskType& rt, const string& label_2) {
359 // Call the shared implementation
361}
362
363string SimmConfigurationCalibration::label2(const QuantLib::ext::shared_ptr<InterestRateIndex>& irIndex) const {
364 // Special for BMA
365 if (boost::algorithm::starts_with(irIndex->name(), "BMA")) {
366 return "Municipal";
367 }
368
369 // Otherwise pass off to base class
370 return SimmConfigurationBase::label2(irIndex);
371}
372
373} // namespace analytics
374} // namespace ore
virtual std::string label2(const QuantLib::ext::shared_ptr< QuantLib::InterestRateIndex > &irIndex) const
QuantLib::Real correlation(const CrifRecord::RiskType &firstRt, const std::string &firstQualifier, const std::string &firstLabel_1, const std::string &firstLabel_2, const CrifRecord::RiskType &secondRt, const std::string &secondQualifier, const std::string &secondLabel_1, const std::string &secondLabel_2, const std::string &calculationCurrency="") const override
const std::tuple< std::string, std::string, std::string > makeKey(const std::string &, const std::string &, const std::string &) const
QuantLib::Real crqResidualIntraCorr_
Credit-Q residual intra correlation.
QuantLib::Real basecorrCorr_
Base correlation risk factor correlation.
std::map< CrifRecord::RiskType, QuantLib::Real > rwRiskType_
QuantLib::Real weight(const CrifRecord::RiskType &rt, boost::optional< std::string > qualifier=boost::none, boost::optional< std::string > label_1=boost::none, const std::string &calculationCurrency="") const override
QuantLib::Real crnqResidualIntraCorr_
Credit-NonQ residual intra correlation.
std::string version_
SIMM configuration version.
QuantLib::Size mporDays() const
MPOR in days.
std::map< CrifRecord::RiskType, std::vector< std::string > > mapLabels_2_
QuantLib::Real irInterCurrencyCorr_
IR correlation across currencies.
std::map< CrifRecord::RiskType, Amounts > rwLabel_1_
std::map< CrifRecord::RiskType, Amounts > intraBucketCorrelation_
std::map< CrifRecord::RiskType, std::vector< std::string > > mapBuckets_
QuantLib::Real crnqDiffIntraCorr_
Credit-NonQ non-residual intra correlation when different underlying names.
QuantLib::Real crqSameIntraCorr_
Credit-Q non-residual intra correlation when same qualifier but different vertex/source.
QuantLib::Real crnqSameIntraCorr_
Credit-NonQ non-residual intra correlation when same underlying names.
std::set< CrifRecord::RiskType > validRiskTypes_
Set of valid risk types for the current configuration.
QuantLib::ext::shared_ptr< SimmConcentration > simmConcentration_
Used to get the concentration thresholds for a given risk type and qualifier.
QuantLib::Real infCorr_
Correlation between any yield and inflation in same currency.
Amounts riskClassCorrelation_
Risk class correlation matrix.
QuantLib::Real crnqInterCorr_
Credit-NonQ non-residual inter bucket correlation.
std::map< CrifRecord::RiskType, QuantLib::Real > historicalVolatilityRatios_
Map from risk type to a historical volatility ratio.
std::map< CrifRecord::RiskType, Amounts > interBucketCorrelation_
std::map< CrifRecord::RiskType, std::vector< std::string > > mapLabels_1_
QuantLib::Real infVolCorr_
Correlation between any yield volatility and inflation volatility in same currency.
QuantLib::Real crqDiffIntraCorr_
Credit-Q non-residual intra correlation when different qualifier.
QuantLib::Real irSubCurveCorr_
IR Label2 level i.e. sub-curve correlation.
void addLabels2Impl(const CrifRecord::RiskType &rt, const std::string &label_2)
A base implementation of addLabels2 that can be shared by derived classes.
std::map< CrifRecord::RiskType, std::vector< QuantLib::Real > > curvatureWeights_
QuantLib::ext::shared_ptr< SimmBucketMapper > simmBucketMapper_
Used to map SIMM Qualifier names to SIMM bucket values.
std::string bucket(const CrifRecord::RiskType &rt, const std::string &qualifier) const override
std::map< CrifRecord::RiskType, Amounts > rwBucket_
QuantLib::Real correlation(const CrifRecord::RiskType &firstRt, const std::string &firstQualifier, const std::string &firstLabel_1, const std::string &firstLabel_2, const CrifRecord::RiskType &secondRt, const std::string &secondQualifier, const std::string &secondLabel_1, const std::string &secondLabel_2, const std::string &calculationCurrency="") const override
SimmConfigurationCalibration(const QuantLib::ext::shared_ptr< SimmBucketMapper > &simmBucketMapper, const QuantLib::ext::shared_ptr< SimmCalibration > &simmCalibration, const QuantLib::Size &mporDays=10, const std::string &name="SIMM Calibration")
QuantLib::Real weight(const CrifRecord::RiskType &rt, boost::optional< std::string > qualifier=boost::none, boost::optional< std::string > label_1=boost::none, const std::string &calculationCurrency="") const override
std::map< std::string, std::set< std::string > > ccyGroups_
std::string label2(const QuantLib::ext::shared_ptr< QuantLib::InterestRateIndex > &irIndex) const override
Return the SIMM Label2 value for the given interest rate index.
void addLabels2(const CrifRecord::RiskType &rt, const std::string &label_2) override
Add SIMM Label2 values under certain circumstances.
std::string group(const std::string &qualifier, const std::map< std::string, std::set< std::string > > &groups) const
Find the group of the qualifier.
QuantLib::Real hvr_ir_
IR Historical volatility ratio.
static std::pair< CrifRecord::RiskType, CrifRecord::RiskType > riskClassToRiskType(const RiskClass &rc)
For a given risk class, return the corresponding risk types.
Real parseReal(const string &s)
RandomVariable pow(RandomVariable x, const RandomVariable &y)
CrifRecord::RiskType RiskType
Definition: crifloader.cpp:92
SIMM concentration thresholds built from SIMM calibration.
SIMM configuration built for SIMM calibration.
string name