Skip to content
All library documents

Why Credit Spread Sensitivity Can Be Small in a Convertible Bond Model

Article Quant Q&A · Author: wanna_be_quant

Summary

The document explains why a callable convertible bond’s value may respond only weakly to changes in its credit spread when priced with QuantLib’s binomial convertible engine. The setup mistakenly calculates the conversion ratio using a note face value of 1,000, although the library constructs the convertible with a face value of 100. This makes conversion far more attractive than intended.

The engine uses the Tsiveriotis–Fernandes approach, in which converted nodes are discounted at the risk-free rate and unconverted nodes at a risky rate that reflects credit spread. Because the incorrect conversion ratio leaves the option in the money across many tree nodes, much of the value is discounted at the risk-free rate, muting credit spread sensitivity. The answer says to align the conversion ratio with the library’s face value. This diagnosis concerns the specific model setup; it does not establish how other convertible bond engines handle credit risk.

Key ideas

  • The QuantLib convertible bond setup uses a face value of 100 in the described case.
  • A conversion ratio based on a different face value can make the conversion option incorrectly attractive.
  • The Tsiveriotis–Fernandes approach discounts converted and unconverted nodes using different rates.
  • When many nodes are converted, credit spread changes have less influence on the modeled bond value.

Tags

Full text
# Convertible Callable Bond - Straight Bond Credit Spread


# Convertible Callable Bond - Straight Bond Credit Spread












I am pricing a callable convertible bond using python and QuantLib. Please see the block of code I am using below:

```
import numpy as np
import pandas as pd
import QuantLib as ql
import matplotlib.pyplot as plt

issue_date    = ql.Date(21, 5, 2025)
maturity_date = ql.Date(21, 5, 2033)
valuation_date = ql.Date(31,12, 2025)
settlement_days = 2

notional = 32_000_000
face_value_per_note = 1000 # assumption that notes are issued in multiples of 1000

coupon = 0.13
current_stock_price = 261.95
price_volatility = 0.25
credit_spread = 0.0865
risk_free_rate = 0.045

conversion_price = 298  
conversion_ratio = face_value_per_note / conversion_price  # shares per 1000 note

steps_binomial = 1000

ql.Settings.instance().evaluationDate = valuation_date
calendar = ql.UnitedKingdom()    
convention = ql.Following
day_count  = ql.Actual365Fixed()

first_coupon = calendar.adjust(ql.Date(30, 6, 2025), convention)

schedule = ql.Schedule(issue_date,
                    maturity_date,
                    ql.Period(ql.Quarterly),
                    calendar,
                    convention,
                    convention,
                    ql.DateGeneration.Forward,
                    True,            
                    first_coupon,    
                    ql.Date()        
                    )
schedule_df = pd.DataFrame({'dates':list(schedule)})
schedule_df

conversion_exercise = ql.AmericanExercise(earliestDate = issue_date,
                                        latestDate = calendar.adjust(issue_date + ql.Period(8, ql.Years), convention))

issuer_call_start = calendar.adjust(issue_date + ql.Period(3, ql.Years), 
                                    convention)
callability = ql.CallabilitySchedule()
for d in schedule:
    if d >= issuer_call_start and d < maturity_date:
        # print(d)
        callability.append(ql.Callability(ql.BondPrice(100, ql.BondPrice.Clean), 
                                        ql.Callability.Call, d))
        
risk_free_rate_curve = ql.FlatForward(valuation_date, risk_free_rate, day_count)
risk_free_rate_term_structure = ql.YieldTermStructureHandle(risk_free_rate_curve)

volatility_term_structure = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(valuation_date,
                                                                               calendar, 
                                                                               price_volatility, 
                                                                               day_count)
                                                           )

dividend_term_structure = ql.YieldTermStructureHandle(ql.FlatForward(valuation_date, 
                                                                     0, 
                                                                     day_count)
                                                      )

initial_stock_price = ql.QuoteHandle(ql.SimpleQuote(current_stock_price))
black_scholes_process = ql.BlackScholesMertonProcess(initial_stock_price, 
                                                    dividend_term_structure, 
                                                    risk_free_rate_term_structure, 
                                                    volatility_term_structure)

credit_spread_ = ql.QuoteHandle(ql.SimpleQuote(credit_spread))

bond = ql.ConvertibleFixedCouponBond(conversion_exercise,
                                    conversion_ratio,
                                    callability,
                                    issue_date,
                                    settlement_days,
                                    [coupon],
                                    day_count,
                                    schedule,
                                    100)

dividend_schedule = ql.DividendSchedule() 

engine = ql.BinomialConvertibleEngine(black_scholes_process,
                                      "crr",
                                      steps_binomial,
                                      credit_spread_,
                                      dividend_schedule
                                      )

bond.setPricingEngine(engine)

npv_per_note   = bond.NPV()
clean_per_note = bond.cleanPrice()
dirty_per_note = bond.dirtyPrice()
accrued_note   = bond.accruedAmount()
total_npv = npv_per_note * notional/face_value_per_note
total_npv
```

I have wrapped the credit spread that flows into the `BinomialConvertibleEngine` as a SimpleQuote:

```
credit_spread_ = ql.QuoteHandle(ql.SimpleQuote(credit_spread))
```

assuming that the engine understands that this is the credit spread of the straight bond. However, when I check the sensitivity of the total NPV of the Callable Convertible Bond to the straight bond's credit spread I see that even at extreme values there is not material difference and I feel that this is quite weird.

Could it be that there is something wrong with my assumption of the credit spread, i.e. that the engine understands that this is the credit spread of the straight bond?

## Answer by Luigi Ballabio (score 2, accepted)

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

You have a problem in the setup of the bond: the library creates the convertible with a face value of 100 (currently there's no way to specify another value) but you're calculating the conversion ratio based on a face value of 1000.

The wrong ratio causes the conversion option to be way in the money, and this in turn results in the credit spread to have little to ne effect: in the Tsiveriotis-Fernandes model that the engine uses, discounting on the binomial tree is done (I'm simplifying, you'll find more details in the paper, but this is the rough idea) using the risk-free rate for converted nodes and the risky rate for non-converted ones. Since your conversion option is in the money on most nodes, the credit spread is not included in the discounting and has little effect on the final price.

Settings your `face_value_per_note` to 100 will give you a correct price and the dependency on the spread you expect.

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.