|
From: Roshan Y. <er....@gm...> - 2024-05-03 07:44:32
|
I am bootstrapping a yield curve using QuantLib. I have implemented a
function to calculate the present value of cash flows for various bonds
using the bootstrapped yield curve. However, I am encountering a difference
in the NPV calculation, particularly at the maturity of the bond. Ideally,
the NPV at the maturity of the bond should be equal to the face value since
it is at par, which in my case is 100. However, the calculated NPV is
slightly different or Is there anything i am missing out. Here's a snippet
of the code and the output:
CODE:
```
> def qlDatetostr(aDate):
> if aDate.month() < 10:
> return str(aDate.dayOfMonth())+'/0'+
> str(aDate.month())+'/'+str(aDate.year())
> else:
> return str(aDate.dayOfMonth())+'/'+
> str(aDate.month())+'/'+str(aDate.year())
>
valuationdate = ql.Date(2, 5, 2011)
> ql.Settings.instance().evaluationDate = valuationdate
> settlement_days = 0
> business_convention = ql.Unadjusted
> end_of_month = False
> coupon_freq = ql.Annual
> facevalue = 100
> depo_maturities = [ql.Period(1, ql.Days),ql.Period(1,
> ql.Months),ql.Period(2, ql.Months),ql.Period(3,
> ql.Months),ql.Period(6,ql.Months),ql.Period(12,ql.Months)]
> depo_rates = [5.2300,5.2310,5.2250,5.2270,5.2280,5.2200]
> # Bond rates
> bond_maturities =
> [ql.Period(2,ql.Years),ql.Period(3,ql.Years),ql.Period(4,ql.Years),ql.Period(5,ql.Years)]
> bond_rates = [5.23,5.24,5.2500,5.2600]
> maturities = depo_maturities+bond_maturities
> calendar = ql.NullCalendar()
> day_count = ql.Actual365Fixed()
> rates = depo_rates+bond_rates
> df_ytm = pd.DataFrame(list(zip(maturities, rates)),
> columns=["Maturities","Curve"],
> index=['']*len(rates))
# Input YTM data
> quotes = [
> (1, ql.Period(1, ql.Days), 5.2300),
> (1, ql.Period(1, ql.Months), 5.2310),
> (1, ql.Period(2, ql.Months), 5.2250),
> (1, ql.Period(3, ql.Months), 5.2270),
> (1, ql.Period(6, ql.Months), 5.2280),
> (1, ql.Period(1, ql.Years), 5.2200),
> (1, ql.Period(2, ql.Years), 5.2983),
> (1, ql.Period(3, ql.Years), 5.3086),
> (1, ql.Period(4, ql.Years), 5.2500),
> (1, ql.Period(5, ql.Years), 5.2600),
> ]
depo_helpers =
> [ql.DepositRateHelper(ql.QuoteHandle(ql.SimpleQuote(r/100)),m,settlement_days,calendar,business_convention,end_of_month,day_count)
> for r, m in zip(depo_rates, depo_maturities)]
bond_helpers = []
> for r, m in zip(bond_rates, bond_maturities):
> termination_date = valuationdate + m
> schedule = ql.Schedule(valuationdate,
> termination_date,
> ql.Period(coupon_freq),
> calendar,
> business_convention,
> business_convention,
> ql.DateGeneration.Backward,
> end_of_month)
> bond_helper =
> ql.FixedRateBondHelper(ql.QuoteHandle(ql.SimpleQuote(facevalue)),settlement_days,facevalue,schedule,[r/100],day_count,business_convention,)
> bond_helpers.append(bond_helper)
rate_helpers = depo_helpers+bond_helpers
> yc_linearzero =
> ql.PiecewiseLinearZero(valuationdate,rate_helpers,day_count)
> scheduledates = [calendar.advance(valuationdate,i[1]) for i in quotes]
>
> def
> get_val_working(scheduledates,yc_linearzero,facevalue=100,daycount=ql.Actual365Fixed()):
> ref_date = yc_linearzero.referenceDate()
>
> leg_df=pd.DataFrame(columns=['bond_name','start_date','end_date','days','yearfrac','rate','cashflow','discountFactor','calc_df','zerorate',"npv","cf_npv","cf_cum_npv","cum_npv",'eq_rate','calc_yield'])
calendar = ql.NullCalendar()
> settlement_days = 0
> facevalue = 100
> ctr = 0
> curve_handle = ql.RelinkableYieldTermStructureHandle()
> bondEngine = ql.DiscountingBondEngine(curve_handle)
> for adate in scheduledates:
> schedule = ql.Schedule(ref_date,adate, ql.Period(ql.Semiannual),
> calendar,ql.Unadjusted, ql.Unadjusted, ql.DateGeneration.Forward,False)
> coupon
> =[round(yc_linearzero.zeroRate(adate,ql.Actual365Fixed(),ql.Continuous).rate(),6)]
> bond =
> ql.FixedRateBond(settlement_days,facevalue,schedule,coupon,daycount)
> bond_name = f"bond {schedule.endDate()} {coupon}%"
> # flat_forward =
> ql.FlatForward(0,ql.NullCalendar(),coupon[0],ql.Thirty360(),ql.Compounded,ql.Annual)
> curve_handle.linkTo(yc_linearzero)
> bond.setPricingEngine(bondEngine)
> cum_npv = 0
> cf_cum_npv = 0
> for index, cf in enumerate(bond.cashflows()):
> date = schedule.previousDate(cf.date()) if
> schedule.startDate() < cf.date() else cf.date()
> nxtdate = cf.date()
> # ql.Settings.instance().evaluationDate = date
> yearfrac = day_count.yearFraction(ref_date,cf.date())
> cf_amount = cf.amount()
> days = day_count.dayCount(ref_date,cf.date())
> zerorate = yc_linearzero.zeroRate(yearfrac,ql.Continuous)
> eq_rate =
> zerorate.equivalentRate(day_count,ql.Compounded,ql.Semiannual,ref_date,cf.date())
> discountFactor = zerorate.discountFactor(ref_date,cf.date())
> # df_calc = 1/((1+zerorate.rate())**yearfrac)
> df_calc =
> round(np.exp(-day_count.yearFraction(ref_date,cf.date()) *
> zerorate.rate()), 8)
> calc_yield = 1/(discountFactor**(1/yearfrac))-1
> npv = df_calc*cf.amount()
> cf_npv = ql.CashFlows_npv([cf],curve_handle,True)
> cum_npv = cum_npv+npv
> cf_cum_npv = cf_cum_npv+cf_npv
>
> row={"bond_name":bond_name,'start_date':qlDatetostr(date),'end_date':qlDatetostr(nxtdate),'days':days,'yearfrac':yearfrac,'rate':coupon,
> 'cashflow':cf_amount,'discountFactor':discountFactor,'calc_df':df_calc,'calc_yield':calc_yield,'zerorate':zerorate.rate(),'npv':npv,"cf_npv":cf_npv,"cf_cum_npv":cf_cum_npv,"cum_npv":cum_npv,'eq_rate':eq_rate.rate()}
> leg_df = leg_df.append(row,ignore_index=True)
> if ctr == len(df_ytm):
> break
> else:
> ctr = ctr+1
> return leg_df
> valuation = get_val_working(scheduledates,yc_linearzero)
print(valuation.tail(11))
which gives:
>OUTPUT:
bond_name start_date end_date days yearfrac rate cashflow discountFactor
calc_df zerorate npv cf_npv cf_cum_npv cum_npv eq_rate calc_yield
34 bond May 2nd, 2016 [0.051283]% 02-05-2011 02-11-2011 184 0.504109589
[0.051283] 2.585225205 0.974321893 0.97432189 0.051602954 2.518841508
2.518841517 2.518841517 2.518841508 0.052274433 0.052957587
35 bond May 2nd, 2016 [0.051283]% 02-11-2011 02-05-2012 366 1.002739726
[0.051283] 2.557124932 0.950260502 0.9502605 0.050879723 2.429934816
2.42993482 4.948776337 4.948776324 0.051532433 0.052196331
36 bond May 2nd, 2016 [0.051283]% 02-05-2012 02-11-2012 550 1.506849315
[0.051283] 2.585225205 0.926127394 0.92612739 0.050929763 2.394247872
2.394247883 7.34302422 7.343024196 0.051583763 0.052248984
37 bond May 2nd, 2016 [0.051283]% 02-11-2012 02-05-2013 731 2.002739726
[0.051283] 2.543074795 0.902941381 0.90294138 0.050978988 2.296247464
2.296247467 9.639271687 9.639271661 0.051634258 0.052300782
38 bond May 2nd, 2016 [0.051283]% 02-05-2013 02-11-2013 915 2.506849315
[0.051283] 2.585225205 0.879921283 0.87992128 0.051029324 2.274794672
2.274794679 11.91406637 11.91406633 0.051685895 0.052353753
39 bond May 2nd, 2016 [0.051283]% 02-11-2013 02-05-2014 1096 3.002739726
[0.051283] 2.543074795 0.857806728 0.85780673 0.051078841 2.181466674
2.181466668 14.09553303 14.09553301 0.051736691 0.052405862
40 bond May 2nd, 2016 [0.051283]% 02-05-2014 02-11-2014 1280 3.506849315
[0.051283] 2.585225205 0.835850894 0.83585089 0.051129952 2.160862789
2.160862798 16.25639583 16.2563958 0.051789126 0.052459654
41 bond May 2nd, 2016 [0.051283]% 02-11-2014 02-05-2015 1461 4.002739726
[0.051283] 2.543074795 0.814760443 0.81476044 0.051180231 2.071996739
2.071996747 18.32839258 18.32839253 0.051840707 0.052512571
42 bond May 2nd, 2016 [0.051283]% 02-05-2015 02-11-2015 1645 4.506849315
[0.051283] 2.585225205 0.793823805 0.79382381 0.051231744 2.052213322
2.05221331 20.38060589 20.38060586 0.051893556 0.052566791
43 bond May 2nd, 2016 [0.051283]% 02-11-2015 02-05-2016 1827 5.005479452
[0.051283] 2.557124932 0.773604524 0.77360452 0.051282697 1.978203405
1.978203416 22.35880931 22.35880926 0.051945832 0.052620424
44 bond May 2nd, 2016 [0.051283]% 02-11-2015 02-05-2016 1827 5.005479452
[0.051283] *100* 0.773604524 0.77360452 0.051282697 77.360452 77.36045243
99.71926174 99.71926126 0.051945832 0.052620424
|