Skip to content

Commit 2c77a16

Browse files
committed
Merge branch 'feature/QPR-13507' into 'master'
QPR-13507 constituent curve calibration of index cds and index cds options Closes QPR-13507 See merge request qs/oreplus!3038
2 parents c694611 + c11fcb4 commit 2c77a16

13 files changed

Lines changed: 298 additions & 121 deletions

Docs/UserGuide/pricing/pricingengines.tex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,10 @@ \subsection{Product Type: IndexCreditDefaultSwap}
18231823
\item Curve: Index, Underlying
18241824
\item SensitivityDecomposition: Underlying, NotionalWeighted, LossWeighted, DeltaWeighted
18251825
\item SensitivityTemplate [optional]: the sensitivity template to use
1826+
\item CalibrateUnderlyingCurves [optional]: Only applies when Curve is set to Underlying.
1827+
If true, it apply a spread to the the individual constituent curves to match the index CDS spread.
1828+
This ensures that pricing the index CDS using underlying curves produces the same NPV as pricing with the index curve directly. Defaults to false.
1829+
(See Calibration of default curves for index tranches for details.)
18261830
\end{itemize}
18271831

18281832
\begin{longlisting}
@@ -1835,6 +1839,7 @@ \subsection{Product Type: IndexCreditDefaultSwap}
18351839
<Parameter name="Curve">Index</Parameter>
18361840
<Parameter name="SensitivityDecomposition">DeltaWeighted</Parameter>
18371841
<Parameter name="SensitivityTemplate">IR_Analytical</Parameter>
1842+
<Parameter name="CalibrateUnderlyingCurves">false</Parameter>
18381843
</EngineParameters>
18391844
</Product>
18401845
\end{minted}
@@ -1867,6 +1872,10 @@ \subsection{Product Type: IndexCreditDefaultSwapOption}
18671872
\item FepCurve: Index, Underlying
18681873
\item SensitivityDecomposition: Underlying, NotionalWeighted, LossWeighted, DeltaWeighted
18691874
\item SensitivityTemplate [optional]: the sensitivity template to use
1875+
\item CalibrateUnderlyingCurves [optional]: Only applies when Curve or FepCurve is set to Underlying.
1876+
If true, it apply a spread to the the individual constituent curves to match the index CDS spread.
1877+
This ensures that pricing the index CDS using underlying curves produces the same NPV as pricing with the index curve directly. Defaults to false.
1878+
(See Calibration of default curves for index tranches for details.)
18701879
\end{itemize}
18711880

18721881
\begin{longlisting}
@@ -1898,6 +1907,10 @@ \subsection{Product Type: IndexCreditDefaultSwapOption}
18981907
\item FepCurve: Index, Underlying
18991908
\item SensitivityDecomposition: Underlying, NotionalWeighted, LossWeighted, DeltaWeighted
19001909
\item SensitivityTemplate [optional]: the sensitivity template to use
1910+
\item CalibrateUnderlyingCurves [optional]: Only applies when Curve or FepCurve is set to Underlying.
1911+
If true, it apply a spread to the the individual constituent curves to match the index CDS spread.
1912+
This ensures that pricing the index CDS using underlying curves produces the same NPV as pricing with the index curve directly. Defaults to false.
1913+
(See Calibration of default curves for index tranches for details.)
19011914
\end{itemize}
19021915

19031916
\begin{longlisting}

OREData/ored/marketdata/basecorrelationcurve.cpp

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,7 @@ void BaseCorrelationCurve::buildFromUpfronts(const Date& asof, const BaseCorrela
509509
dpts.push_back(creditCurve->curve());
510510
}
511511

512-
Handle<DefaultProbabilityTermStructure> indexCurve;
513-
Handle<Quote> indexRecovery;
512+
514513
Handle<YieldTermStructure> discountCurve;
515514
// check if curveID has term suffix already (e.g. "RED:ABCDEFGH_5Y"), if so use that, otherwise use term from config
516515
auto p = ore::data::splitCurveIdWithTenor(config.curveID());
@@ -525,12 +524,9 @@ void BaseCorrelationCurve::buildFromUpfronts(const Date& asof, const BaseCorrela
525524
QL_REQUIRE(indexCreditCurve != nullptr,
526525
"Can not imply base correlation, index credit curve " << indexNameWithTerm << " missing");
527526
discountCurve = indexCreditCurve->rateCurve();
528-
indexCurve = indexCreditCurve->curve();
529-
indexRecovery = indexCreditCurve->recovery();
530-
527+
531528
if (config.calibrateConstituentsToIndexSpread()) {
532-
auto curveCalibration = ext::make_shared<QuantExt::CreditIndexConstituentCurveCalibration>(
533-
config.startDate(), term, config.indexSpread(), indexRecovery, indexCurve, discountCurve);
529+
auto curveCalibration = ext::make_shared<QuantExt::CreditIndexConstituentCurveCalibration>(indexCreditCurve);
534530

535531
auto calibrationResults = curveCalibration->calibratedCurves(
536532
basketData.remainingNames, basketData.remainingWeights, dpts, recoveryRates);

OREData/ored/marketdata/defaultcurve.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,11 +336,12 @@ void DefaultCurve::buildCdsCurve(const std::string& curveID, const DefaultCurveC
336336
refData.tenor = Period(cdsConv->frequency());
337337
refData.calendar = cdsConv->calendar();
338338
refData.convention = cdsConv->paymentConvention();
339-
refData.termConvention = cdsConv->paymentConvention();
339+
refData.termConvention = Unadjusted;
340340
refData.rule = cdsConv->rule();
341341
refData.payConvention = cdsConv->paymentConvention();
342342
refData.dayCounter = cdsConv->dayCounter();
343-
refData.lastPeriodDayCounter = cdsConv->lastPeriodDayCounter();
343+
if (cdsConv->lastPeriodDayCounter() != DayCounter())
344+
refData.lastPeriodDayCounter = cdsConv->lastPeriodDayCounter();
344345
refData.cashSettlementDays = cdsConv->upfrontSettlementDays();
345346

346347
// If the configuration instructs us to imply a default from the market data, we do it here.

OREData/ored/portfolio/builders/indexcreditdefaultswap.cpp

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
1717
*/
1818

19-
#include <ored/portfolio/builders/indexcreditdefaultswap.hpp>
2019
#include <qle/pricingengines/midpointindexcdsengine.hpp>
20+
#include <qle/utilities/creditindexconstituentcurvecalibration.hpp>
2121

22-
#include <ored/utilities/marketdata.hpp>
22+
#include <ored/portfolio/builders/indexcreditdefaultswap.hpp>
2323
#include <ored/utilities/log.hpp>
24+
#include <ored/utilities/marketdata.hpp>
2425
#include <ored/utilities/to_string.hpp>
2526

2627
#include <boost/make_shared.hpp>
@@ -36,23 +37,29 @@ CreditPortfolioSensitivityDecomposition IndexCreditDefaultSwapEngineBuilder::sen
3637
vector<string> IndexCreditDefaultSwapEngineBuilder::keyImpl(const Currency& ccy, const string& creditCurveId,
3738
const vector<string>& creditCurveIds,
3839
const QuantLib::ext::optional<string>& overrideCurve,
39-
Real recoveryRate, const bool inCcyDiscountCurve) {
40+
const QuantLib::ext::optional<bool>& calibrateConstituentCurvesOverride,
41+
const std::vector<double>& constituentNotional, Real recoveryRate,
42+
const bool inCcyDiscountCurve) {
4043
vector<string> res{ccy.code()};
4144
res.insert(res.end(), creditCurveIds.begin(), creditCurveIds.end());
4245
res.push_back(creditCurveId);
4346
res.push_back(overrideCurve ? *overrideCurve : "");
4447
if (recoveryRate != Null<Real>())
4548
res.push_back(to_string(recoveryRate));
4649
res.push_back(inCcyDiscountCurve ? "1" : "0");
50+
res.push_back(
51+
calibrateConstituentCurvesOverride.has_value() ? (calibrateConstituentCurvesOverride.value() ? "1" : "0") : "");
4752
return res;
4853
}
4954

5055
QuantLib::ext::shared_ptr<PricingEngine> MidPointIndexCdsEngineBuilder::engineImpl(
5156
const Currency& ccy, const string& creditCurveId, const vector<string>& creditCurveIds,
52-
const QuantLib::ext::optional<string>& overrideCurve, Real recoveryRate, const bool inCcyDiscountCurve) {
57+
const QuantLib::ext::optional<string>& overrideCurve,
58+
const QuantLib::ext::optional<bool>& calibrateConstituentCurvesOverride,
59+
const std::vector<double>& constituentNotionals, Real recoveryRate, const bool inCcyDiscountCurve) {
5360

5461
std::string curve = overrideCurve ? *overrideCurve : engineParameter("Curve", {}, false, "Underlying");
55-
62+
5663
if (curve == "Index") {
5764
auto creditCurve = indexCdsDefaultCurve(market_, creditCurveId, configuration(MarketContext::pricing));
5865
Handle<Quote> mktRecovery = market_->recoveryRate(creditCurveId, configuration(MarketContext::pricing));
@@ -70,10 +77,52 @@ QuantLib::ext::shared_ptr<PricingEngine> MidPointIndexCdsEngineBuilder::engineIm
7077
dpts.push_back(tmp->curve());
7178
recovery.push_back(recoveryRate != Null<Real>() ? recoveryRate : tmp2->value());
7279
}
73-
return QuantLib::ext::make_shared<QuantExt::MidPointIndexCdsEngine>(
74-
dpts, recovery,
75-
market_->discountCurve(
76-
ccy.code(), configuration(inCcyDiscountCurve ? MarketContext::irCalibration : MarketContext::pricing)));
80+
auto discountCurve = market_->discountCurve(
81+
ccy.code(), configuration(inCcyDiscountCurve ? MarketContext::irCalibration : MarketContext::pricing));
82+
83+
bool calibrateConstituentCurves =
84+
calibrateConstituentCurvesOverride
85+
? *calibrateConstituentCurvesOverride
86+
: parseBool(engineParameter("CalibrateUnderlyingCurves", {}, false, "false"));
87+
std::string runType = "";
88+
auto it = globalParameters_.find("RunType");
89+
if (it != globalParameters_.end()) {
90+
runType = it->second;
91+
}
92+
calibrateConstituentCurves = calibrateConstituentCurves && runType != "PortfolioAnalyser";
93+
if (calibrateConstituentCurves) {
94+
TLOG("IndexCreditDefaultSwap: Calibrate constituent curves to index spread");
95+
QL_REQUIRE(!creditCurveId.empty(),
96+
"IndexCreditDefaultSwap: cannot calibrate constituent curves to index spread "
97+
"if index credit curve ID is not set");
98+
auto indexCreditCurve = indexCdsDefaultCurve(market_, creditCurveId, configuration(MarketContext::pricing));
99+
QL_REQUIRE(indexCreditCurve->refData().startDate != Null<Date>(),
100+
"IndexCreditDefaultSwap: cannot calibrate constituent curves to index spread "
101+
"if index credit curve start date is not set, please check index credit curve configuration");
102+
QL_REQUIRE(indexCreditCurve->refData().indexTerm != 0 * Days,
103+
"IndexCreditDefaultSwap: cannot calibrate constituent curves to index spread "
104+
"if index credit curve index term is not set, please check index credit curve configuration");
105+
QL_REQUIRE(indexCreditCurve->refData().runningSpread != Null<Real>(),
106+
"IndexCreditDefaultSwap: cannot calibrate constituent curves to index spread "
107+
"if index credit curve running spread is not set, please check index credit curve configuration");
108+
auto curveCalibration = ext::make_shared<QuantExt::CreditIndexConstituentCurveCalibration>(indexCreditCurve);
109+
auto res = curveCalibration->calibratedCurves(creditCurveIds, constituentNotionals, dpts, recovery);
110+
TLOG("Calibration success: " << res.success);
111+
if (res.success) {
112+
TLOG("maturity,marketNPV,impliedNPV,calibrationFactor:");
113+
for (Size i = 0; i < res.marketNpv.size(); ++i) {
114+
TLOG(res.cdsMaturity[i] << res.marketNpv[i] << "," << res.impliedNpv[i] << ","
115+
<< res.calibrationFactor[i]);
116+
}
117+
dpts = res.curves;
118+
} else {
119+
ALOG("IndexCreditDefaultSwap: Calibration of constituent curves to index spread failed, "
120+
"proceeding with non-calibrated "
121+
"curves. Got "
122+
<< res.errorMessage << " continue with non-calibrated curves.");
123+
}
124+
}
125+
return QuantLib::ext::make_shared<QuantExt::MidPointIndexCdsEngine>(dpts, recovery, discountCurve);
77126
} else {
78127
QL_FAIL("MidPointIndexCdsEngineBuilder: Curve Parameter value \""
79128
<< engineParameter("Curve") << "\" not recognised, expected Underlying or Index");

OREData/ored/portfolio/builders/indexcreditdefaultswap.hpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ namespace data {
4040

4141
class IndexCreditDefaultSwapEngineBuilder
4242
: public CachingPricingEngineBuilder<vector<string>, const Currency&, const string&, const vector<string>&,
43-
const QuantLib::ext::optional<string>&, Real, bool> {
43+
const QuantLib::ext::optional<string>&, const QuantLib::ext::optional<bool>&,
44+
const std::vector<double>&, Real, bool> {
4445

4546
public:
4647
CreditPortfolioSensitivityDecomposition sensitivityDecomposition();
@@ -50,8 +51,10 @@ class IndexCreditDefaultSwapEngineBuilder
5051
: CachingEngineBuilder(model, engine, {"IndexCreditDefaultSwap"}) {}
5152

5253
vector<string> keyImpl(const Currency& ccy, const string& creditCurveId, const vector<string>& creditCurveIds,
53-
const QuantLib::ext::optional<string>& overrideCurve, Real recoveryRate = Null<Real>(),
54-
const bool inCcyDiscountCurve = false) override;
54+
const QuantLib::ext::optional<string>& overrideCurve,
55+
const QuantLib::ext::optional<bool>& calibrateConstituentCurvesOverride,
56+
const std::vector<double>& constituentNotionals,
57+
Real recoveryRate = Null<Real>(), const bool inCcyDiscountCurve = false) override;
5558
};
5659

5760
//! Midpoint Engine Builder class for IndexCreditDefaultSwaps
@@ -68,6 +71,8 @@ class MidPointIndexCdsEngineBuilder : public IndexCreditDefaultSwapEngineBuilder
6871
QuantLib::ext::shared_ptr<PricingEngine> engineImpl(const Currency& ccy, const string& creditCurveId,
6972
const vector<string>& creditCurveIds,
7073
const QuantLib::ext::optional<string>& overrideCurve,
74+
const QuantLib::ext::optional<bool>& calibrateConstituentCurvesOverride,
75+
const std::vector<double>& constituentNotionals,
7176
Real recoveryRate = Null<Real>(),
7277
const bool inCcyDiscountCurve = false) override;
7378
};

0 commit comments

Comments
 (0)