Skip to content
All library documents

Calibrating CDS Pricing Inputs in QuantLib

Article Quant Q&A · Author: Gustavo Amarante

Summary

The example concerns matching a credit default swap valuation in QuantLib to a spreadsheet and diagnosing a failed upfront basis point calculation. The accepted answer identifies three input mismatches: the CDS coupon should reflect the contract coupon rather than the market spread, the day-count convention should be Actual/360, and the flat hazard rate should be approximated from the spread after adjusting for the recovery rate. It also changes the protection position in the working example.

With those changes, the author reports that the net present value aligns with the spreadsheet, and uses NPV divided by notional as the upfront amount per unit notional. The example does not resolve why the library’s upfront basis point method fails, and its hazard-rate conversion is presented as a practical approximation rather than a general calibration procedure. Results depend on conventions, curve inputs, contract terms, and the pricing engine, so the setup should not be treated as a universal CDS recipe.

Key ideas

  • The CDS coupon input represents the contractual coupon, not necessarily the quoted market spread.
  • Day-count conventions affect premium accrual and valuation, so they must match the reference calculation.
  • The example approximates hazard rate by scaling spread for the loss given default.
  • The author reports matching spreadsheet NPV after correcting trade inputs and conventions.
  • The upfront basis point method remains unresolved in the discussion.

Tags

Full text
# Get upfront bps from a CDS with QuantLib


# Get upfront bps from a CDS with QuantLib












I have an example the pricing of a CDS in Excel and I am trying to match it with QuantLib, in order to get the upfront bps.

Below there is a print of the excel screen where I know all the values are correct. I am adding the python quantlib code that tries to replicate what is in the worksheet. The code runs fine when you ask the NPV at the end, but it yields the wrong value. But when I ask for the upfrontBPS the code doesn't even compute a value for it.

What am I missing here?

```
import QuantLib as ql

# Trade parameters
today = ql.Date(22, ql.June, 2018)
ql.Settings.instance().evaluationDate = today

position = ql.Protection.Seller

notional = 10_000_000

spread = 210 / 10_000

recovery_rate = 0.25

calendar = ql.TARGET()

maturity = ql.Date(20, ql.June, 2023)

# Risk-free zero curve
curve_value = {
    ql.Period(1, ql.Months): 2.091,  # 1M
    ql.Period(2, ql.Months): 2.172,  # 2M
    ql.Period(3, ql.Months): 2.335,  # 3M
    ql.Period(6, ql.Months): 2.504,  # 6M
    ql.Period(1, ql.Years): 2.77,    # 1Y
    ql.Period(2, ql.Years): 2.806,   # 2Y
    ql.Period(3, ql.Years): 2.878,   # 3Y
    ql.Period(4, ql.Years): 2.903,   # 4Y
    ql.Period(5, ql.Years): 2.914,   # 5Y
    ql.Period(6, ql.Years): 2.92,    # 6Y
    ql.Period(7, ql.Years): 2.931,   # 7Y
    ql.Period(8, ql.Years): 2.937,   # 8Y
    ql.Period(9, ql.Years): 2.947,   # 9Y
    ql.Period(10, ql.Years): 2.961,  # 10Y
    ql.Period(12, ql.Years): 2.989,  # 12Y
    ql.Period(15, ql.Years): 3.001,  # 15Y
    ql.Period(20, ql.Years): 3,      # 20Y
    ql.Period(25, ql.Years): 2.989,  # 25Y
    ql.Period(30, ql.Years): 2.967,  # 30Y
}

dates = [today + p for p in curve_value.keys()]
zero_quotes = [val/100 for val in curve_value.values()]
curve = ql.YieldTermStructureHandle(ql.ZeroCurve(dates, zero_quotes, ql.Actual365Fixed(), ql.TARGET()))

# Payment Schedule
schedule = ql.Schedule(  # bool endOfMonth, Date firstDate=Date(), Date nextToLastDate=Date()) -> Schedule
    today,  # Start date
    maturity,  # last date
    ql.Period(ql.Quarterly),  # Frequency of payments
    calendar,  # Calendar
    ql.Following,  # Business Day Payment Convention
    ql.Unadjusted,  # Termination Date Convention
    ql.DateGeneration.TwentiethIMM,  # Rule for date generation
    False,  # end of month convention
)

# CDS Contract
cds = ql.CreditDefaultSwap(
    position,  # Buyer or seller of protection
    notional,  # Real notional
    spread,  # Rate spread
    schedule,  # Schedule schedule
    ql.Following,  # BusinessDayConvention paymentConvention
    ql.Actual365Fixed(),  # DayCounter dayCounter
)

hazard_curve = ql.FlatHazardRate(
    2,  # settlementDays
    calendar,  # Calendar
    ql.QuoteHandle(ql.SimpleQuote(spread)),
    ql.Actual365Fixed(),
)

probability = ql.DefaultProbabilityTermStructureHandle(hazard_curve)

engine = ql.MidPointCdsEngine(
    probability,
    recovery_rate,
    curve,
)
cds.setPricingEngine(engine)
print(cds.NPV())  # Code works up to here, but yield the wrong value
print(cds.upfrontBPS())  # does not work here
```

## Answer by Gustavo Amarante (score 1, accepted)

https://quant.stackexchange.com/a/79283

I have found the issues. There were 3 of them:

- The CDS instrument declaration was receiving the spread quote instead of the spread of the coupons. It should receive 100bps (contract convention) instead of the current price.

- Daycount convetions changed to Actual360

- The hazard rate object was receving the spread as well. I thought it would turn the spread into in the model hazard rate. But we have to divide the spread by 1 minus the recovery rate.

Below is the full working code and the values match with the ones from the worksheet.

The computation of `upfrontBPS` is still not working, but NPV divided by the notional yields the upfront bps.

```
import QuantLib as ql
import pandas as pd

# Trade parameters
today = ql.Date(22, ql.June, 2018)
ql.Settings.instance().evaluationDate = today

position = ql.Protection.Buyer

notional = 10_000_000

spread = 210 / 10_000

recovery_rate = 0.25

calendar = ql.TARGET()

maturity = ql.Date(20, ql.June, 2023)

# Risk-free zero curve
curve_value = {
    ql.Period(1, ql.Months): 2.091,  # 1M
    ql.Period(2, ql.Months): 2.172,  # 2M
    ql.Period(3, ql.Months): 2.335,  # 3M
    ql.Period(6, ql.Months): 2.504,  # 6M
    ql.Period(1, ql.Years): 2.77,    # 1Y
    ql.Period(2, ql.Years): 2.806,   # 2Y
    ql.Period(3, ql.Years): 2.878,   # 3Y
    ql.Period(4, ql.Years): 2.903,   # 4Y
    ql.Period(5, ql.Years): 2.914,   # 5Y
    ql.Period(6, ql.Years): 2.92,    # 6Y
    ql.Period(7, ql.Years): 2.931,   # 7Y
    ql.Period(8, ql.Years): 2.937,   # 8Y
    ql.Period(9, ql.Years): 2.947,   # 9Y
    ql.Period(10, ql.Years): 2.961,  # 10Y
    ql.Period(12, ql.Years): 2.989,  # 12Y
    ql.Period(15, ql.Years): 3.001,  # 15Y
    ql.Period(20, ql.Years): 3,      # 20Y
    ql.Period(25, ql.Years): 2.989,  # 25Y
    ql.Period(30, ql.Years): 2.967,  # 30Y
}

dates = [today + p for p in curve_value.keys()]
zero_quotes = [val/100 for val in curve_value.values()]
curve = ql.YieldTermStructureHandle(ql.ZeroCurve(dates, zero_quotes, ql.Actual360(), ql.TARGET()))

# Payment Schedule
schedule = ql.Schedule(  # bool endOfMonth, Date firstDate=Date(), Date nextToLastDate=Date()) -> Schedule
    today,  # Start date
    maturity,  # last date
    ql.Period(ql.Quarterly),  # Frequency of payments
    calendar,  # Calendar
    ql.Following,  # Business Day Payment Convention
    ql.Unadjusted,  # Termination Date Convention
    ql.DateGeneration.TwentiethIMM,  # Rule for date generation
    False,  # end of month convention
)

# CDS Contract
cds = ql.CreditDefaultSwap(
    position,  # Buyer or seller of protection
    notional,  # Real notional
    0.01,  # Coupon in bps
    schedule,  # Schedule schedule
    ql.Following,  # BusinessDayConvention paymentConvention
    ql.Actual360(),  # DayCounter dayCounter
)

hazard_curve = ql.FlatHazardRate(
    2,  # settlementDays
    calendar,  # Calendar
    ql.QuoteHandle(ql.SimpleQuote(spread/(1-recovery_rate))),
    ql.Actual360(),
)

probability = ql.DefaultProbabilityTermStructureHandle(hazard_curve)

engine = ql.MidPointCdsEngine(
    probability,
    recovery_rate,
    curve,
)
cds.setPricingEngine(engine)
print(cds.NPV() / notional)
```
```

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.