Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
crifloader.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
26
27#include <algorithm>
28#include <boost/algorithm/string.hpp>
29#include <boost/range/adaptor/map.hpp>
30#include <boost/range/adaptor/indexed.hpp>
31#include <boost/range/algorithm/max_element.hpp>
35
42using QuantLib::Size;
43using QuantLib::Real;
44using std::exception;
45using std::getline;
46using std::ifstream;
47using std::map;
48using std::max_element;
49using std::pair;
50using std::set;
51using std::string;
52using std::vector;
53
54namespace ore {
55namespace analytics {
56
57// Required headers
58map<Size, set<string>> CrifLoader::requiredHeaders = {
59 {0, {"tradeid", "trade_id"}},
60 {1, {"portfolioid", "portfolio_id"}},
61 {2, {"productclass", "product_class", "asset_class"}},
62 {3, {"risktype", "risk_type"}},
63 {4, {"qualifier"}},
64 {5, {"bucket"}},
65 {6, {"label1"}},
66 {7, {"label2"}},
67 {8, {"amountcurrency", "currency", "amount_currency"}},
68 {9, {"amount"}},
69 {10, {"amountusd", "amount_usd"}}};
70
71// Optional headers
72map<Size, set<string>> CrifLoader::optionalHeaders = {
73
74 {11, {"agreementtype", "agreement_type"}},
75 {12, {"calltype", "call_type"}},
76 {13, {"initialmargintype", "initial_margin_type"}},
77 {14, {"legalentityid", "legal_entity_id"}},
78 {15, {"tradetype", "trade_type"}},
79 {16, {"immodel", "im_model"}},
80 {17, {"post_regulations"}},
81 {18, {"collect_regulations"}},
82 {19, {"end_date"}},
83 {20, {"label_3"}},
84 {21, {"creditquality"}},
85 {22, {"longshortind"}},
86 {23, {"coveredbonind"}},
87 {24, {"tranchethickness"}},
88 {25, {"bb_rw"}}};
89
90
91// Ease syntax
94
95void CrifLoader::addRecordToCrif(Crif& crif, CrifRecord&& recordToAdd) const {
96 bool add = recordToAdd.type() != CrifRecord::RecordType::Generic;
97 if (recordToAdd.type() == CrifRecord::RecordType::SIMM) {
98 validateSimmRecord(recordToAdd);
99 currencyOverrides(recordToAdd);
100 add = configuration_->isValidRiskType(recordToAdd.riskType);
101 }
102 if (aggregateTrades_) {
103 recordToAdd.tradeId = "";
104 }
105 if (add) {
106 crif.addRecord(recordToAdd);
107 } else {
108 QL_FAIL("Risk type string " << recordToAdd.riskType << " does not correspond to a valid SimmConfiguration::RiskType");
109 }
110}
111
113 switch (cr.riskType) {
114 case RiskType::AddOnFixedAmount:
115 case RiskType::AddOnNotionalFactor:
116 QL_REQUIRE(cr.productClass == ProductClass::Empty,
117 "Expected product class " << ProductClass::Empty << " for risk type " << cr.riskType);
118 break;
119 case RiskType::ProductClassMultiplier: {
120
121 QL_REQUIRE(cr.productClass == ProductClass::Empty,
122 "Expected product class " << ProductClass::Empty << " for risk type " << cr.riskType);
123 // Check that the qualifier is a valid Product class
124 auto pc = parseProductClass(cr.qualifier);
125 QL_REQUIRE(pc != ProductClass::Empty,
126 "The qualifier " << cr.qualifier << " should parse to a valid product class for risk type "
127 << cr.riskType);
128 // Check that the amount is a number >= 1.0
129 QL_REQUIRE(cr.amount >= 0.0, "Expected an amount greater than or equal to 0 "
130 << "for risk type " << cr.riskType << " and qualifier " << cr.qualifier
131 << " but got " << cr.amount);
132 break;
133 }
134 case RiskType::Notional:
135 case RiskType::PV:
136 if (cr.imModel == "Schedule")
137 QL_REQUIRE(!cr.endDate.empty(),
138 "Expected end date for risk type " << cr.riskType << " and im_model=\'Schedule\'");
139 break;
140 default:
141 break;
142 }
143}
144
146 switch (cr.riskType) {
147 case RiskType::IRCurve:
148 case RiskType::IRVol:
149 case RiskType::Inflation:
150 case RiskType::InflationVol:
151 case RiskType::XCcyBasis:
152 case RiskType::FX:
153 // TODO: Do we really need to switch CNH to CNY here?
154 // How many more are like this?
155 if (cr.qualifier == "CNH")
156 cr.qualifier = "CNY";
157 QL_REQUIRE(checkCurrency(cr.qualifier),
158 "currency code '" << cr.qualifier << "' is not a supported currency code");
159 break;
160 case RiskType::FXVol: {
161
162 // Normalise the qualifier i.e. XXXYYY and YYYXXX are the same
163 QL_REQUIRE(cr.qualifier.size() == 6,
164 "Expected a string of length 6 for FXVol qualifier but got " << cr.qualifier);
165 auto ccy_1 = cr.qualifier.substr(0, 3);
166 auto ccy_2 = cr.qualifier.substr(3);
167 if (ccy_1 == "CNH")
168 ccy_1 = "CNY";
169 if (ccy_2 == "CNH")
170 ccy_2 = "CNY";
171 QL_REQUIRE(checkCurrency(ccy_1), "currency code 1 in pair '" << cr.qualifier << "' (" << ccy_1
172 << ") is not a supported currency code");
173 QL_REQUIRE(checkCurrency(ccy_2), "currency code 2 in pair '" << cr.qualifier << "' (" << ccy_2
174 << ") is not a supported currency code");
175 if (ccy_1 > ccy_2)
176 ccy_1.swap(ccy_2);
177 cr.qualifier = ccy_1 + ccy_2;
178
179 break;
180 }
181 default:
182 break;
183 }
184}
185
186void CrifLoader::updateMapping(const CrifRecord& cr) const {
187 // Update the SIMM configuration's bucket mapper if the
188 // loader has set this flag
189 if (updateMapper_ && !cr.isSimmParameter()) {
190 const auto& bm = configuration_->bucketMapper();
191 if (bm->hasBuckets(cr.riskType)) {
192 bm->addMapping(cr.riskType, cr.qualifier, cr.bucket);
193 }
194 }
195}
196
197StringStreamCrifLoader::StringStreamCrifLoader(const QuantLib::ext::shared_ptr<SimmConfiguration>& configuration,
198 const std::vector<std::set<std::string>>& additionalHeaders, bool updateMapper,
199 bool aggregateTrades, char eol, char delim, char quoteChar, char escapeChar, const std::string& nullString)
200 : CrifLoader(configuration, additionalHeaders, updateMapper, aggregateTrades), eol_(eol), delim_(delim),
201 quoteChar_(quoteChar), escapeChar_(escapeChar), nullString_(nullString) {
202
203 size_t maxIndexRequired = *boost::max_element(requiredHeaders | boost::adaptors::map_keys);
204 size_t maxIndexOptional = *boost::max_element(optionalHeaders | boost::adaptors::map_keys);
205 size_t maxIndex = std::max(maxIndexRequired, maxIndexOptional);
206
207 size_t i = 1;
208 for (const auto& addHeader : additionalHeaders_) {
209 additionalHeadersIndexMap_[maxIndex + i] = addHeader;
210 i++;
211 }
212
213}
214
215
216std::stringstream CsvFileCrifLoader::stream() const {
217 // Try to open the file
218 ifstream file;
219 std::stringstream result;
220 file.open(filename_);
221 QL_REQUIRE(file.is_open(), "error opening file " << filename_);
222 result << file.rdbuf();
223 file.close();
224 return result;
225}
226
227std::stringstream CsvBufferCrifLoader::stream() const {
228 std::stringstream csvStream;
229 csvStream << buffer_;
230 return csvStream;
231}
232
233Crif StringStreamCrifLoader::loadFromStream(std::stringstream&& stream) {
234 string line;
235 vector<string> entries;
236 bool headerProcessed = false;
237 Size emptyLines = 0;
238 Size validLines = 0;
239 Size invalidLines = 0;
240 Size maxIndex = 0;
241 Size currentLine = 0;
242 Crif result;
243 while (getline(stream, line, eol_)) {
244
245 // Keep track of current line number for messages
246 ++currentLine;
247
248 // Trim leading and trailing space
249 boost::trim(line);
250
251 // Skip empty lines
252 if (line.empty()) {
253 ++emptyLines;
254 continue;
255 }
256
257 // Break line up in to its elements.
259
260 if (headerProcessed) {
261 // Process a regular line of the CRIF file
262 if (process(entries, maxIndex, currentLine, result)) {
263 ++validLines;
264 } else {
265 ++invalidLines;
266 }
267 } else {
268 // Process the header line of the CRIF file
269 processHeader(entries);
270 headerProcessed = true;
271 auto maxPair = max_element(
272 columnIndex_.begin(), columnIndex_.end(),
273 [](const pair<Size, Size>& p1, const pair<Size, Size>& p2) { return p1.second < p2.second; });
274 maxIndex = maxPair->second;
275 }
276 }
277
278 LOG("Out of " << currentLine << " lines, there were " << validLines << " valid lines, " << invalidLines
279 << " invalid lines and " << emptyLines << " empty lines.");
280 return result;
281}
282
283
284void StringStreamCrifLoader::processHeader(const vector<string>& headers) {
285 columnIndex_.clear();
286
287 // Get mapping for all required headers in to column index in the file
288 string header;
289 for (const auto& kv : requiredHeaders) {
290 for (Size i = 0; i < headers.size(); ++i) {
291 header = boost::to_lower_copy(headers[i]);
292 if (kv.second.count(header) > 0) {
293 columnIndex_[kv.first] = i;
294 }
295 }
296 // Some headers are allowed to be missing (under certain circumstances)
297 // trade_id, portfolioid and productclass arent required for frtb crif
298 if (kv.first == 0 || kv.first == 1 || kv.first == 2) {
299 // Portfolio ID header is allowed to be missing
300 if (columnIndex_.count(kv.first) == 0) {
301 WLOG("Did not find a header for portfolioid in the CRIF file so using a default value");
302 }
303 } else if (kv.first == 10) {
304 // Allow either amount_usd missing or amount and amount_currency, but not all three.
305 // For SIMM, we ultimately use amount_usd, but if missing, we use amount and amount_currency
306 // and let the SimmCalculator handle the conversion to amount_usd
307 if (columnIndex_.count(10) == 0)
308 QL_REQUIRE(columnIndex_.count(8) > 0 && columnIndex_.count(9) > 0,
309 "Must provide either amount and amount_currency, or amount_usd");
310 } else {
311 // All other headers should be there
312 QL_REQUIRE(columnIndex_.count(kv.first) > 0,
313 "Could not find a header in the CRIF file for " << *kv.second.begin());
314 }
315 }
316
317 for (const auto& kv : optionalHeaders) {
318 for (Size i = 0; i < headers.size(); ++i) {
319 header = boost::to_lower_copy(headers[i]);
320 if (kv.second.count(header) > 0) {
321 columnIndex_[kv.first] = i;
322 }
323 }
324 }
325
326 for (const auto& kv: additionalHeadersIndexMap_) {
327 for (Size columnPos = 0; columnPos < headers.size(); ++columnPos) {
328 header = boost::to_lower_copy(headers[columnPos]);
329 if (kv.second.count(header) > 0) {
330 columnIndex_[kv.first] = columnPos;
331 }
332 }
333 }
334}
335
336bool StringStreamCrifLoader::process(const vector<string>& entries, Size maxIndex, Size currentLine, Crif& result) {
337 CrifRecord cr;
338 // Return early if there are not enough entries in the line
339 if (entries.size() <= maxIndex) {
340 WLOG("Line number: " << currentLine << ". Expected at least " << maxIndex + 1 << " entries but got only "
341 << entries.size());
342 return false;
343 }
344
345 // Try to create and add a CRIF record
346 // There could still be issues here so we surround with try..catch to allow processing to continue
347 auto loadOptionalString = [&entries, this](int column) {
348 return columnIndex_.count(column) == 0 ? "" : entries[columnIndex_[column]];
349 };
350 auto loadOptionalReal = [&entries, this](int column) -> QuantLib::Real{
351 if (columnIndex_.count(column) == 0) {
352 return QuantLib::Null<QuantLib::Real>();
353 } else{
354 const std::string& value = entries[columnIndex_[column]];
355
356 return value.empty() || value == nullString_ ? QuantLib::Null<QuantLib::Real>()
357 : parseReal(value);
358 }
359 };
360
361 string tradeId, tradeType, imModel;
362 try {
363 tradeId = loadOptionalString(0);
364 tradeType = loadOptionalString(15);
365 imModel = loadOptionalString(16);
366
367 cr.tradeId = tradeId;
368 cr.tradeType = tradeType;
369 cr.imModel = imModel;
370 cr.portfolioId = columnIndex_.count(1) == 0 ? "DummyPortfolio" : entries[columnIndex_.at(1)];
371 cr.productClass = parseProductClass(loadOptionalString(2));
372 cr.riskType = parseRiskType(entries[columnIndex_.at(3)]);
373
374 // Qualifier - There are many other possible qualifier values, but we only do case-insensitive checks
375 // for those with standardised values, i.e. currencies or ccy pairs
376 cr.qualifier = entries[columnIndex_.at(4)];
377 if ((cr.riskType == RiskType::IRCurve || cr.riskType == RiskType::IRVol || cr.riskType == RiskType::FX) &&
378 cr.qualifier.size() == 3) {
379 string ccyUpper = boost::to_upper_copy(cr.qualifier);
380
381 // If ccy is already valid, do nothing. Otherwise, replace with all uppercase equivalent.
382 // FIXME: Minor currencies will fail to get spotted here, though it is not likely that we will have
383 // a qualifier in a minor ccy?
384 if (!checkCurrency(cr.qualifier) && checkCurrency(ccyUpper))
385 cr.qualifier = ccyUpper;
386 } else if (cr.riskType == RiskType::FXVol && (cr.qualifier.size() == 6 || cr.qualifier.size() == 7)) {
387
388 // Remove delimiters between the two currencies
389 const string ccyPairDelimiters = "/.,-_|;: ";
390 auto ccyPair = ore::data::parseCurrencyPair(boost::to_upper_copy(cr.qualifier), ccyPairDelimiters);
391
392 // Convert to uppercase
393 string ccy1Upper = ccyPair.first.code();
394 string ccy2Upper = ccyPair.second.code();
395
396 cr.qualifier = ccy1Upper + ccy2Upper;
397 }
398
399 // Bucket - Hardcoded "Residual" for case-insensitive check since this is currently the only non-numeric value
400 cr.bucket = entries[columnIndex_.at(5)];
401 if (boost::to_lower_copy(cr.bucket) == "residual")
402 cr.bucket = "Residual";
403
404 // Label1
405 cr.label1 = entries[columnIndex_.at(6)];
406 if (configuration_->isValidRiskType(cr.riskType)) {
407 for (const string& l : configuration_->labels1(cr.riskType)) {
408 if (boost::to_lower_copy(cr.label1) == boost::to_lower_copy(l))
409 cr.label1 = l;
410 }
411 }
412 // Label2
413 cr.label2 = entries[columnIndex_.at(7)];
414 if (configuration_->isValidRiskType(cr.riskType)) {
415 for (const string& l : configuration_->labels2(cr.riskType)) {
416 if (boost::to_lower_copy(cr.label2) == boost::to_lower_copy(l))
417 cr.label2 = l;
418 }
419 }
420
421 // We populate these 'required' values using loadOptional*, but they will have been validated already in processHeader,
422 // and missing amountUsd (but with valid amount and amountCurrency) values populated later on in the analytics
423
424 cr.amountCurrency = loadOptionalString(8);
425 string amountCcyUpper = boost::to_upper_copy(cr.amountCurrency);
426 if (!amountCcyUpper.empty() && !checkCurrency(cr.amountCurrency) && checkCurrency(amountCcyUpper))
427 cr.amountCurrency = amountCcyUpper;
428
429 cr.amount = loadOptionalReal(9);
430 cr.amountUsd = loadOptionalReal(10);
431
432 // Populate netting set details
433 cr.agreementType = loadOptionalString(11);
434 cr.callType = loadOptionalString(12);
435 cr.initialMarginType = loadOptionalString(13);
436 cr.legalEntityId = loadOptionalString(14);
438 cr.legalEntityId);
439 cr.postRegulations = loadOptionalString(17);
440 cr.collectRegulations = loadOptionalString(18);
441 cr.endDate = loadOptionalString(19);
442 cr.label3 = loadOptionalString(20);
443 cr.creditQuality = loadOptionalString(21);
444 cr.longShortInd = loadOptionalString(22);
445 cr.coveredBondInd = loadOptionalString(23);
446 cr.trancheThickness = loadOptionalString(24);
447 cr.bb_rw = loadOptionalString(25);
448
449 // Check the IM model
450 try {
452 } catch (...) {
453 // If we cannot convert to a valid im_model, then it was either provided blank
454 // or is simply not a valid value
455 }
456
457 // Store additional data that matches the defined additional headers in the additional fields map
458 for (auto& additionalField : additionalHeadersIndexMap_) {
459 std::string value = loadOptionalString(additionalField.first);
460 if (!value.empty())
461 cr.additionalFields[*additionalField.second.begin()] = value;
462 }
463
464 // Add the CRIF record to the net records
465 addRecordToCrif(result, std::move(cr));
466 } catch (const exception& e) {
467 ore::data::StructuredTradeErrorMessage(tradeId, tradeType, "CRIF loading",
468 "Line number: " + to_string(currentLine) +
469 ". Error processing CRIF line, so skipping it. Error: " + to_string(e.what()))
470 .log();
471 return false;
472 }
473
474 return true;
475}
476
477} // namespace analytics
478} // namespace ore
void addRecord(const CrifRecord &record, bool aggregateDifferentAmountCurrencies=false, bool sortFxVolQualifer=true)
Definition: crif.cpp:41
void currencyOverrides(CrifRecord &crifRecord) const
Override currency codes.
Definition: crifloader.cpp:145
static std::map< QuantLib::Size, std::set< std::string > > requiredHeaders
Map giving required CRIF file headers and their allowable alternatives.
Definition: crifloader.hpp:95
QuantLib::ext::shared_ptr< SimmConfiguration > configuration_
Simm configuration that is used during loading of CRIF records.
Definition: crifloader.hpp:79
void updateMapping(const CrifRecord &cr) const
update bucket mappings
Definition: crifloader.cpp:186
static std::map< QuantLib::Size, std::set< std::string > > optionalHeaders
Map giving optional CRIF file headers and their allowable alternatives.
Definition: crifloader.hpp:98
void validateSimmRecord(const CrifRecord &cr) const
Check if the record is a valid Simm Crif Record.
Definition: crifloader.cpp:112
std::vector< std::set< std::string > > additionalHeaders_
Defines accepted column headers, beyond required and optional headers, see crifloader....
Definition: crifloader.hpp:82
void addRecordToCrif(Crif &crif, CrifRecord &&recordToAdd) const
Definition: crifloader.cpp:95
std::stringstream stream() const override
Definition: crifloader.cpp:227
std::stringstream stream() const override
Definition: crifloader.cpp:216
std::map< QuantLib::Size, QuantLib::Size > columnIndex_
Definition: crifloader.hpp:119
bool process(const std::vector< std::string > &entries, QuantLib::Size maxIndex, QuantLib::Size currentLine, Crif &result)
Definition: crifloader.cpp:336
virtual std::stringstream stream() const =0
StringStreamCrifLoader(const QuantLib::ext::shared_ptr< SimmConfiguration > &configuration, const std::vector< std::set< std::string > > &additionalHeaders={}, bool updateMapper=false, bool aggregateTrades=true, char eol='\n', char delim='\t', char quoteChar='\0', char escapeChar='\\', const std::string &nullString="#N/A")
Definition: crifloader.cpp:197
std::map< QuantLib::Size, std::set< std::string > > additionalHeadersIndexMap_
Definition: crifloader.hpp:122
Crif loadFromStream(std::stringstream &&stream)
Core CRIF loader from generic istream.
Definition: crifloader.cpp:233
void processHeader(const std::vector< std::string > &headers)
Process the elements of a header line of a CRIF file.
Definition: crifloader.cpp:284
SafeStack< ValueType > value
Class for loading CRIF records.
bool parseBool(const string &s)
pair< Currency, Currency > parseCurrencyPair(const string &s, const string &delimiters)
Real parseReal(const string &s)
bool checkCurrency(const string &code)
#define LOG(text)
#define WLOG(text)
SimmConfiguration::IMModel parseIMModel(const string &model)
boost::bimap< T, boost::bimaps::set_of< string, string_cmp > > bm
Definition: riskfilter.cpp:42
CrifRecord::RiskType parseRiskType(const string &rt)
Definition: crifrecord.cpp:117
CrifRecord::ProductClass parseProductClass(const string &pc)
Definition: crifrecord.cpp:127
std::vector< string > parseListOfValues(string s, const char escape, const char delim, const char quote)
std::string to_string(const LocationInfo &l)
Abstract base class for classes that map SIMM qualifiers to buckets.
SIMM configuration interface.
std::map< std::string, std::variant< std::string, double, bool > > additionalFields
Definition: crifrecord.hpp:171
NettingSetDetails nettingSetDetails
Definition: crifrecord.hpp:156
Class for structured analytics warnings.