lballabio / QuantLib

The QuantLib C++ library
http://quantlib.org
Other
5k stars 1.73k forks source link

OAS calculations using nominal instead of percent price #2000

Closed HristoRaykov closed 1 week ago

HristoRaykov commented 1 week ago

I notice that OAS for identical bonds with different notionals give different results. Problem representation.

import QuantLib as ql

val_date = ql.Date(31, 1, 2024)
ql.Settings.instance().evaluationDate = val_date

settlement_days = 0
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)
compounding = ql.Compounded
dc = ql.Thirty360(ql.Thirty360.BondBasis)
bdc = ql.Unadjusted
frequency = ql.Semiannual

ts = ql.FlatForward(val_date, 0.04, dc)
ts_handle = ql.YieldTermStructureHandle(ts)

model = ql.HullWhite(ts_handle)
engine = ql.TreeCallableFixedRateBondEngine(model, 100, ts_handle)

issue_date = ql.Date(31, 1, 2024)
maturity_date = ql.Date(31, 1, 2026)
coupon = 0.06
clean_price = 92.00

schedule = ql.Schedule(issue_date, maturity_date, ql.Period(frequency), calendar, bdc, bdc,
                       ql.DateGeneration.Backward, True)

call_schedule = ql.CallabilitySchedule()

bond100 = ql.CallableFixedRateBond(settlement_days, 100, schedule, [coupon], dc, bdc, 100, issue_date, call_schedule)
bond25 = ql.CallableFixedRateBond(settlement_days, 25, schedule, [coupon], dc, bdc, 100, issue_date, call_schedule)
bond100.setPricingEngine(engine)
bond25.setPricingEngine(engine)

oas100 = bond100.OAS(clean_price, ts_handle, dc, compounding, frequency)
oas25 = bond25.OAS(clean_price, ts_handle, dc, compounding, frequency)
nominal_price = clean_price / 100 * 25
oas25_nom_price = bond25.OAS(nominal_price, ts_handle, dc, compounding, frequency)

print(f"""
OAS(bps) for notinal 100 and price {clean_price:.2f}: {oas100 * 10000} \n
OAS(bps) for notinal 25 and price {clean_price:.2f}:: {oas25 * 10000} \n
OAS(bps) for notinal 25 and price {nominal_price:.2f}:: {oas25_nom_price * 10000} \n
""")

Output:

OAS(bps) for notinal 100 and price 92.00: 650.0283787796501 
OAS(bps) for notinal 25 and price 92.00:: -5682.452474038515 
OAS(bps) for notinal 25 and price 23.00:: 650.0283787796501 

It seems that callable bond OAS / cleanPriceOAS methods does not convert prices to / from nominal value.

If that's the case I prepared pull request with fixes and test case added.

lballabio commented 1 week ago

Fixed by #2001.