Discounting the Exercise Value of a Deep In-the-Money Callable Bond
Summary
The question examines the BlackCallableFixedRateBondEngine in QuantLib using a zero-coupon callable bond with a call price well below the bond's expected value, a nearly zero volatility, and a flat interest-rate curve. The author expects the bond's value to reduce to the discounted call price when the call is certain to be exercised.
The example reports that QuantLib's result matches an undiscounted Black call value subtracted from the discounted bond value. Supplying the discount factor to the Black formula, or directly discounting the call price to its date, instead produces the expected value. The issue raised is whether the engine omits a discount factor in valuing the embedded call. This is a focused implementation question, not a general derivation or documented resolution; the numerical example alone does not establish whether the discrepancy reflects a bug, a convention, or a misunderstanding of the engine's inputs.
Key ideas
- A callable bond can be analyzed as the underlying bond value minus the value of its embedded call option.
- With negligible volatility and a call price far below the bond value, the example expects the call to be exercised with near certainty.
- The reported engine output differs from the value obtained when the Black formula includes a discount factor.
- The question flags discounting conventions in QuantLib's callable bond engine but does not provide a definitive resolution.
Tags
Full text
# Confusion about results of BlackCallableFixedRateBondEngine in QuantLib
# Confusion about results of BlackCallableFixedRateBondEngine in QuantLib
I have trouble understanding the results of BlackCallableFixedRateBondEngine. E.g. in the following example I would expect the price of the callable bond to be the present value of the strike, i.e. 50/1.05 = 47.619048, as the option is clearly far in the money.
```
import QuantLib as ql
daycount = ql.Thirty360(ql.Thirty360.USA)
valuation_date = ql.Date(25,1,2018)
flat_spot = 0.05
coupon = 0.00
ttm = 2
nominal = 100
strike = 50
time_to_call = 1
black_vol = 1e-10
ql.Settings.instance().evaluationDate = valuation_date
spotDates = [valuation_date, valuation_date + ql.Period(ttm, ql.Years)]
spotRates = [flat_spot, flat_spot]
spotCurve = ql.ZeroCurve(spotDates, spotRates, daycount, ql.TARGET(), ql.Linear(), ql.Compounded, ql.Annual)
spotCurveHandle = ql.YieldTermStructureHandle(spotCurve)
schedule = ql.MakeSchedule(ql.Date(25,1,2018), valuation_date + ql.Period(ttm, ql.Years), ql.Period(1, ql.Years))
callSchedule = ql.CallabilitySchedule()
callSchedule.push_back(ql.Callability(ql.BondPrice(strike,ql.BondPrice.Clean),ql.Callability.Call,valuation_date + ql.Period(time_to_call, ql.Years)))
callable_bond = ql.CallableFixedRateBond (0, nominal, schedule, [coupon], daycount,redemption=nominal,issueDate=valuation_date,paymentConvention=ql.Unadjusted,putCallSchedule=callSchedule)
blackEngine = ql.BlackCallableFixedRateBondEngine(ql.QuoteHandle(ql.SimpleQuote(black_vol)),spotCurveHandle)
callable_bond.setPricingEngine(blackEngine)
print("NPV (black): %f" % callable_bond.NPV())
```
The result of the above is however: NPV (black): 45.464853
I tried to replicate the calculations and I was able to do so, but I have the suspicion that the call to ql.blackFormula is missing the discount.
In fact
```
df = 1/(1+flat_spot)
print("NPV (black, check): %f" % (nominal*df**ttm - ql.blackFormula(ql.Option.Call,strike,nominal*df**ttm/df,0)))
print("NPV (black, corrected?): %f" % (nominal*df**ttm - ql.blackFormula(ql.Option.Call,strike,nominal*df**ttm/df,0,df)))
print("NPV (black, corrected?): %f" % (strike*df**time_to_call))
```
leads to the following results.
```
NPV (black, check): 45.464853
NPV (black, corrected?): 47.619048
NPV (black, corrected?): 47.619048
```
Am I missing something?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.