CDS: Imply spreads from hazard rates

Following examples, as e.g. mentioned in another thread I was able to imply a risky curve / survival probabilities from quoted CDS spreads.

The spreads are taken from Markit and the risk-free curve bootstrapped from Markit IR curves (e.g. markit-com - news - InterestRates_EUR_20200410.zip)

Now I wonder whether there is an easy way to do the reverse: Supply a risky curve, i.e. survival probabilities, and imply the CDS spreads from it and the risk-free curve. Any example or test-case that may point me to the right direction?

EDIT: The ISDA C library function would be JpmcdsCdsParSpreads in cds.h for what it is worth.

Also, about bootstrapping the risk-free curve: I set up the curve definition myself and get quite close to the reference model from this site. Judging from your paper on CDS pricing, I wonder whether you also have a 1:1 implementation of the ISDA model bootstrapper in the Strata OpenSource version?

EDIT: I found IsdaCompliantDiscountCurveCalibratorTest which showed how to exactly replicate the risk free curve discount factors as given by the benchmark ISDA C library.

So my approach so far is the following to bootstrap survival probabilities from par spreads:

// MarketData yieldCvMd = ...
// IsdaCreditCurveDefinition yieldCv = ...
// StandardId legalEntity = ...
// LocalData valData = ...

val refData = ReferenceData.standard();
val calibrator = IsdaCompliantDiscountCurveCalibrator.standard();
val creditDf = calibrator.calibrate(yieldCv, yieldCvMd);

// MarketData spreadMarketData = ...
// List<CdsIsdaCreditCurveNode> nodes = ...
// val spreadCv = IsdaCreditCurveDefinition.of(... nodes ...)

val ratesProvider = ImmutableCreditRatesProvider.builder()
        .valuationDate(valDate)
        .discountCurves(ImmutableMap.of(EUR, creditDf))
        .recoveryRateCurves(ImmutableMap.of(legalEntity, ConstantRecoveryRates.of(legalEntity, valDate, 0.40)))
        .build();
val creditCurveCalibrator = new SimpleCreditCurveCalibrator(AccrualOnDefaultFormula.ORIGINAL_ISDA);
val spProvider = creditCurveCalibrator.calibrate(spreadCv, spreadMarketData, ratesProvider, refData);

Then I use the survival probabilities in a new CreditRatesProvider to price the calibration instruments resolved from the nodes list:

List<ResolvedCdsTrade> resolvedCdsTrades = nodes.stream()
        .map(node -> node.trade(1_000_000, spreadMarketData, refData))
        .map(trade -> trade.getUnderlyingTrade().resolve(refData))
        .collect(Collectors.toList());
IsdaCdsProductPricer productPricer = new IsdaCdsProductPricer(AccrualOnDefaultFormula.ORIGINAL_ISDA);
val fullRatesProvider = ratesProvider.toBuilder()
        .creditCurves(ImmutableMap.of(Pair.of(legalEntity, EUR), spProvider))
        .build();
resolvedCdsTrades.forEach(trade -> {
    val spread = productPricer.parSpread(trade.getProduct(), fullRatesProvider, curveDate, refData);
    val pv = productPricer.presentValue(trade.getProduct(), fullRatesProvider, curveDate, PriceType.CLEAN, refData);
});

With that, I would expect PVs around zero, however:

CDS[2020-06-21] -> EUR -5924.592531981477
CDS[2020-12-21] -> EUR -9704.891994688198
CDS[2021-12-21] -> EUR -16435.035826877473
CDS[2022-12-21] -> EUR -21422.731380332774
CDS[2023-12-21] -> EUR -25434.764999466224
CDS[2024-12-21] -> EUR -27482.126656941047
CDS[2026-12-21] -> EUR -28425.630313105994
CDS[2029-12-21] -> EUR -29563.248504175994

Found the issue:

I accidentally added the same quote during node creation

CdsIsdaCreditCurveNode.ofQuotedSpread(cdsTemplate, id, legalEntity, CONSTANT_QUOTE)

While it had to be

CdsIsdaCreditCurveNode.ofQuotedSpread(cdsTemplate, id, legalEntity, quote[i])

Glad you got this worked out, and sorry we didn’t get back to you earlier!