Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
parsensitivityanalysis.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2017 Quaternion Risk Management Ltd
3 All rights reserved.
4
5 This file is part of ORE, a free-software/open-source library
6 for transparent pricing and risk analysis - http://opensourcerisk.org
7
8 ORE is free software: you can redistribute it and/or modify it
9 under the terms of the Modified BSD License. You should have received a
10 copy of the license along with this program.
11 The license is also available online at <http://opensourcerisk.org>
12
13 This program is distributed on the basis that it will form a useful
14 contribution to risk analytics and model standardisation, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
28
34
49
50#include <ql/cashflows/capflooredinflationcoupon.hpp>
51#include <ql/cashflows/indexedcashflow.hpp>
52#include <ql/cashflows/overnightindexedcoupon.hpp>
53#include <ql/errors.hpp>
54#include <ql/indexes/ibor/libor.hpp>
55#include <ql/instruments/creditdefaultswap.hpp>
56#include <ql/instruments/forwardrateagreement.hpp>
57#include <ql/instruments/makecapfloor.hpp>
58#include <ql/instruments/makeois.hpp>
59#include <ql/instruments/makevanillaswap.hpp>
60#include <ql/instruments/yearonyearinflationswap.hpp>
61#include <ql/instruments/zerocouponinflationswap.hpp>
62#include <ql/math/solvers1d/newtonsafe.hpp>
63#include <ql/pricingengines/capfloor/bacheliercapfloorengine.hpp>
64#include <ql/pricingengines/capfloor/blackcapfloorengine.hpp>
65#include <ql/pricingengines/credit/midpointcdsengine.hpp>
66#include <ql/pricingengines/swap/discountingswapengine.hpp>
67#include <ql/quotes/derivedquote.hpp>
68#include <ql/termstructures/yield/flatforward.hpp>
69#include <ql/termstructures/yield/oisratehelper.hpp>
71
72#include <boost/lexical_cast.hpp>
73#include <boost/numeric/ublas/operation.hpp>
74#include <boost/numeric/ublas/vector.hpp>
75
76using namespace QuantLib;
77using namespace QuantExt;
78using namespace std;
79using namespace ore::data;
80using namespace ore::analytics;
81
82using boost::numeric::ublas::element_div;
83using boost::numeric::ublas::element_prod;
84
85namespace ore {
86namespace analytics {
87
88//! Constructor
90 const QuantLib::ext::shared_ptr<ScenarioSimMarketParameters>& simMarketParams,
91 const SensitivityScenarioData& sensitivityData,
92 const string& marketConfiguration, const bool continueOnError,
93 const set<RiskFactorKey::KeyType>& typesDisabled)
94 : asof_(asof), simMarketParams_(simMarketParams), sensitivityData_(sensitivityData),
95 marketConfiguration_(marketConfiguration), continueOnError_(continueOnError), typesDisabled_(typesDisabled) {
96 const QuantLib::ext::shared_ptr<Conventions>& conventions = InstrumentConventions::instance().conventions();
97 QL_REQUIRE(conventions != nullptr, "conventions are empty");
101}
102
104 LOG("Augment relevant risk factors, starting with " << relevantRiskFactors_.size() << " risk factors.");
105 std::set<ore::analytics::RiskFactorKey> addFactors1 = relevantRiskFactors_, addFactors2, tmp;
106 do {
107 // DLOG("new pass with " << addFactors1.size() << " risk factors.");
108 for (auto const& r : addFactors1) {
109 // DLOG("candidate risk factor " << r);
110 for (auto const& s : instruments_.parHelpers_) {
111 if (riskFactorKeysAreSimilar(s.first, r)) {
112 tmp.insert(s.first);
113 // DLOG("factor " << r << ": found similar risk factor " << s.first);
114 for (auto const& d : instruments_.parHelperDependencies_[s.first]) {
115 if (std::find(relevantRiskFactors_.begin(), relevantRiskFactors_.end(), d) ==
117 addFactors2.insert(d);
118 }
119 }
120 }
121 for (auto const& s : instruments_.parCaps_) {
122 if (riskFactorKeysAreSimilar(s.first, r)) {
123 tmp.insert(s.first);
124 // DLOG("factor " << r << ": found similar risk factor " << s.first);
125 for (auto const& d : instruments_.parHelperDependencies_[s.first]) {
126 if (std::find(relevantRiskFactors_.begin(), relevantRiskFactors_.end(), d) ==
128 addFactors2.insert(d);
129 }
130 }
131 }
132 }
133 addFactors1.swap(addFactors2);
134 addFactors2.clear();
135 relevantRiskFactors_.insert(tmp.begin(), tmp.end());
136 tmp.clear();
137 } while (addFactors1.size() > 0);
138 LOG("Done, relevant risk factor size now " << relevantRiskFactors_.size());
139}
140
141namespace {
142void writeSensitivity(const RiskFactorKey& a, const RiskFactorKey& b, const Real value,
143 std::map<std::pair<RiskFactorKey, RiskFactorKey>, Real>& parSensi,
144 std::set<RiskFactorKey>& parKeysNonZero, std::set<RiskFactorKey>& rawKeysNonZero) {
145 if (close_enough(value, 0.0))
146 return;
147 parKeysNonZero.insert(a);
148 rawKeysNonZero.insert(b);
149 parSensi[std::make_pair(a, b)] = value;
150 DLOG("ParInstrument Sensi " << a << " w.r.t. " << b << " " << setprecision(6) << value);
151}
152} // namespace
153
154void ParSensitivityAnalysis::computeParInstrumentSensitivities(const QuantLib::ext::shared_ptr<ScenarioSimMarket>& simMarket) {
155
156 LOG("Cache base scenario par rates and flat vols");
157
158 if (relevantRiskFactors_.empty()) {
159 DLOG("Relevant risk factors not provided");
160 } else {
161 DLOG("Relevant risk factors provided:");
162 for (auto const& rf : relevantRiskFactors_)
163 DLOG("Relevant risk factor " << rf);
164 }
165
166 // remove todays fixings from relevant indices for the scope of this method
167 struct TodaysFixingsRemover {
168 TodaysFixingsRemover(const std::set<std::string>& names) : today_(Settings::instance().evaluationDate()) {
169 Date today = Settings::instance().evaluationDate();
170 for (auto const& n : names) {
171 TimeSeries<Real> t = IndexManager::instance().getHistory(n);
172 if (t[today] != Null<Real>()) {
173 DLOG("removing todays fixing (" << std::setprecision(6) << t[today] << ") from " << n);
174 savedFixings_.insert(std::make_pair(n, t[today]));
175 t[today] = Null<Real>();
176 IndexManager::instance().setHistory(n, t);
177 }
178 }
179 }
180 ~TodaysFixingsRemover() {
181 for (auto const& p : savedFixings_) {
182 TimeSeries<Real> t = IndexManager::instance().getHistory(p.first);
183 t[today_] = p.second;
184 IndexManager::instance().setHistory(p.first, t);
185 DLOG("restored todays fixing (" << std::setprecision(6) << p.second << ") for " << p.first);
186 }
187 }
188 const Date today_;
189 std::set<std::pair<std::string, Real>> savedFixings_;
190 };
191 TodaysFixingsRemover fixingRemover(instruments_.removeTodaysFixingIndices_);
192
193 // We must have a ShiftScenarioGenerator
194 QuantLib::ext::shared_ptr<ScenarioGenerator> simMarketScenGen = simMarket->scenarioGenerator();
195 QuantLib::ext::shared_ptr<ShiftScenarioGenerator> scenarioGenerator =
196 QuantLib::ext::dynamic_pointer_cast<ShiftScenarioGenerator>(simMarketScenGen);
197
198 struct SimMarketResetter {
199 SimMarketResetter(QuantLib::ext::shared_ptr<SimMarket> simMarket) : simMarket_(simMarket) {}
200 ~SimMarketResetter() { simMarket_->reset(); }
201 QuantLib::ext::shared_ptr<SimMarket> simMarket_;
202 } simMarketResetter(simMarket);
203
204 simMarket->reset();
205 scenarioGenerator->reset();
206 simMarket->update(asof_);
207
208 if (!relevantRiskFactors_.empty())
213
214 map<RiskFactorKey, Real> parRatesBase, parCapVols; // for both ir and yoy caps
215
216 for (auto& p : instruments_.parHelpers_) {
217 try {
218 Real parRate = impliedQuote(p.second);
219 parRatesBase[p.first] = parRate;
220
221 // Populate zero and par shift size for the current risk factor
222 populateShiftSizes(p.first, parRate, simMarket);
223
224 } catch (const std::exception& e) {
225 QL_FAIL("could not imply quote for par helper " << p.first << ": " << e.what());
226 }
227 }
228
229 for (auto& c : instruments_.parCaps_) {
230
231 QL_REQUIRE(instruments_.parCapsYts_.count(c.first) > 0,
232 "computeParInstrumentSensitivities(): no cap yts found for key " << c.first);
233 QL_REQUIRE(instruments_.parCapsVts_.count(c.first) > 0,
234 "computeParInstrumentSensitivities(): no cap vts found for key " << c.first);
235
236 Real price = c.second->NPV();
238 *c.second, price, instruments_.parCapsYts_.at(c.first), 0.01,
239 instruments_.parCapsVts_.at(c.first)->volatilityType(), instruments_.parCapsVts_.at(c.first)->displacement());
240 parCapVols[c.first] = parVol;
241 TLOG("Fair implied cap volatility for key " << c.first << " is " << std::fixed << std::setprecision(12)
242 << parVol << ".");
243
244 // Populate zero and par shift size for the current risk factor
245 populateShiftSizes(c.first, parVol, simMarket);
246 }
247
248 for (auto& c : instruments_.parYoYCaps_) {
249
250 QL_REQUIRE(instruments_.parYoYCapsYts_.count(c.first) > 0,
251 "computeParInstrumentSensitivities(): no cap yts found for key " << c.first);
252 QL_REQUIRE(instruments_.parYoYCapsIndex_.count(c.first) > 0,
253 "computeParInstrumentSensitivities(): no cap index found for key " << c.first);
254 QL_REQUIRE(instruments_.parYoYCapsVts_.count(c.first) > 0,
255 "computeParInstrumentSensitivities(): no cap vts found for key " << c.first);
256
257 Real price = c.second->NPV();
259 *c.second, price, instruments_.parYoYCapsYts_.at(c.first), 0.01,
260 instruments_.parYoYCapsVts_.at(c.first)->volatilityType(),
261 instruments_.parYoYCapsVts_.at(c.first)->displacement(), instruments_.parYoYCapsIndex_.at(c.first));
262 parCapVols[c.first] = parVol;
263 TLOG("Fair implied yoy cap volatility for key " << c.first << " is " << std::fixed << std::setprecision(12)
264 << parVol << ".");
265
266 // Populate zero and par shift size for the current risk factor
267 populateShiftSizes(c.first, parVol, simMarket);
268 }
269
270 LOG("Caching base scenario par rates and float vols done.");
271
272 /****************************************************************
273 * Discount curve instrument fair rate sensitivity to zero shifts
274 * Index curve instrument fair rate sensitivity to zero shifts
275 * Cap/Floor flat vol sensitivity to optionlet vol shifts
276 *
277 * Step 3:
278 * - Apply all single up-shift scenarios,
279 * - Compute respective fair par rates and flat vols
280 * - Compute par rate / flat vol sensitivities
281 */
282 LOG("Compute par rate and flat vol sensitivities");
283
284 vector<ShiftScenarioGenerator::ScenarioDescription> desc = scenarioGenerator->scenarioDescriptions();
285 QL_REQUIRE(desc.size() == scenarioGenerator->samples(),
286 "descriptions size " << desc.size() << " does not match samples " << scenarioGenerator->samples());
287
288 std::set<RiskFactorKey> parKeysCheck, parKeysNonZero;
289 std::set<RiskFactorKey> rawKeysCheck, rawKeysNonZero;
290
291 for (auto const& p : instruments_.parHelpers_) {
292 parKeysCheck.insert(p.first);
293 }
294
295 for (auto const& p : instruments_.parCaps_) {
296 parKeysCheck.insert(p.first);
297 }
298
299 for (auto const& p : instruments_.parYoYCaps_) {
300 parKeysCheck.insert(p.first);
301 }
302
303 for (Size i = 1; i < scenarioGenerator->samples(); ++i) {
304
305 simMarket->update(asof_);
306
307 // use single "UP" shift scenarios only, use only scenarios relevant for par instruments,
308 // use relevant scenarios only, if specified
309 // ignore risk factor types that have been disabled
311 !isParType(desc[i].key1().keytype) || typesDisabled_.count(desc[i].key1().keytype) == 1 ||
312 !(relevantRiskFactors_.empty() || relevantRiskFactors_.find(desc[i].key1()) != relevantRiskFactors_.end()))
313 continue;
314
315 // Since we are not using ValuationEngine we need to manually perform the trade updates here
316 // TODO - explore means of utilising valuation engine
317 if (ObservationMode::instance().mode() == ObservationMode::Mode::Disable) {
318 for (auto it : instruments_.parHelpers_)
319 it.second->deepUpdate();
320 for (auto it : instruments_.parCaps_)
321 it.second->deepUpdate();
322 for (auto it : instruments_.parYoYCaps_)
323 it.second->deepUpdate();
324 }
325
326 rawKeysCheck.insert(desc[i].key1());
327
328 // Get the absolute shift size and skip if close to zero
329
330 Real shiftSize = getShiftSize(desc[i].key1(), sensitivityData_, simMarket);
331
332 if (close_enough(shiftSize, 0.0)) {
333 ALOG("Shift size for " << desc[i].key1() << " is zero, skipping");
334 continue;
335 }
336
337 // process par helpers
338
339 std::set<RiskFactorKey::KeyType> survivalAndRateCurveTypes = {
342
343 for (auto const& p : instruments_.parHelpers_) {
344
345 // skip if par helper has no sensi to zero risk factor (except the special treatment below kicks in)
346
347 if (p.second->isCalculated() &&
348 (survivalAndRateCurveTypes.find(p.first.keytype) == survivalAndRateCurveTypes.end() ||
349 p.first != desc[i].key1())) {
350 continue;
351 }
352
353 // compute fair and base quotes
354
355 Real fair = impliedQuote(p.second);
356 auto base = parRatesBase.find(p.first);
357 QL_REQUIRE(base != parRatesBase.end(), "internal error: did not find parRatesBase[" << p.first << "]");
358
359 Real tmp = (fair - base->second) / shiftSize;
360
361 // special treatments for certain risk factors
362
363 // for curves with survival probabilities / discount factors going to zero quickly we might see a
364 // sensitivity that is close to zero, which we sanitise here in order to prevent the Jacobi matrix
365 // getting ill-conditioned or even singular
366
367 if (survivalAndRateCurveTypes.find(p.first.keytype) != survivalAndRateCurveTypes.end() &&
368 p.first == desc[i].key1() && std::abs(tmp) < 0.01) {
369 WLOG("Setting Diagonal Sensi " << p.first << " w.r.t. " << desc[i].key1() << " to 0.01 (got " << tmp
370 << ")");
371 tmp = 0.01;
372 }
373
374 // YoY diagnoal entries are 1.0
375
376 if (p.first.keytype == RiskFactorKey::KeyType::YoYInflationCurve && p.first == desc[i].key1() &&
377 close_enough(tmp, 0.0)) {
378 tmp = 1.0;
379 }
380
381 // write sensitivity
382
383 writeSensitivity(p.first, desc[i].key1(), tmp, parSensi_, parKeysNonZero, rawKeysNonZero);
384 }
385
386 // process par caps
387
388 for (auto const& p : instruments_.parCaps_) {
389
390 if (p.second->isCalculated() && p.first != desc[i].key1())
391 continue;
392
393 auto fair = impliedVolatility(p.first, instruments_);
394 auto base = parCapVols.find(p.first);
395 QL_REQUIRE(base != parCapVols.end(), "internal error: did not find parCapVols[" << p.first << "]");
396
397 Real tmp = (fair - base->second) / shiftSize;
398
399 // ensure Jacobi matrix is regular and not (too) ill-conditioned, this is necessary because
400 // a) the shift size used to compute dpar / dzero might be close to zero and / or
401 // b) the implied vol calculation has numerical inaccuracies
402
403 if (p.first == desc[i].key1() && std::abs(tmp) < 0.01) {
404 WLOG("Setting Diagonal CapFloorVol Sensi " << p.first << " w.r.t. " << desc[i].key1()
405 << " to 0.01 (got " << tmp << ")");
406 tmp = 0.01;
407 }
408
409 // write sensitivity
410
411 writeSensitivity(p.first, desc[i].key1(), tmp, parSensi_, parKeysNonZero, rawKeysNonZero);
412 }
413
414 // process par yoy caps
415
416 for (auto const& p : instruments_.parYoYCaps_) {
417
418 if (p.second->isCalculated() && p.first != desc[i].key1())
419 continue;
420
421 auto fair = impliedVolatility(p.first, instruments_);
422 auto base = parCapVols.find(p.first);
423 QL_REQUIRE(base != parCapVols.end(), "internal error: did not find parCapVols[" << p.first << "]");
424
425 Real tmp = (fair - base->second) / shiftSize;
426
427 // ensure Jacobi matrix is regular and not (too) ill-conditioned, this is necessary because
428 // a) the shift size used to compute dpar / dzero might be close to zero and / or
429 // b) the implied vol calculation has numerical inaccuracies
430
431 if (p.first == desc[i].key1() && std::abs(tmp) < 0.01) {
432 WLOG("Setting Diagonal CapFloorVol Sensi " << p.first << " w.r.t. " << desc[i].key1()
433 << " to 0.01 (got " << tmp << ")");
434 tmp = 0.01;
435 }
436
437 // write sensitivity
438
439 writeSensitivity(p.first, desc[i].key1(), tmp, parSensi_, parKeysNonZero, rawKeysNonZero);
440 }
441
442 } // end of loop over samples
443
444 // check for
445 // a) par instruments which have no sensitivity to any of the risk factors
446 // b) risk factors w.r.t. which no par instrument has a sensitivity
447 std::set<RiskFactorKey> parKeysZero, rawKeysZero;
448 std::set_difference(parKeysCheck.begin(), parKeysCheck.end(), parKeysNonZero.begin(), parKeysNonZero.end(),
449 std::inserter(parKeysZero, parKeysZero.begin()));
450 std::set_difference(rawKeysCheck.begin(), rawKeysCheck.end(), rawKeysNonZero.begin(), rawKeysNonZero.end(),
451 std::inserter(rawKeysZero, rawKeysZero.begin()));
452 std::set<RiskFactorKey> problematicKeys;
453 problematicKeys.insert(parKeysZero.begin(), parKeysZero.end());
454 problematicKeys.insert(rawKeysZero.begin(), rawKeysZero.end());
455 for (auto const& k : problematicKeys) {
456 std::string type;
457 if (parKeysZero.find(k) != parKeysZero.end())
458 type = "par instrument is insensitive to all zero risk factors";
459 else if (rawKeysZero.find(k) != rawKeysZero.end())
460 type = "zero risk factor that does not affect an par instrument";
461 else
462 type = "unknown";
463 Real parHelperValue = Null<Real>();
464 if (auto tmp = instruments_.parHelpers_.find(k); tmp != instruments_.parHelpers_.end())
465 parHelperValue = impliedQuote(tmp->second);
466 else if (auto tmp = instruments_.parCaps_.find(k); tmp != instruments_.parCaps_.end())
467 parHelperValue = tmp->second->NPV();
468 else if (auto tmp = instruments_.parYoYCaps_.find(k); tmp != instruments_.parYoYCaps_.end())
469 parHelperValue = tmp->second->NPV();
470 Real zeroFactorValue = Null<Real>();
471 if (simMarket->baseScenarioAbsolute()->has(k))
472 zeroFactorValue = simMarket->baseScenarioAbsolute()->get(k);
473 WLOG("zero/par relation problem for key '"
474 << k << "', type " + type + ", par value = "
475 << (parHelperValue == Null<Real>() ? "na" : std::to_string(parHelperValue))
476 << ", zero value = " << (zeroFactorValue == Null<Real>() ? "na" : std::to_string(zeroFactorValue)));
477 }
478
479 LOG("Computing par rate and flat vol sensitivities done");
480} // compute par instrument sensis
481
483 LOG("Align simulation market pillars to actual latest relevant dates of par instruments");
484 // If any of the yield curve types are still active, align the pillars.
488 for (auto const& p : instruments_.yieldCurvePillars_) {
489 simMarketParams_->setYieldCurveTenors(p.first, p.second);
490 DLOG("yield curve tenors for " << p.first << " set (" << p.second.size() << ")");
491 for (auto const& t : p.second)
492 TLOG("set tenor " << t.length() << " " << t.units())
493 }
494 }
496 for (auto const& p : instruments_.capFloorPillars_) {
497 simMarketParams_->setCapFloorVolExpiries(p.first, p.second);
498 DLOG("cap floor expiries for " << p.first << " set (" << p.second.size() << ")");
499 for (auto const& t : p.second)
500 TLOG("set tenor " << t.length() << " " << t.units())
501 }
502 }
504 for (auto const& p : instruments_.yoyCapFloorPillars_) {
505 simMarketParams_->setYoYInflationCapFloorVolExpiries(p.first, p.second);
506 DLOG("yoy cap floor expiries for " << p.first << " set (" << p.second.size() << ")");
507 for (auto const& t : p.second)
508 TLOG("set tenor " << t.length() << " " << t.units())
509 }
510 }
512 for (auto const& p : instruments_.cdsPillars_) {
513 simMarketParams_->setDefaultTenors(p.first, p.second);
514 DLOG("default expiries for " << p.first << " set (" << p.second.size() << ")");
515 for (auto const& t : p.second)
516 TLOG("set tenor " << t.length() << " " << t.units())
517 }
518 }
520 for (auto const& p : instruments_.zeroInflationPillars_) {
521 simMarketParams_->setZeroInflationTenors(p.first, p.second);
522 DLOG("zero inflation expiries for " << p.first << " set (" << p.second.size() << ")");
523 for (auto const& t : p.second)
524 TLOG("set tenor " << t.length() << " " << t.units())
525 }
526 }
528 for (auto const& p : instruments_.yoyInflationPillars_) {
529 simMarketParams_->setYoyInflationTenors(p.first, p.second);
530 DLOG("yoy inflation expiries for " << p.first << " set (" << p.second.size() << ")");
531 for (auto const& t : p.second)
532 TLOG("set tenor " << t.length() << " " << t.units())
533 }
534 }
535 LOG("Alignment of pillars done.");
536}
537
539
540void ParSensitivityAnalysis::disable(const set<RiskFactorKey::KeyType>& types) {
541 // Insert only types that are available for par sensitivity analysis in to the set of disabled types.
542 for (const auto& type : types) {
543 if (isParType(type)) {
544 typesDisabled_.insert(type);
545 }
546 }
547}
548
550 const QuantLib::ext::shared_ptr<ScenarioSimMarket>& simMarket) {
551
552 // Get zero and par shift size for the key
553 Real zeroShiftSize = getShiftSize(key, sensitivityData_, simMarket);
554 auto shiftData = sensitivityData_.shiftData(key.keytype, key.name);
555 Real parShiftSize = shiftData.shiftSize;
556 if (shiftData.shiftType == ShiftType::Relative)
557 parShiftSize *= parRate;
558
559 // Update the map
560 shiftSizes_[key] = make_pair(zeroShiftSize, parShiftSize);
561
562 TLOG("Zero and par shift size for risk factor '" << key << "' is (" << std::fixed << std::setprecision(12)
563 << zeroShiftSize << "," << parShiftSize << ")");
564}
565
566set<RiskFactorKey::KeyType> ParSensitivityAnalysis::parTypes_ = {
571
573 const map<RiskFactorKey, pair<Real, Real>>& shiftSizes) {
574
575 // Populate the set of par keys (rows of Jacobi) and raw zero keys (columns of Jacobi)
576 for (auto parEntry : parSensitivities) {
577 parKeys_.insert(parEntry.first.first);
578 rawKeys_.insert(parEntry.first.second);
579 }
580
581 Size n_par = parKeys_.size();
582 Size n_raw = rawKeys_.size();
583 SparseMatrix jacobi_transp(n_raw, n_par); // transposed Jacobi
584 LOG("Transposed Jacobi matrix dimension " << n_raw << " x " << n_par);
585 if (parKeys_ != rawKeys_) {
586 std::set<RiskFactorKey> parMinusRaw, rawMinusPar;
587 std::set_difference(parKeys_.begin(), parKeys_.end(), rawKeys_.begin(), rawKeys_.end(),
588 std::inserter(parMinusRaw, parMinusRaw.begin()));
589 std::set_difference(rawKeys_.begin(), rawKeys_.end(), parKeys_.begin(), parKeys_.end(),
590 std::inserter(rawMinusPar, rawMinusPar.begin()));
591 for (auto const& p : parMinusRaw) {
592 ALOG("par key '" << p << "' not in raw key set");
593 }
594 for (auto const& p : rawMinusPar) {
595 ALOG("raw key '" << p << "' not in par key set");
596 }
597 QL_FAIL("Zero and par keys should be equal for par conversion, see log for differences");
598 }
599 QL_REQUIRE(n_raw > 0, "Transposed Jacobi matrix has size 0");
600
601 LOG("Populating the vector of zero and par shift sizes");
602 zeroShifts_.resize(n_raw);
603 parShifts_.resize(n_par);
604 Size i = 0;
605 for (const auto& key : rawKeys_) {
606 QL_REQUIRE(shiftSizes.count(key) == 1, "ParSensitivityConverter is missing shift sizes for key " << key);
607 // avoid division by zero below: if we have a zero shift, the corresponding zero sensi will be zero, too,
608 // but if we divide this sensi by zero, we will get nan instead of zero
609 zeroShifts_[i] = std::max(shiftSizes.at(key).first, 1E-10);
610 parShifts_[i] = shiftSizes.at(key).second;
611 i++;
612 }
613
614 LOG("Populating Transposed Jacobi matrix");
615 for (auto const& p : parSensitivities) {
616 Size parIdx = std::distance(parKeys_.begin(), parKeys_.find(p.first.first));
617 Size rawIdx = std::distance(rawKeys_.begin(), rawKeys_.find(p.first.second));
618 QL_REQUIRE(parIdx < n_par, "internal error: parKey " << p.first.first << " not found in parKeys_");
619 QL_REQUIRE(rawIdx < n_raw, "internal error: rawKey " << p.first.second << " not found in parKeys_");
620 jacobi_transp(rawIdx, parIdx) = p.second;
621 TLOG("Matrix entry [" << rawIdx << ", " << parIdx << "] ~ [raw:" << p.first.second << ", par:" << p.first.first
622 << "]: " << p.second);
623 }
624 LOG("Finished populating transposed Jacobi matrix, non-zero entries = "
625 << parSensitivities.size() << " ("
626 << 100.0 * static_cast<Real>(parSensitivities.size()) / static_cast<Real>(n_par * n_raw) << "%)");
627
628 LOG("Populating block indices");
629 vector<Size> blockIndices;
630 pair<RiskFactorKey::KeyType, string> previousGroup(RiskFactorKey::KeyType::None, "");
631 Size blockIndex = 0;
632 for (auto r : rawKeys_) {
633 // Update block indices with the index where the risk factor group changes
634 // e.g. when move from (DiscountCurve, EUR) to (DiscountCurve, USD)
635 pair<RiskFactorKey::KeyType, string> thisGroup(r.keytype, r.name);
636 if (blockIndex > 0 && previousGroup != thisGroup) {
637 blockIndices.push_back(blockIndex);
638 TLOG("Adding block index " << blockIndex);
639 }
640 ++blockIndex;
641
642 // Update the risk factor group
643 previousGroup = thisGroup;
644 }
645 blockIndices.push_back(blockIndex);
646 TLOG("Adding block index " << blockIndex);
647 LOG("Finished Populating block indices.");
648
649 LOG("Invert Transposed Jacobi matrix");
650 bool success = true;
651 try {
652 jacobi_transp_inv_ = blockMatrixInverse(jacobi_transp, blockIndices);
653 } catch (const std::exception& e) {
654 // something went wrong during the matrix inversion, so we run an extended analysis on the original matrix
655 // to see whether there are zero or linearly dependent rows / columns
656 StructuredAnalyticsErrorMessage("Par sensitivity conversion", "Transposed Jacobi matrix inversion failed",
657 e.what())
658 .log();
659 LOG("Running extended matrix diagnostics (looking for zero or linearly dependent rows / columns...)");
660 constexpr Size nOp = 1000; // number of operations for close_enough comparisons below
661 LOG("Checking for zero rows...");
662 for (auto it1 = jacobi_transp.begin1(); it1 != jacobi_transp.end1(); ++it1) {
663 Real tmp = 0.0;
664 for (auto it2 = it1.begin(); it2 != it1.end(); ++it2) {
665 tmp += (*it2) * (*it2);
666 }
667 if (close_enough(tmp, 0.0, n_par * nOp)) {
668 WLOG("row " << it1.index1() << " is zero");
669 }
670 }
671 LOG("Checking for zero columns...");
672 for (auto it2 = jacobi_transp.begin2(); it2 != jacobi_transp.end2(); ++it2) {
673 Real tmp = 0.0;
674 for (auto it1 = it2.begin(); it1 != it2.end(); ++it1) {
675 tmp += (*it1) * (*it1);
676 }
677 if (close_enough(tmp, 0.0, n_par * nOp)) {
678 WLOG("column " << it2.index2() << " is zero");
679 }
680 }
681 LOG("Checking for linearly dependent rows...");
682 for (auto it1 = jacobi_transp.begin1(); it1 != jacobi_transp.end1(); ++it1) {
683 for (auto it1b = jacobi_transp.begin1(); it1b != jacobi_transp.end1(); ++it1b) {
684 if (it1b.index1() <= it1.index1())
685 continue;
686 Real ratio = Null<Real>();
687 if (it1.begin() != it1.end() && it1b.begin() != it1b.end()) {
688 bool dependent = true;
689 auto it2b = it1b.begin();
690 for (auto it2 = it1.begin(); it2 != it1.end() && dependent; ++it2) {
691 if (close_enough(*it2, 0.0, nOp))
692 continue;
693 bool foundMatchingIndex = false;
694 while (it2b != it1b.end() && it2b.index2() <= it2.index2() && dependent) {
695 if (it2b.index2() < it2.index2()) {
696 if (!close_enough(*it2b, 0.0, nOp))
697 dependent = false;
698 } else {
699 foundMatchingIndex = true;
700 if (close_enough(*it2b, 0.0, nOp)) {
701 dependent = false;
702 } else if (ratio == Null<Real>()) {
703 ratio = *it2b / *it2;
704 } else if (!close_enough(*it2b / *it2, ratio, nOp)) {
705 dependent = false;
706 }
707 }
708 ++it2b;
709 }
710 if (!foundMatchingIndex)
711 dependent = false;
712 }
713 while (it2b != it1b.end() && dependent) {
714 if (!close_enough(*it2b, 0.0))
715 dependent = false;
716 ++it2b;
717 }
718 if (dependent) {
719 WLOG("rows " << it1.index1() << " and " << it1b.index1() << " are linearly dependent.");
720 }
721 }
722 }
723 }
724 LOG("Checking for linearly dependent columns...");
725 for (auto it1 = jacobi_transp.begin2(); it1 != jacobi_transp.end2(); ++it1) {
726 for (auto it1b = jacobi_transp.begin2(); it1b != jacobi_transp.end2(); ++it1b) {
727 if (it1b.index2() <= it1.index2())
728 continue;
729 Real ratio = Null<Real>();
730 if (it1.begin() != it1.end() && it1b.begin() != it1b.end()) {
731 bool dependent = true;
732 auto it2b = it1b.begin();
733 for (auto it2 = it1.begin(); it2 != it1.end() && dependent; ++it2) {
734 if (close_enough(*it2, 0.0, nOp))
735 continue;
736 bool foundMatchingIndex = false;
737 while (it2b != it1b.end() && it2b.index1() <= it2.index1() && dependent) {
738 if (it2b.index1() < it2.index1()) {
739 if (!close_enough(*it2b, 0.0, nOp))
740 dependent = false;
741 } else {
742 foundMatchingIndex = true;
743 if (close_enough(*it2b, 0.0, nOp)) {
744 dependent = false;
745 } else if (ratio == Null<Real>()) {
746 ratio = *it2b / *it2;
747 } else if (!close_enough(*it2b / *it2, ratio, nOp)) {
748 dependent = false;
749 }
750 }
751 ++it2b;
752 }
753 if (!foundMatchingIndex)
754 dependent = false;
755 }
756 while (it2b != it1b.end() && dependent) {
757 if (!close_enough(*it2b, 0.0))
758 dependent = false;
759 ++it2b;
760 }
761 if (dependent) {
762 WLOG("columns " << it1.index2() << " and " << it1b.index2() << " are linearly dependent.");
763 }
764 }
765 }
766 }
767 LOG("Extended matrix diagnostics done. Exiting application.");
768 success = false;
769 }
770 QL_REQUIRE(success, "Jacobi matrix inversion failed, see log file for more details.");
771 Real conditionNumber = modifiedMaxNorm(jacobi_transp) * modifiedMaxNorm(jacobi_transp_inv_);
772 LOG("Inverse Jacobi done, condition number of Jacobi matrix is " << conditionNumber);
773 DLOG("Diagonal entries of Jacobi and inverse Jacobi:");
774 DLOG("row/col Jacobi Inverse");
775 for (Size j = 0; j < jacobi_transp.size1(); ++j) {
776 DLOG(right << setw(7) << j << setw(20) << jacobi_transp(j, j) << setw(20) << jacobi_transp_inv_(j, j));
777 }
778}
779
780boost::numeric::ublas::vector<Real>
781ParSensitivityConverter::convertSensitivity(const boost::numeric::ublas::vector<Real>& zeroSensitivities) {
782
783 DLOG("Start sensitivity conversion");
784
785 Size dim = zeroSensitivities.size();
786 QL_REQUIRE(jacobi_transp_inv_.size1() == dim, "Size mismatch between Transoposed Jacobi inverse matrix ["
787 << jacobi_transp_inv_.size1() << " x "
788 << jacobi_transp_inv_.size2() << "] and zero sensitivity array ["
789 << dim << "]");
790
791 // Vector storing approximation for \frac{\partial V}{\partial z_i} for each zero factor z_i
792 boost::numeric::ublas::vector<Real> zeroDerivs(dim);
793 zeroDerivs = element_div(zeroSensitivities, zeroShifts_);
794
795 // Vector initially storing approximation for \frac{\partial V}{\partial c_i} for each par factor c_i
796 boost::numeric::ublas::vector<Real> parSensitivities(dim);
797 boost::numeric::ublas::axpy_prod(jacobi_transp_inv_, zeroDerivs, parSensitivities, true);
798
799 // Update parSensitivities vector to hold the first order approximation of the NPV change due to the configured
800 // shift in each of the par factors c_i
801 parSensitivities = element_prod(parSensitivities, parShifts_);
802
803 DLOG("Sensitivity conversion done");
804
805 return parSensitivities;
806}
807
809
810 // Report headers
811 report.addColumn("RawFactor(z)", string());
812 report.addColumn("ParFactor(c)", string());
813 report.addColumn("dz/dc", double(), 12);
814
815 // Write report contents i.e. entries where sparse matrix is non-zero
816 Size parIdx = 0;
817 for (const auto& parKey : parKeys_) {
818 Size rawIdx = 0;
819 for (const auto& rawKey : rawKeys_) {
820 if (!close(jacobi_transp_inv_(parIdx, rawIdx), 0.0)) {
821 report.next();
822 report.add(to_string(rawKey));
823 report.add(to_string(parKey));
824 report.add(jacobi_transp_inv_(parIdx, rawIdx));
825 }
826 rawIdx++;
827 }
828 parIdx++;
829 }
830
831 // Close report
832 report.end();
833}
834
836
837 // Report headers
838 report.addColumn("ParFactor", string());
839 report.addColumn("RawFactor", string());
840 report.addColumn("ParSensitivity", double(), 12);
841
842 // Report body
843 for (const auto& parSensitivity : parSensitivities) {
844 RiskFactorKey parKey = parSensitivity.first.first;
845 RiskFactorKey rawKey = parSensitivity.first.second;
846 Real sensi = parSensitivity.second;
847
848 report.next();
849 report.add(to_string(parKey));
850 report.add(to_string(rawKey));
851 report.add(sensi);
852 }
853
854 report.end();
855}
856
857} // namespace analytics
858} // namespace ore
ore::analytics::SensitivityScenarioData sensitivityData_
Sensitivity data.
void disable(const std::set< ore::analytics::RiskFactorKey::KeyType > &types)
std::set< ore::analytics::RiskFactorKey::KeyType > typesDisabled_
Set of risk factor types disabled for this instance of ParSensitivityAnalysis.
void alignPillars()
align pillars in scenario simulation market parameters with those of the par instruments
ParContainer parSensi_
sensitivity of par rates w.r.t. raw rate shifts (including optionlet/cap volatility)
QuantLib::ext::shared_ptr< ore::analytics::ScenarioSimMarketParameters > simMarketParams_
Simulation market parameters.
QuantLib::Date asof_
As of date for the calculation of the par sensitivities.
std::map< ore::analytics::RiskFactorKey, std::pair< QuantLib::Real, QuantLib::Real > > shiftSizes_
std::set< ore::analytics::RiskFactorKey > relevantRiskFactors_
ParSensitivityInstrumentBuilder::Instruments instruments_
void augmentRelevantRiskFactors()
Augment relevant risk factors.
void computeParInstrumentSensitivities(const QuantLib::ext::shared_ptr< ore::analytics::ScenarioSimMarket > &simMarket)
Compute par instrument sensitivities.
std::map< std::pair< ore::analytics::RiskFactorKey, ore::analytics::RiskFactorKey >, Real > ParContainer
static bool isParType(ore::analytics::RiskFactorKey::KeyType type)
Returns true if risk factor type is applicable for par conversion.
static std::set< ore::analytics::RiskFactorKey::KeyType > parTypes_
ParSensitivityAnalysis(const QuantLib::Date &asof, const QuantLib::ext::shared_ptr< ore::analytics::ScenarioSimMarketParameters > &simMarketParams, const ore::analytics::SensitivityScenarioData &sensitivityData, const string &marketConfiguration=Market::defaultConfiguration, const bool continueOnError=false, const std::set< ore::analytics::RiskFactorKey::KeyType > &typesDisabled={})
Constructor.
void populateShiftSizes(const ore::analytics::RiskFactorKey &key, QuantLib::Real parRate, const QuantLib::ext::shared_ptr< ore::analytics::ScenarioSimMarket > &simMarket)
Populate shiftSizes_ for key given the implied fair par rate parRate.
void writeConversionMatrix(ore::data::Report &reportOut) const
Write the inverse of the transposed Jacobian to the reportOut.
std::set< ore::analytics::RiskFactorKey > parKeys_
boost::numeric::ublas::vector< Real > convertSensitivity(const boost::numeric::ublas::vector< Real > &zeroSensitivities)
Takes an array of zero sensitivities and returns an array of par sensitivities.
std::set< ore::analytics::RiskFactorKey > rawKeys_
boost::numeric::ublas::vector< QuantLib::Real > zeroShifts_
Vector of absolute zero shift sizes.
ParSensitivityConverter(const ParSensitivityAnalysis::ParContainer &parSensitivities, const std::map< ore::analytics::RiskFactorKey, std::pair< QuantLib::Real, QuantLib::Real > > &shiftSizes)
boost::numeric::ublas::vector< QuantLib::Real > parShifts_
Vector of absolute par shift sizes.
void createParInstruments(ParSensitivityInstrumentBuilder::Instruments &instruments, const QuantLib::Date &asof, const QuantLib::ext::shared_ptr< ore::analytics::ScenarioSimMarketParameters > &simMarketParams, const ore::analytics::SensitivityScenarioData &sensitivityData, const std::set< ore::analytics::RiskFactorKey::KeyType > &typesDisabled={}, const std::set< ore::analytics::RiskFactorKey::KeyType > &parTypes={}, const std::set< ore::analytics::RiskFactorKey > &relevantRiskFactors={}, const bool continueOnError=false, const std::string &marketConfiguration=ore::data::Market::defaultConfiguration, const QuantLib::ext::shared_ptr< ore::analytics::Market > &simMarket=nullptr) const
Create par QuantLib::Instruments.
Data types stored in the scenario class.
Definition: scenario.hpp:48
KeyType keytype
Key type.
Definition: scenario.hpp:89
std::string name
Key name.
Definition: scenario.hpp:94
KeyType
Risk Factor types.
Definition: scenario.hpp:51
Description of sensitivity shift scenarios.
const ShiftData & shiftData(const ore::analytics::RiskFactorKey::KeyType &keyType, const std::string &name) const
Give back the shift data for the given risk factor type, keyType, with the given name.
virtual Report & add(const ReportType &rt)=0
virtual Report & next()=0
virtual void end()=0
virtual Report & addColumn(const string &name, const ReportType &, Size precision=0)=0
SafeStack< ValueType > value
A cube implementation that stores the cube in memory.
#define LOG(text)
#define DLOG(text)
#define ALOG(text)
#define WLOG(text)
#define TLOG(text)
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
Real modifiedMaxNorm(const QuantLib::SparseMatrix &A)
Matrix blockMatrixInverse(const Matrix &A, const std::vector< Size > &blockIndices)
Volatility impliedVolatility(const QuantLib::CapFloor &cap, Real targetValue, const Handle< YieldTermStructure > &d, Volatility guess, VolatilityType type, Real displacement)
bool close(const Real &t_1, const Real &t_2)
void writeParConversionMatrix(const ParSensitivityAnalysis::ParContainer &parSensitivities, Report &report)
Write par instrument sensitivity report.
Real getShiftSize(const RiskFactorKey &key, const SensitivityScenarioData &sensiParams, const QuantLib::ext::shared_ptr< ScenarioSimMarket > &simMarket, const string &marketConfiguration)
bool riskFactorKeysAreSimilar(const ore::analytics::RiskFactorKey &x, const ore::analytics::RiskFactorKey &y)
true if key type and name are equal, do not care about the index though
Real impliedQuote(const QuantLib::ext::shared_ptr< Instrument > &i)
std::string to_string(const LocationInfo &l)
boost::bimap< std::string, TRS::FundingData::NotionalType > types
Singleton class to hold global Observation Mode.
Perfrom sensitivity analysis for a given portfolio.
A class to hold the parametrisation for building sensitivity scenarios.
factory classes for simple scenarios
std::map< ore::analytics::RiskFactorKey, QuantLib::Handle< QuantExt::YoYOptionletVolatilitySurface > > parYoYCapsVts_
std::map< std::string, std::vector< QuantLib::Period > > zeroInflationPillars_
std::map< ore::analytics::RiskFactorKey, QuantLib::Handle< QuantLib::YieldTermStructure > > parYoYCapsYts_
par helpers: YoY cap / floors
std::map< ore::analytics::RiskFactorKey, QuantLib::ext::shared_ptr< QuantLib::Instrument > > parHelpers_
par helpers (all except cap/floors)
std::map< ore::analytics::RiskFactorKey, std::set< ore::analytics::RiskFactorKey > > parHelperDependencies_
list of (raw) risk factors on which a par helper depends
std::map< std::string, std::vector< QuantLib::Period > > yieldCurvePillars_
par QuantLib::Instrument pillars
std::map< ore::analytics::RiskFactorKey, QuantLib::Handle< QuantLib::YieldTermStructure > > parCapsYts_
std::map< ore::analytics::RiskFactorKey, QuantLib::ext::shared_ptr< QuantLib::CapFloor > > parCaps_
par helpers: IR cap / floors
std::map< ore::analytics::RiskFactorKey, QuantLib::Handle< QuantLib::OptionletVolatilityStructure > > parCapsVts_
std::map< ore::analytics::RiskFactorKey, QuantLib::ext::shared_ptr< QuantLib::YoYInflationCapFloor > > parYoYCaps_
std::map< ore::analytics::RiskFactorKey, QuantLib::Handle< QuantLib::YoYInflationIndex > > parYoYCapsIndex_
std::map< std::string, std::vector< QuantLib::Period > > yoyCapFloorPillars_
std::map< std::string, std::vector< QuantLib::Period > > capFloorPillars_
std::map< std::string, std::vector< QuantLib::Period > > cdsPillars_
std::map< std::string, std::vector< QuantLib::Period > > yoyInflationPillars_
Structured analytics error.
Date asof(14, Jun, 2018)
The cube valuation core.