Calibrating CDS Pricing Inputs in QuantLib
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.