Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion OREAnalytics/orea/engine/saccrcalculator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@ SaccrTradeData::AssetClass riskTypeToAssetClass(const RiskType& rt) {
}
}

// Basel CRE52.44 credit supervisory factors. Single-name factors are keyed by
// the reference-entity rating; if no rating is available the investment-grade
// default (BBB, the weakest IG rating) is used. Index factors distinguish
// investment grade from speculative grade.
Real creditSupervisoryFactor(const std::string& ratingBucket, bool isIndex) {
static const std::map<std::string, Real> singleName = {
{"AAA", 0.0038}, {"AA", 0.0038}, {"A", 0.0042}, {"BBB", 0.0054},
{"BB", 0.0106}, {"B", 0.0160}, {"CCC", 0.0600},
{"IG", 0.0054}, {"SG", 0.0106}};
if (isIndex) {
// ratingBucket is "Index-IG" / "Index-SG" (or a raw rating as fallback)
static const std::set<std::string> sg = {"Index-SG", "SG", "BB", "B", "CCC"};
return sg.count(ratingBucket) ? 0.0106 : 0.0038;
}
auto it = singleName.find(ratingBucket);
return it != singleName.end() ? it->second : 0.0054; // default: IG (BBB)
}

} // namespace

namespace ore {
Expand All @@ -107,6 +125,7 @@ void SaccrCalculator::clear() {
addOnHedgingSet_.clear();
nettingSets_.clear();
isIndex_.clear();
creditQuality_.clear();
basisHedgingSets_.clear();
volatilityHedgingSets_.clear();
}
Expand Down Expand Up @@ -218,6 +237,10 @@ void SaccrCalculator::processCrifRecord(const CrifRecord& record) {
else if (record.riskType == RiskType::EQ_SN || record.riskType == RiskType::CR_SN)
isIndex_[record.qualifier] = false;

// capture the credit rating sub-asset class (bucket) for the credit SF
if (record.riskType == RiskType::CR_SN || record.riskType == RiskType::CR_IX)
creditQuality_[record.qualifier] = record.bucket;

tradeAssetClasses_[record.tradeId] = assetClass;
assetClasses_[record.nettingSetDetails].insert(assetClass);

Expand Down Expand Up @@ -335,7 +358,31 @@ void SaccrCalculator::aggregate() {
}
addOnHedgingSet_[it->first] = std::sqrt(addonType * addonType + addonTypeSquared);
} else if (assetClass == AssetClass::Credit) {
// TODO
// Basel SA-CCR credit add-on (CRE52.42-52.51). The single credit
// hedging set aggregates reference-entity effective notionals via a
// systematic/idiosyncratic decomposition:
// AddOn = sqrt( (sum_k rho_k SF_k EN_k)^2
// + sum_k (1 - rho_k^2) (SF_k EN_k)^2 )
// with the supervisory factor SF keyed by the reference-entity
// rating (single name) or IG/SG (index), and correlation rho =
// 0.50 (single name) / 0.80 (index).
Real systematic = 0;
Real idiosyncratic = 0;
for (const auto& [hedgingSubsetKey, effectiveNotional] : subsetEffectiveNotional_) {
string hedgingSubset = std::get<3>(hedgingSubsetKey);
HedgingSetKey hedgingSetKey(std::get<0>(hedgingSubsetKey),
std::get<1>(hedgingSubsetKey),
std::get<2>(hedgingSubsetKey));
if (hedgingSetKey != it->first)
continue;
bool isCreditIndex = isIndex_[hedgingSubset];
Real supervisoryFactor = creditSupervisoryFactor(creditQuality_[hedgingSubset], isCreditIndex);
Real corr = isCreditIndex ? 0.8 : 0.5;
Real tmp = supervisoryFactor * effectiveNotional;
systematic += corr * tmp;
idiosyncratic += (1 - corr * corr) * tmp * tmp;
}
addOnHedgingSet_[it->first] = std::sqrt(systematic * systematic + idiosyncratic);
} else
QL_FAIL("asset class " << assetClass << " not covered");

Expand Down
2 changes: 2 additions & 0 deletions OREAnalytics/orea/engine/saccrcalculator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ class SaccrCalculator {
map<HedgingSetKey, Real> effectiveNotional_;
map<HedgingSubsetKey, Real> subsetEffectiveNotional_;
map<string, bool> isIndex_;
// reference-entity credit rating bucket per qualifier (SA-CCR credit SF)
map<string, string> creditQuality_;

vector<NettingSetDetails> nettingSetDetails_;
map<NettingSetDetails, std::set<SaccrTradeData::AssetClass>> assetClasses_;
Expand Down
30 changes: 21 additions & 9 deletions OREAnalytics/orea/engine/saccrtradedata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1654,17 +1654,29 @@ string SaccrTradeData::Impl::getBucket(const Contribution& contribution) const {
bucket = "";

} else if (assetClass == AssetClass::Credit) {
// For single name: SubAsset Class for Credit Single Name, e.g., AAA, AA...IG
// For index: Concatenation of �Index -� and SubAsset Class, e.g., Index - IG, Index - SG
// TODO
/// SNRFOR -> IG, AAA-A
// PREFT1 -> IG, A, BBB
// SECDOM -> SG, BB, B
// SUBL2 -> SG, B-CCC
// Credit sub-asset class drives the supervisory factor (Basel CRE52.44):
// single name -> rating (AAA, AA, A, BBB, BB, B, CCC)
// index -> "Index-IG" or "Index-SG"
// The rating is sourced from the SIMM bucket mapper (RiskType CreditQ)
// if a mapping is configured for the reference-entity qualifier, else we
// fall back to the investment-grade default.
string quality;
auto td = tradeData_.lock();
if (td && td->bucketMapper()) {
try {
if (td->bucketMapper()->hasBuckets(RiskType::CreditQ))
quality = td->bucketMapper()->bucket(RiskType::CreditQ,
contribution.underlyingData.qualifier);
} catch (...) {
quality = "";
}
}
if (contribution.underlyingData.isIndex) {
bucket = "Index-IG"; // "Index-SG"
// investment grade unless the mapped quality is a speculative rating
static const std::set<string> sg = {"BB", "B", "CCC", "SG"};
bucket = sg.count(quality) ? "Index-SG" : "Index-IG";
} else {
bucket = "IG";
bucket = quality.empty() ? "IG" : quality;
}
} else if (assetClass == AssetClass::Commodity) {
// TODO: SubAsset Class for Commodity - same as hedgingSet in many cases, but not always, e.g. HS=Energy,
Expand Down
11 changes: 9 additions & 2 deletions OREAnalytics/orea/simm/simmbucketmapperbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,15 @@ void SimmBucketMapperBase::fromXML(XMLNode* node) {
bucketMapping_[riskType][qualifier].insert(mapping);
TLOG("Added SIMM bucket mapping: {" << riskType << ": {" << qualifier << ", " << bucket << ", " << validFrom << ", " << validTo << ", " << fallback << "}}");
if (bucket != "Residual") {
int bucketInt = ore::data::parseInteger(bucket);
QL_REQUIRE(bucketInt >= 1, "found bucket " << bucket << ", expected >= 1");
// SIMM buckets are positive integers. SA-CCR credit rating
// buckets (e.g. AAA, AA, BBB) are non-numeric labels and are
// accepted as-is; the >= 1 check applies only to numeric buckets.
try {
int bucketInt = ore::data::parseInteger(bucket);
QL_REQUIRE(bucketInt >= 1, "found bucket " << bucket << ", expected >= 1");
} catch (const std::exception&) {
// non-integer bucket label (e.g. a credit rating) - allowed
}
}
}
}
Expand Down