-2
$\begingroup$

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?

enter image description 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
$\endgroup$
4
  • $\begingroup$ You really need to provide a lot more details. It is unlikely that anyone's going to debug your code for you. Also, CDS daycount is usually Actual / 360. $\endgroup$ Commented May 4 at 0:54
  • $\begingroup$ The code is absolutely self contained. If you copy and paste it, and change upfront bps for NPV, it will run. I cannot imagine any other details that I could put here. $\endgroup$ Commented May 4 at 23:38
  • 1
    $\begingroup$ I have reopened your question. Although, being conscious that there are questions out there where people have fixed other people's QuantLib code, I am not of the view that "Debug my QuantLib code.." is a valid type of question that should merit answers. $\endgroup$
    – Attack68
    Commented May 6 at 16:35
  • $\begingroup$ Thank you @Attack86. I find questions that have a full working example much easier to deal with. It turns opinions and guesses into tangible advice. Hopefully this was not interpreted as a "Please debug my code", I was really looking to understand QuantLib and get advice from those who understand CDS pricing. Thank you for reopening the question. $\endgroup$ Commented May 6 at 19:20

1 Answer 1

1
$\begingroup$

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

  1. 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.
  2. Daycount convetions changed to Actual360
  3. 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)
```
$\endgroup$

Not the answer you're looking for? Browse other questions tagged or ask your own question.