Valuation of Bonds with Credit Spreads in QuantLib Python

Goutham Balaraman

In an earlier example on pricing fixed rate bonds I demonstrated how to construct and value bonds using the given yield curve. In this example, let us take a look at valuing bonds with credit spreads. We will show how to add credit spreads to the give yield curve using different approaches.

As usual, let us start by importing the QuantLib library and pick a valuation date and set the calculation instance evaluation date.


In [1]:
import QuantLib as ql
calc_date = ql.Date(26, 7, 2016)
ql.Settings.instance().evaluationDate = calc_date

For simplicity, let us imagine that the treasury yield curve is flat. This makes it easier to construct the yield curve easily. This also allows us to directly shock the yield curve, and provides a validation for the more general treatment of shocks on yield curve.


In [2]:
flat_rate = ql.SimpleQuote(0.0015)
rate_handle = ql.QuoteHandle(flat_rate)
day_count = ql.Actual360()
calendar = ql.UnitedStates()
ts_yield = ql.FlatForward(calc_date, rate_handle, day_count)
ts_handle = ql.YieldTermStructureHandle(ts_yield)

Now let us construct the bond itself. We do that by first constructing the schedule, and then passing the schedule into the bond.


In [3]:
issue_date = ql.Date(15, 7, 2016)
maturity_date = ql.Date(15, 7, 2021)
tenor = ql.Period(ql.Semiannual)
calendar = ql.UnitedStates()
bussiness_convention = ql.Unadjusted
date_generation = ql.DateGeneration.Backward
month_end = False
schedule = ql.Schedule (issue_date, maturity_date, 
                        tenor, calendar, 
                        bussiness_convention,
                        bussiness_convention, 
                        date_generation, 
                        month_end)

In [4]:
settlement_days = 2
day_count = ql.Thirty360()
coupon_rate = .03
coupons = [coupon_rate]

# Now lets construct the FixedRateBond
settlement_days = 0
face_value = 100
fixed_rate_bond = ql.FixedRateBond(
    settlement_days, 
    face_value, 
    schedule, 
    coupons, 
    day_count)

Now that we have the fixed_rate_bond object, we can create a DiscountingBondEngine and value the bond.


In [5]:
bond_engine = ql.DiscountingBondEngine(ts_handle)
fixed_rate_bond.setPricingEngine(bond_engine)
fixed_rate_bond.NPV()


Out[5]:
114.18461651948999

So far, we have valued the bond under the treasury yield curve and have not incorporated the credit spreads. Let us assume that the market prices this bond with a 50BP spread on top of the treasury yield curve. Now we can, in this case, directly shock the flat_rate used in the yield term structure. Let us see what the value is:


In [6]:
flat_rate.setValue(0.0065)
fixed_rate_bond.NPV()


Out[6]:
111.5097766266561

Above we shocked the flat_rate and since the yield term structure is an Observer observing the Observable flat_rate, we could just shock the rate, and QuantLib behind the scenes recalculates all the Observers. Though, this approach is not always viable, in cases such as a bootstrapped bond curve. So let us look at two different approaches that can be used. Before we do that, we need to reset the flat_rate back to what it was.


In [7]:
flat_rate.setValue(0.0015)
fixed_rate_bond.NPV()


Out[7]:
114.18461651948999

Parallel Shift of the Yield Curve

The whole yield curve can be shifted up and down, and the bond revalued with the help of the ZeroSpreadedTermStructure. The constructor takes the yield curve and the spread as argument.


In [8]:
spread1 = ql.SimpleQuote(0.0050)
spread_handle1 = ql.QuoteHandle(spread1)
ts_spreaded1 = ql.ZeroSpreadedTermStructure(ts_handle, spread_handle1)
ts_spreaded_handle1 = ql.YieldTermStructureHandle(ts_spreaded1)

bond_engine = ql.DiscountingBondEngine(ts_spreaded_handle1)
fixed_rate_bond.setPricingEngine(bond_engine)

# Finally the price
fixed_rate_bond.NPV()


Out[8]:
111.50977662665609

Once we have constructed the spreaded term structure, it is rather easy to value for other spreads. All we need to do is change the SimpleQuote object spread1 here.


In [9]:
spread1.setValue(0.01)
fixed_rate_bond.NPV()


Out[9]:
108.89999943320038

Non-Parallel Shift of the Yield Curve

The above method allows only for parallel shift of the yield curve. The SpreadedLinearZeroInterpolatedTermStructure class allows for non parallel shock. First, let us mimic a parallel shift using this class. For the constructor, we need to pass the yield term structure that we wish to shift, and the a list of spreads and a list of the corresponding dates.


In [10]:
spread21 = ql.SimpleQuote(0.0050)
spread22 = ql.SimpleQuote(0.0050)
start_date = calc_date
end_date = calendar.advance(start_date, ql.Period(50, ql.Years))
ts_spreaded2 = ql.SpreadedLinearZeroInterpolatedTermStructure(
    ts_handle,
    [ql.QuoteHandle(spread21), ql.QuoteHandle(spread22)],
    [start_date, end_date]
)
ts_spreaded_handle2 = ql.YieldTermStructureHandle(ts_spreaded2)

bond_engine = ql.DiscountingBondEngine(ts_spreaded_handle2)
fixed_rate_bond.setPricingEngine(bond_engine)

# Finally the price
fixed_rate_bond.NPV()


Out[10]:
111.50977662665609

Here, once again we can change the value of spread2 to value for other shocks.


In [11]:
spread21.setValue(0.01)
spread22.setValue(0.01)
fixed_rate_bond.NPV()


Out[11]:
108.89999943320038

We can easily do non-parallel shifts just by shocking one end.


In [12]:
spread21.setValue(0.005)
spread22.setValue(0.01)
fixed_rate_bond.NPV()


Out[12]:
111.25358792334083

The SpreadedLinearZeroInterpolatedTermStructure is a very powerful class and can be used to implement key-rate duration calculations.