Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
Public Member Functions | Private Attributes | List of all members
ParSensitivityConverter Class Reference

ParSensitivityConverter class. More...

#include <orea/engine/parsensitivityanalysis.hpp>

+ Collaboration diagram for ParSensitivityConverter:

Public Member Functions

 ParSensitivityConverter (const ParSensitivityAnalysis::ParContainer &parSensitivities, const std::map< ore::analytics::RiskFactorKey, std::pair< QuantLib::Real, QuantLib::Real > > &shiftSizes)
 
const std::set< ore::analytics::RiskFactorKey > & rawKeys ()
 Inspectors. More...
 
const 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. More...
 
void writeConversionMatrix (ore::data::Report &reportOut) const
 Write the inverse of the transposed Jacobian to the reportOut. More...
 
ParSensitivityAnalysis::ParContainer inverseJacobian () const
 

Private Attributes

std::set< ore::analytics::RiskFactorKeyrawKeys_
 
std::set< ore::analytics::RiskFactorKeyparKeys_
 
QuantLib::SparseMatrix jacobi_transp_inv_
 
boost::numeric::ublas::vector< QuantLib::Real > zeroShifts_
 Vector of absolute zero shift sizes. More...
 
boost::numeric::ublas::vector< QuantLib::Real > parShifts_
 Vector of absolute par shift sizes. More...
 

Detailed Description

ParSensitivityConverter class.

1) Build Jacobi matrix containing sensitivities of par rates (first index) w.r.t. zero shifts (second index) 2) Convert zero rate and optionlet vol sensitivities into par rate/vol sensitivities

Let: p_ij denote curve i's par instrument j (curve may be a discount or an index curve) z_kl denote curve k's zero shift l (curve may be a discount or an index curve) i,k run from 0 to I j,l run from 0 to J

Organisation of the matrix: z_00 z_01 z_02 ... z_0J ... z_10 z_11 ... z_1J ... z_I0 z_I1 ... z_IJ p_00 p_01 p_02 ... p_0J p_10 p_11 ... p_1J ... p_I0 p_I1 ... p_IJ

The curve numbering starts with discount curves (by ccy), followed by index curves (by index name). The number J of par instruments respectively zero shifts can differ between discount and index curves. The number of zero shifts matches the number of par instruments. The Jacobi matrix is therefore quadratic by construction.

Definition at line 174 of file parsensitivityanalysis.hpp.

Constructor & Destructor Documentation

◆ ParSensitivityConverter()

ParSensitivityConverter ( const ParSensitivityAnalysis::ParContainer parSensitivities,
const std::map< ore::analytics::RiskFactorKey, std::pair< QuantLib::Real, QuantLib::Real > > &  shiftSizes 
)

Constructor where parSensitivities is the par rate sensitivities w.r.t. zero shifts shiftSizes gives the absolute zero and par shift sizes for each risk factor key.

Definition at line 572 of file parsensitivityanalysis.cpp.

573 {
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}
std::set< ore::analytics::RiskFactorKey > parKeys_
std::set< ore::analytics::RiskFactorKey > rawKeys_
boost::numeric::ublas::vector< QuantLib::Real > zeroShifts_
Vector of absolute zero shift sizes.
boost::numeric::ublas::vector< QuantLib::Real > parShifts_
Vector of absolute par shift sizes.
#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)
+ Here is the call graph for this function:

Member Function Documentation

◆ rawKeys()

const std::set< ore::analytics::RiskFactorKey > & rawKeys ( )

Inspectors.

Return the set of raw, i.e. zero, risk factor keys The ordering in this set defines the order of the columns in the Jacobi matrix

Definition at line 187 of file parsensitivityanalysis.hpp.

187{ return rawKeys_; }

◆ parKeys()

const std::set< ore::analytics::RiskFactorKey > & parKeys ( )

Return the set of par risk factor keys The ordering in this set defines the order of the rows in the Jacobi matrix

Definition at line 190 of file parsensitivityanalysis.hpp.

190{ return parKeys_; }

◆ convertSensitivity()

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.

Parameters
zeroSensitivitiesarray of zero sensitivities ordered according to rawKeys()
Returns
array of par sensitivities ordered according to parKeys()

Definition at line 781 of file parsensitivityanalysis.cpp.

781 {
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}

◆ writeConversionMatrix()

void writeConversionMatrix ( ore::data::Report reportOut) const

Write the inverse of the transposed Jacobian to the reportOut.

Definition at line 808 of file parsensitivityanalysis.cpp.

808 {
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}
bool close(const Real &t_1, const Real &t_2)
std::string to_string(const LocationInfo &l)
+ Here is the call graph for this function:

◆ inverseJacobian()

ParSensitivityAnalysis::ParContainer inverseJacobian ( ) const

Definition at line 206 of file parsensitivityanalysis.hpp.

207 Size parIdx = 0;
208 for (const auto& parKey : parKeys_) {
209 Size rawIdx = 0;
210 for (const auto& rawKey : rawKeys_) {
211 results[{rawKey, parKey}] = jacobi_transp_inv_(parIdx, rawIdx);
212 rawIdx++;
213 }
214 parIdx++;
215 }
216 return results;
217 }
std::map< std::pair< ore::analytics::RiskFactorKey, ore::analytics::RiskFactorKey >, Real > ParContainer

Member Data Documentation

◆ rawKeys_

std::set<ore::analytics::RiskFactorKey> rawKeys_
private

Definition at line 220 of file parsensitivityanalysis.hpp.

◆ parKeys_

std::set<ore::analytics::RiskFactorKey> parKeys_
private

Definition at line 221 of file parsensitivityanalysis.hpp.

◆ jacobi_transp_inv_

QuantLib::SparseMatrix jacobi_transp_inv_
private

Definition at line 223 of file parsensitivityanalysis.hpp.

◆ zeroShifts_

boost::numeric::ublas::vector<QuantLib::Real> zeroShifts_
private

Vector of absolute zero shift sizes.

Definition at line 225 of file parsensitivityanalysis.hpp.

◆ parShifts_

boost::numeric::ublas::vector<QuantLib::Real> parShifts_
private

Vector of absolute par shift sizes.

Definition at line 227 of file parsensitivityanalysis.hpp.