lballabio / QuantLib

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

CPIBond final cashflow only contains inflation of notional not notional itself #2095

Closed davidt922 closed 1 month ago

davidt922 commented 1 month ago

This is a working code in python for Quantlib >=1.30:

import QuantLib as ql

class Datum:
    def __init__(self, date, rate):
        self.date = date
        self.rate = rate

def main():
    startTime = ql.Date(15,10,2024)

    calendar = ql.UnitedKingdom()
    dayCounter = ql.ActualActual(ql.ActualActual.ISDA)
    convention = ql.ModifiedFollowing

    today = ql.Date(25, ql.November, 2009)
    evaluationDate = calendar.adjust(today)
    ql.Settings.instance().evaluationDate = evaluationDate

    yTS = ql.YieldTermStructureHandle(
        ql.FlatForward(evaluationDate, 0.05, dayCounter))

    from_date = ql.Date(20, ql.July, 2007)
    to_date = ql.Date(20, ql.November, 2009)
    tenor = ql.Period(1, ql.Months)
    rpiSchedule = ql.Schedule(from_date, to_date, tenor, calendar,
                              convention, convention,
                              ql.DateGeneration.Backward, False)

    cpiTS = ql.RelinkableZeroInflationTermStructureHandle()
    inflationIndex = ql.UKRPI(cpiTS)
    fixData = [206.1, 207.3, 208.0, 208.9, 209.7, 210.9,
               209.8, 211.4, 212.1, 214.0, 215.1, 216.8,
               216.5, 217.2, 218.4, 217.7, 216,
               212.9, 210.1, 211.4, 211.3, 211.5,
               212.8, 213.4, 213.4, 213.4, 214.4]

    for i in range(len(fixData)):
        inflationIndex.addFixing(rpiSchedule.dates()[i], fixData[i])

    observationLag = ql.Period(2, ql.Months)
    zciisData = [
        Datum(ql.Date(25, ql.November, 2010), 3.0495),
        Datum(ql.Date(25, ql.November, 2011), 2.93),
        Datum(ql.Date(26, ql.November, 2012), 2.9795),
        Datum(ql.Date(25, ql.November, 2013), 3.029),
        Datum(ql.Date(25, ql.November, 2014), 3.1425),
        Datum(ql.Date(25, ql.November, 2015), 3.211),
        Datum(ql.Date(25, ql.November, 2016), 3.2675),
        Datum(ql.Date(25, ql.November, 2017), 3.3625),
        Datum(ql.Date(25, ql.November, 2018), 3.405),
        Datum(ql.Date(25, ql.November, 2019), 3.48),
        Datum(ql.Date(25, ql.November, 2021), 3.576),
        Datum(ql.Date(25, ql.November, 2024), 3.649),
        Datum(ql.Date(26, ql.November, 2029), 3.751),
        Datum(ql.Date(27, ql.November, 2034), 3.77225),
        Datum(ql.Date(25, ql.November, 2039), 3.77),
        Datum(ql.Date(25, ql.November, 2049), 3.734),
        Datum(ql.Date(25, ql.November, 2059), 3.714)
    ]

    zeroSwapHelpers = []
    for datum in zciisData:
        zeroSwapHelpers.append(
            ql.ZeroCouponInflationSwapHelper(
                ql.QuoteHandle(ql.SimpleQuote(datum.rate / 100.0)), observationLag,
                datum.date, calendar, convention, dayCounter, inflationIndex,
                ql.CPI.AsIndex, yTS))

    cpiTS.linkTo(ql.PiecewiseZeroInflation(
        evaluationDate, calendar, dayCounter, observationLag,
        inflationIndex.frequency(), zciisData[0].rate / 100.0, zeroSwapHelpers))

    notional = 100

    fixedRates = ql.DoubleVector()
    fixedRates.append(0.1)

    fixedDayCounter = ql.Actual365Fixed()
    fixedPaymentConvention = ql.ModifiedFollowing
    fixedPaymentCalendar = ql.UnitedKingdom()
    contractObservationLag = ql.Period(3, ql.Months)
    observationInterpolation = ql.CPI.Flat
    settlementDays = 3
    growthOnly = True

    baseCPI = 214.5  # 206.1
    startDate = ql.Date(2, ql.October, 2007)
    endDate = ql.Date(2, ql.October, 2020)

    fixedRates = [2.05 / 100]
    fixedDayCounter = ql.Actual365Fixed()
    fixedPaymentConvention = ql.ModifiedFollowing

    fixedSchedule = ql.Schedule(startDate, endDate,
                                ql.Period(12, ql.Months), fixedPaymentCalendar,
                                ql.Unadjusted,
                                ql.Unadjusted,
                                ql.DateGeneration.Backward, True)

    bond = ql.CPIBond(settlementDays, notional, growthOnly,
                      baseCPI, contractObservationLag,
                      inflationIndex, observationInterpolation,
                      fixedSchedule, fixedRates, fixedDayCounter)

    bond.setPricingEngine(ql.DiscountingBondEngine(yTS))

    # Print out the cashflows
    for cf in bond.cashflows():
        print(f"Date: {cf.date()}, Amount: {cf.amount()}")

  if __name__ == "__main__":
      main()

When executing you will get the following:

Date: October 2nd, 2008, Amount: 2.074783025193984 Date: October 2nd, 2009, Amount: 2.039487179487179 Date: October 4th, 2010, Amount: 2.10078312627201 Date: October 3rd, 2011, Amount: 2.1610340159526076 Date: October 2nd, 2012, Amount: 2.232253620324762 Date: October 2nd, 2013, Amount: 2.2964239421360775 Date: October 2nd, 2014, Amount: 2.3771955783605696 Date: October 2nd, 2015, Amount: 2.4620125358043627 Date: October 3rd, 2016, Amount: 2.5577482349514837 Date: October 2nd, 2017, Amount: 2.6514547175785808 Date: October 2nd, 2018, Amount: 2.7522468704257936 Date: October 2nd, 2019, Amount: 2.86461569336743 Date: October 2nd, 2020, Amount: 2.9886229182175446 Date: October 2nd, 2020, Amount: 45.388160089218154

As you can see last 2 cashflows have to be last coupon and notional, but notional is lower than 100.

The inflation coeficient as of October 2nd 2022 is: 2.9886229182175446/2.05 = 1.45786483815

So last cashflow (Notional + inflation) should be 1.45786483815 * 100 = 145.786483815 (I'm getting only the inflation part 45.786483815 but not the notional itself "100"

Can you check this.

Thank you

boring-cyborg[bot] commented 1 month ago

Thanks for posting! It might take a while before we look at your issue, so don't worry if there seems to be no feedback. We'll get to it.

lballabio commented 1 month ago

You're explicitly setting growthOnly = True. This causes the final coupon to only pay the inflation. Use False instead.

davidt922 commented 1 month ago

Understood, I was thinking this parameter was to limit the cupons to never be lower than the previous value.

Thank you so much for the clarification

El dt., 15 d’oct. 2024, 16:45, Luigi Ballabio @.***> va escriure:

You're explicitly setting growthOnly = True. This causes the final coupon to only pay the inflation. Use False instead.

— Reply to this email directly, view it on GitHub https://github.com/lballabio/QuantLib/issues/2095#issuecomment-2414140389, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHU7NU4SF263FVBKK5ZHPODZ3UTARAVCNFSM6AAAAABP7HKNMOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDIMJUGE2DAMZYHE . You are receiving this because you authored the thread.Message ID: @.***>