Skip to content
All library documents

Callable Bond Effective Duration and Modified Duration Differences

Article Quant Q&A · Author: Eduardo

Summary

The document examines why QuantLib can report a callable bond’s effective duration as higher than its modified duration. It describes a setup using a Hull–White pricing engine, a callable fixed-rate bond, and a rate curve, then reports that effective duration remained slightly higher in the example. The author also compares the library’s clean-price calculation with a manual calculation using dirty prices; the latter narrows the gap but does not eliminate it.

The response distinguishes the assumptions behind the measures. Modified duration holds the principal cash flow fixed under rate changes, while effective duration recalculates the bond price under rate bumps and can reflect changes in call behavior. The answer asserts that this flexibility should make effective duration no greater than modified duration. The example’s contrary result is not resolved, and the response gives no diagnosis of the code, bump convention, or numerical effects, so it does not establish which implementation detail caused the discrepancy.

Key ideas

  • Modified duration treats principal cash flows as unchanged when rates move.
  • Effective duration reprices the callable bond under rate changes and may reflect altered call behavior.
  • The example reports effective duration above modified duration, even after a dirty-price adjustment.
  • The response says effective duration should not exceed modified duration but does not resolve the example’s discrepancy.

Tags

Full text
# Can effective duration > modifed duration for a callable bond? I get that in QuantLib


# Can effective duration > modifed duration for a callable bond? I get that in QuantLib












I am using QuantLib to create a CallableFixedRateBond. I set up the HullWhite model as the pricing engine and compute effective duration and modified duration. Given the price of the bond is heavily discounted (thus unlikely to get called), my expectation is that EffDuration would be equal or perhaps slightly smaller than modified duration. It comes out higher. Here's the code:

```
# Create Callable Fixed Rate Bond

# Define the payment schedule
effective_date = ql.Date("2021-05-20","yyyy-mm-dd")
maturity_date = ql.Date("2029-06-01","yyyy-mm-dd")
first_coupon_date = ql.Date("2021-12-01","yyyy-mm-dd")
end_of_month = True
frequency = ql.Period('6M')
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
holiday_convention = ql.Unadjusted
date_generation_rule = ql.DateGeneration.Backward
schedule = ql.Schedule(effective_date, maturity_date, frequency, calendar, holiday_convention, holiday_convention, date_generation_rule, end_of_month, first_coupon_date)

# Define call schedule
call_schedule = ql.CallabilitySchedule()
calls = [
    ("2029-06-01", 100)
    ]
for entry in calls:
  call_schedule.append(ql.Callability(ql.BondPrice(entry[1], ql.BondPrice.Clean),ql.Callability.Call, ql.Date(entry[0],"yyyy-mm-dd")))

# Create the bond
settlement_days = 2
face_amount = 100
coupons = [0.04875]
daycount = ql.Thirty360(ql.Thirty360.ISDA)
redemption = 100
callable_bond = ql.CallableFixedRateBond(settlement_days, face_amount, schedule, coupons, daycount, holiday_convention, redemption, effective_date, call_schedule)

# Create a flat curve, prep for bumping
base_curve = ql.FlatForward(2, ql.UnitedStates(ql.UnitedStates.GovernmentBond), 0.05, ql.Thirty360(ql.Thirty360.ISDA))
bump = ql.RelinkableQuoteHandle(ql.SimpleQuote(0.0))
curve = ql.ZeroSpreadedTermStructure(ql.YieldTermStructureHandle(base_curve), bump)
curve = ql.YieldTermStructureHandle(curve)

# Create HW model
alpha = 0.03
sigma = 0.012
hw0 = ql.HullWhite(curve, alpha, sigma)

# Create Pricing Engine
settlement_date = ql.Date("2023-11-30","yyyy-mm-dd")
grid_steps = int((maturity_date - settlement_date) / 30)
engine = ql.TreeCallableFixedRateBondEngine(hw0, grid_steps)
callable_bond.setPricingEngine(engine);

# Compute OAS
clean_price = 70.926
oas = callable_bond.OAS(clean_price, curve, daycount, ql.Compounded, ql.Semiannual, settlement_date)
print(f"OAS = {oas * 10_000:,.2f} bp")

# Compute Effective Duration using ql function
spread = 0.001
eff_dur = callable_bond.effectiveDuration(oas, curve, daycount, ql.Compounded, ql.Semiannual, spread)
print("\n*** Using QL function ***\n")
print(f"Eff Dur = {eff_dur:,.4f}")

# Compute mod duration
bond_yield = callable_bond.bondYield(clean_price, daycount, ql.Compounded, ql.Semiannual, settlement_date)
ir = ql.InterestRate(bond_yield, daycount, ql.Compounded, ql.Semiannual)
mod_dur = ql.BondFunctions.duration(callable_bond, ir, ql.Duration.Modified )
print(f"Mod Dur = {mod_dur:,.4f}")
```

The results are:

`Eff Dur = 4.4045` and `Mod Dur = 4.3499`

A difference of `Mod Dur - Eff Dur = 0.0546`

Looking at the quantlib sourcecode I notice that Effective Duration is computed based on clean prices. When I manually compute it using dirty prices it comes closer to Mod Dur, but still a little higher:

```
# Manually compute Eff Dur using dirty price
accrued = callable_bond.accruedAmount(settlement_date)
base_price = callable_bond.cleanPriceOAS(oas, curve, daycount, ql.Compounded, ql.Semiannual, settlement_date) + accrued
up_price = callable_bond.cleanPriceOAS(oas + spread, curve, daycount, ql.Compounded, ql.Semiannual, settlement_date) + accrued
down_price = callable_bond.cleanPriceOAS(oas - spread, curve, daycount, ql.Compounded, ql.Semiannual, settlement_date) + accrued
eff_dur = (down_price - up_price) / (2 * base_price * spread )
print("\n***Bumping OAS manually, using dirty price***\n")
print(f"Eff Dur = {eff_dur:,.4f}")
```

This gives: `Eff Dur = 4.36629` and `Mod Dur = 4.3499`

A diff of `Mod Dur - Eff Dur = 0.0164`

So an improvement but still higher than Mod Duration. The difference seems small, is this justified by numerical approximation? Any suggestions or comments?

## Answer by joe (score 0)

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

Modified Duration assumes principal cashflow stays the same when rate goes 50bps up/down. That is, call date will not change when rate move. However, effective duration calculation takes into account that call date would change when rate move up/down. Therefore, Mod Duration >= Effective Duration.

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.