|
From: Luigi B. <lui...@gm...> - 2025-07-25 15:04:43
|
Hi—it turns out that the rates coming out of the spreaded curve are not as
expected. If you calculate the original rates from the curve and then add
the spread, as you did in your sample code, you get
>>> zero_rates = [ handle.zeroRate( l.date(), daycount, ql.Compounded,
ql.Monthly ).rate() for ii,l in enumerate(leg) ]
>>> print([r + zspread for r in zero_rates])
[0.054693695527939915, 0.05243448138456887]
while if you extract the same rates from the spreaded curve you get
>>> print([ zspreaded_handle.zeroRate( l.date(), daycount, ql.Compounded,
ql.Monthly ).rate() for ii,l in enumerate(leg) ])
[0.05502806821107864, 0.05226732979489501]
The problem seems to be that the curve and the added spread have two
different day counters (act/360 for the curve and 30/360 for the spread)
and somehow ZeroSpreadedTermStructure fails to account for that. If the
two day counters are the same (either both 30/360 or both act/360) the
problem disappears, which is probably why nobody noticed until now.
May you open an issue on GitHub so someone can pick it up and analyse it
further? Thanks!
Luigi
On Tue, Jun 10, 2025 at 6:17 AM Stats Student <sta...@gm...>
wrote:
> Hi - I have been trying to use the spread from ql.CashFlows.zSpread to
> discount cash flows. I am able to use it in ZeroSpreadedTermStructure and
> get the right numbers.
> But I would like to understand how to replicate the same output by hand.
> The NPV I get is very close, but there is still a small difference which I
> suspect will likely get bigger over longer periods.
>
> Does anyone know what is causing this discrepancy? Thanks in advance.
>
>
>
> zspread: 0.009989751634951987 ( from ql.CashFlows.zSpread )
>
>
> zSpread / ZeroSpreadedTermStructure npv: 198.6780*623718125 (CORRECT)*
>
>
> zSpread manual npv: 198.6780*7379166345*
>
>
>
> ###########################################
>
>
> import QuantLib as ql
>
> daycount = ql.Thirty360(ql.Thirty360.USA)
>
> start = ql.Date(1, 1, 2025)
>
> ql.Settings.instance().evaluationDate = start
>
> rates = ( ['SOFR1D', 4.3564348072], ['SOFR1W', 4.3493027605], ['SOFR1M',
> 4.3238954677], ['SOFR3M', 4.3084328938], ['SOFR6M', 4.2045895664] )
>
> sofr_index = ql.Sofr()
>
> ois_helpers = []
>
> for period, rate in rates:
> tenor = period.replace('SOFR','')
> ois_helpers.append( ql.OISRateHelper(0,
> ql.Period ( tenor ),
>
> ql.QuoteHandle(ql.SimpleQuote(rate/100)),
> sofr_index) )
>
> curve = ql.PiecewiseLogCubicDiscount( start, ois_helpers, ql.Actual360() )
>
> handle = ql.YieldTermStructureHandle( curve )
>
> leg = ql.Leg( [ ql.SimpleCashFlow( 100, ql.Date(1,2,2025) ),
> ql.SimpleCashFlow( 100, ql.Date(1,3,2025) ) ] )
>
> spreaded_npv = 198.6780623718125
>
> zspread = ql.CashFlows.zSpread(leg, spreaded_npv, curve, daycount,
> ql.Compounded, ql.Monthly, True)
>
> print(f"zspread: {zspread}") # zspread: 0.009989751634951987
>
> zspread_quote = ql.SimpleQuote( zspread )
>
> zspreaded_curve = ql.ZeroSpreadedTermStructure( handle,
> ql.QuoteHandle(zspread_quote), ql.Compounded, ql.Monthly, daycount )
>
> zspreaded_handle = ql.YieldTermStructureHandle( zspreaded_curve )
>
> zspreaded_npv = ql.CashFlows.npv(leg, zspreaded_handle, True)
>
> print(f"zSpread ZeroSpreadedTermStructure npv: {zspreaded_npv}") # zSpread
> npv: 198.6780623718125 (MATCH)
>
> zero_rates = [ handle.zeroRate( l.date(), daycount, ql.Compounded,
> ql.Monthly ).rate() for ii,l in enumerate(leg) ]
>
> # zero_rates = [ handle.zeroRate( (ii+1)/12, ql.Compounded, ql.Monthly
> ).rate() for ii,l in enumerate(leg) ] # 198.67814955576728 (BIGGER
> DELTA)
>
> zpread_manual_npv = 100 / (1 + (zero_rates[0] + zspread) / 12) + 100 / ( 1
> + (zero_rates[1] + zspread) / 12 ) ** 2
>
> print(f"zSpread manual npv: {zpread_manual_npv}\n") # manual zSpread npv:
> 198.67807379166345 (*NO MATCH*)
>
>
>
> _______________________________________________
> QuantLib-users mailing list
> Qua...@li...
> https://lists.sourceforge.net/lists/listinfo/quantlib-users
>
|