domokane / FinancePy

A Python Finance Library that focuses on the pricing and risk-management of Financial Derivatives, including fixed-income, equity, FX and credit derivatives.
GNU General Public License v3.0
2.16k stars 322 forks source link

Schedule generation bug #69

Closed mwalkup55 closed 3 years ago

mwalkup55 commented 3 years ago

There appears to be a bug in the schedule generation, which is making swaps fail: "FinError: Swap coupons are not on the same date grid. "

an example:

mycal = FinCalendar(FinCalendarTypes.UNITED_STATES)

valuationDate = FinDate(m=3,d=29,y=2005)

sched1 = FinSchedule( valuationDate.addTenor('2d'), mycal.adjust(valuationDate.addTenor('2d').addTenor('4Y'),FinBusDayAdjustTypes.MODIFIED_FOLLOWING), freqType=FinFrequencyTypes.SEMI_ANNUAL, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING, dateGenRuleType=FinDateGenRuleTypes.BACKWARD, calendarType=FinCalendarTypes.UNITED_STATES, adjustTerminationDate=False )

sched2 = FinSchedule( valuationDate.addTenor('2d'), mycal.adjust(valuationDate.addTenor('2d').addTenor('50Y'),FinBusDayAdjustTypes.MODIFIED_FOLLOWING), freqType=FinFrequencyTypes.SEMI_ANNUAL, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING, dateGenRuleType=FinDateGenRuleTypes.BACKWARD, calendarType=FinCalendarTypes.UNITED_STATES, adjustTerminationDate=False )

print(sched1._adjustedDates[-1])

print(sched2._adjustedDates[len(sched1._adjustedDates)-1])

It appears that the termination date is not aligned with the data generation for the coupons

gulls-on-parade commented 3 years ago

I see this error too. It looks to me like it is coming from recursion in FinDate._generate() used to generate unadjustedScheduleDates:

def _generate(self):
''' Generate schedule of dates according to specified date generation
rules and also adjust these dates for holidays according to the
specified business day convention and the specified calendar. '''

# NEED TO INCORPORATE ADJUST TERMINATION DATE FLAG

calendar = FinCalendar(self._calendarType)
frequency = FinFrequency(self._freqType)
numMonths = int(12 / frequency)

unadjustedScheduleDates = []
self._adjustedDates = []

if self._dateGenRuleType == FinDateGenRuleTypes.BACKWARD:

    nextDate = self._terminationDate
    flowNum = 0

    while nextDate > self._effectiveDate:
        unadjustedScheduleDates.append(nextDate)
        nextDate = nextDate.addMonths(-numMonths)
        flowNum += 1

For instance, in the example above, once the iteration passes over a month with <31 days (say from December to March, going backward quarterly), the subsequent dates in the schedule will reference that day of the month - instead of the initial day.

Perhaps the best way to address this is to change _generate() so that each day in the schedule references the seed date, instead of the adjacent date? @domokane can opine.

domokane commented 3 years ago

Looking into this. The termination date can be different from the final coupon date. Will revert.

domokane commented 3 years ago

The issue arises when the termination date is the 31st date of the month.

For example, with a 6 month period between coupons, 31 Mar jumps back to 30 Sep which then jumps back to 30 Mar and so the 31 March does not reappear in the unadjusted dates. It will only appear in the adjusted dates due to a 30 March flow falling on a holiday/weekend and then being adjusted forward to the 31 March.

To fix this, the swap schedule would need to know that this is an end-of-month (EOM) schedule. This can happen in two ways:

i) It could in theory be clever and set this if the termination date is an EOM. ii) or the user can set an EOM flag.

With this, all dates will be EOM and all will fall on the same grid.

I like (i) as I doubt there are many cases when you would have the termination date on the 31 but still want the earlier dates not to be EOM. But (ii) requires the user to choose this explicitly. This is probably the "safer" choice.

Let me know if you have any view. This will be easy to implement.

gulls-on-parade commented 3 years ago

I agree (i) is probably a better route, but perhaps having an attribute in FinSchedule (with a default of TRUE) for an EOM flag would provide for the odd circumstance where the user did want to reference the day of the month, instead of the month-end.

Thanks!

mwalkup55 commented 3 years ago

I prefer (i) as well. Would still need to have some holiday handling. As for example, Memorial Day is 5/31/2021 this year and 12/31 is a Japan Bank Holiday, etc....

domokane commented 3 years ago

Holiday handling happens as a next stage and affects all dates the same, irrespective of the swap termination date. EOM dates that fall on a holiday will be adjusted using the business day convention. If they are on the same grid before adjustment, they are on the same grid after holiday adjustment.

domokane commented 3 years ago

This is done. I have just pushed a new version to github and a new version 0.192 to PyPI. Let me know if you have any more problems.

mwalkup55 commented 3 years ago

There might be a leap year problem as flows are still misaligned if effective date is february end of month

Heres a reproducable example:

mycal = FinCalendar(FinCalendarTypes.UNITED_STATES)

valuationDate = FinDate(m=8,d=28,y=2006)

sched1 = FinSchedule( valuationDate.addDays(2), mycal.adjust(valuationDate.addDays(2).addTenor('2Y'),FinBusDayAdjustTypes.MODIFIED_FOLLOWING), freqType=FinFrequencyTypes.SEMI_ANNUAL, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING, dateGenRuleType=FinDateGenRuleTypes.BACKWARD, calendarType=FinCalendarTypes.UNITED_STATES, adjustTerminationDate=False )

sched2 = FinSchedule( valuationDate.addTenor('2d'), mycal.adjust(valuationDate.addTenor('2d').addTenor('50Y'),FinBusDayAdjustTypes.MODIFIED_FOLLOWING), freqType=FinFrequencyTypes.SEMI_ANNUAL, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING, dateGenRuleType=FinDateGenRuleTypes.BACKWARD, calendarType=FinCalendarTypes.UNITED_STATES, adjustTerminationDate=False )

print(sched1._adjustedDates)

print(sched2._adjustedDates[:len(sched1._adjustedDates)])

domokane commented 3 years ago

OK. I am looking at this now. Regards D

From: mwalkup55mailto:notifications@github.com Sent: 05 February 2021 18:15 To: domokane/FinancePymailto:FinancePy@noreply.github.com Cc: Dominic O'Kanemailto:quant@financepy.com; State changemailto:state_change@noreply.github.com Subject: Re: [domokane/FinancePy] Schedule generation bug (#69)

There might be a leap year problem as flows are still misaligned if effective date is february end of month

Heres a reproducable example:

mycal = FinCalendar(FinCalendarTypes.UNITED_STATES)

valuationDate = FinDate(m=8,d=28,y=2006)

sched1 = FinSchedule( valuationDate.addDays(2), mycal.adjust(valuationDate.addDays(2).addTenor('2Y'),FinBusDayAdjustTypes.MODIFIED_FOLLOWING), freqType=FinFrequencyTypes.SEMI_ANNUAL, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING, dateGenRuleType=FinDateGenRuleTypes.BACKWARD, calendarType=FinCalendarTypes.UNITED_STATES, adjustTerminationDate=False )

sched2 = FinSchedule( valuationDate.addTenor('2d'), mycal.adjust(valuationDate.addTenor('2d').addTenor('50Y'),FinBusDayAdjustTypes.MODIFIED_FOLLOWING), freqType=FinFrequencyTypes.SEMI_ANNUAL, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING, dateGenRuleType=FinDateGenRuleTypes.BACKWARD, calendarType=FinCalendarTypes.UNITED_STATES, adjustTerminationDate=False )

print(sched1._adjustedDates)

print(sched2._adjustedDates[:len(sched1._adjustedDates)])

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHubhttps://github.com/domokane/FinancePy/issues/69#issuecomment-774166908, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ABJ73PJ6CP366ONQK4HTJSDS5QRTXANCNFSM4XDCEV6Q.

domokane commented 3 years ago

Note that the first swap termination date is 28 Feb 2010 is a Sunday but you adjust it to the 26 Feb which is not EOM.

The second swap termination date is the 28 Feb 2056 which is a Monday. But it’s not the EOM as it’s a leap year.

So neither schedule is based on an EOM date.

So you need to pass in 28 Feb 2010 as first termination date and 29 Feb 2056 as the second termination date.

What do you expect/prefer the behaviour to be ?

Regards D

From: mwalkup55mailto:notifications@github.com Sent: 05 February 2021 18:15 To: domokane/FinancePymailto:FinancePy@noreply.github.com Cc: Dominic O'Kanemailto:quant@financepy.com; State changemailto:state_change@noreply.github.com Subject: Re: [domokane/FinancePy] Schedule generation bug (#69)

There might be a leap year problem as flows are still misaligned if effective date is february end of month

Heres a reproducable example:

mycal = FinCalendar(FinCalendarTypes.UNITED_STATES)

valuationDate = FinDate(m=8,d=28,y=2006)

sched1 = FinSchedule( valuationDate.addDays(2), mycal.adjust(valuationDate.addDays(2).addTenor('2Y'),FinBusDayAdjustTypes.MODIFIED_FOLLOWING), freqType=FinFrequencyTypes.SEMI_ANNUAL, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING, dateGenRuleType=FinDateGenRuleTypes.BACKWARD, calendarType=FinCalendarTypes.UNITED_STATES, adjustTerminationDate=False )

sched2 = FinSchedule( valuationDate.addTenor('2d'), mycal.adjust(valuationDate.addTenor('2d').addTenor('50Y'),FinBusDayAdjustTypes.MODIFIED_FOLLOWING), freqType=FinFrequencyTypes.SEMI_ANNUAL, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING, dateGenRuleType=FinDateGenRuleTypes.BACKWARD, calendarType=FinCalendarTypes.UNITED_STATES, adjustTerminationDate=False )

print(sched1._adjustedDates)

print(sched2._adjustedDates[:len(sched1._adjustedDates)])

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHubhttps://github.com/domokane/FinancePy/issues/69#issuecomment-774166908, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ABJ73PJ6CP366ONQK4HTJSDS5QRTXANCNFSM4XDCEV6Q.

mwalkup55 commented 3 years ago

The problem is that the collection of swaps fails the "FinError: Swap coupons are not on the same date grid. " because the cash flow dates are not aligned. For example the above code returns:

[30-AUG-2006, 28-FEB-2007, 29-AUG-2007, 29-FEB-2008, 29-AUG-2008] [30-AUG-2006, 28-FEB-2007, 28-AUG-2007, 28-FEB-2008, 28-AUG-2008]

Where the last 3 dates do not match. For the first swap, using BBG's pricer I would expect the dates to be:

[30-AUG-2006, 28-FEB-2007, 30-AUG-2007, 29-FEB-2008, 29-AUG-2008]

The below reproduces the alignment error

def _validateInputs(iborSwaps):
    longestSwap = iborSwaps[-1]
    longestSwapCpnDates = longestSwap._fixedLeg._paymentDates
    for swap in iborSwaps[0:-1]:
        print(swap)
        swapCpnDates = swap._fixedLeg._paymentDates
        numFlows = len(swapCpnDates)
        for iFlow in range(0, numFlows):
            if swapCpnDates[iFlow] != longestSwapCpnDates[iFlow]:
                raise FinError("Swap coupons are not on the same date grid.")

valuationDate = FinDate(m=8,d=28,y=2006)

swapType = FinSwapTypes.PAY
fixedDCCType = FinDayCountTypes.THIRTY_360_BOND
fixedFreqType = FinFrequencyTypes.SEMI_ANNUAL
spotDays = 2
mycal = FinCalendar(FinCalendarTypes.UNITED_STATES)
settlementDate = valuationDate.addDays(spotDays)

swaps = [
    FinIborSwap(settlementDate, '2Y', swapType, .01, fixedFreqType, fixedDCCType, calendarType=cal, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING,dateGenRuleType=FinDateGenRuleTypes.BACKWARD),
    FinIborSwap(settlementDate, '50Y', swapType, .02, fixedFreqType, fixedDCCType, calendarType=cal, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING,dateGenRuleType=FinDateGenRuleTypes.BACKWARD)
]

_validateInputs(swaps)
domokane commented 3 years ago

Hi – I understand that the flows are not aligned. I just want to understand what behaviour you expect given the termination dates you provided. Remember the logic we agreed last night was that the EOM flag would be triggered only if the termination date is an EOM and the EOM flag is true. However by calendar adjusting the termination date, in the previous example you shifted both dates so that they were not EOMs and so EOM is not True and the dates just step back without falling on the same grid. One solution is to state that if you set EOM true, you get an EOM schedule irrespective of the chosen date of the termination date. I am fine with this. In many ways it makes more sense. I can even throw an exception if the termination date is not an EOM OR a holiday adjusted EOM. This is a good double check. Regards D

From: mwalkup55mailto:notifications@github.com Sent: 05 February 2021 19:23 To: domokane/FinancePymailto:FinancePy@noreply.github.com Cc: Dominic O'Kanemailto:quant@financepy.com; State changemailto:state_change@noreply.github.com Subject: Re: [domokane/FinancePy] Schedule generation bug (#69)

The problem is that the collection of swaps fails the "FinError: Swap coupons are not on the same date grid. " because the cash flow dates are not aligned. For example the below code returns:

[30-AUG-2006, 28-FEB-2007, 29-AUG-2007, 29-FEB-2008, 29-AUG-2008] [30-AUG-2006, 28-FEB-2007, 28-AUG-2007, 28-FEB-2008, 28-AUG-2008]

Where the last 3 dates do not match. For the first swap, using BBG's pricer I would expect the dates to be:

[30-AUG-2006, 28-FEB-2007, 30-AUG-2007, 29-FEB-2008, 29-AUG-2008]

def _validateInputs(iborSwaps):

longestSwap = iborSwaps[-1]

longestSwapCpnDates = longestSwap._fixedLeg._paymentDates

for swap in iborSwaps[0:-1]:

    print(swap)

    swapCpnDates = swap._fixedLeg._paymentDates

    numFlows = len(swapCpnDates)

    for iFlow in range(0, numFlows):

        if swapCpnDates[iFlow] != longestSwapCpnDates[iFlow]:

            raise FinError("Swap coupons are not on the same date grid.")

valuationDate = FinDate(m=8,d=28,y=2006)

swapType = FinSwapTypes.PAY

fixedDCCType = FinDayCountTypes.THIRTY_360_BOND

fixedFreqType = FinFrequencyTypes.SEMI_ANNUAL

spotDays = 2

mycal = FinCalendar(FinCalendarTypes.UNITED_STATES)

settlementDate = valuationDate.addDays(spotDays)

swaps = [

FinIborSwap(settlementDate, '2Y', swapType, .01, fixedFreqType, fixedDCCType, calendarType=cal, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING,dateGenRuleType=FinDateGenRuleTypes.BACKWARD),

FinIborSwap(settlementDate, '50Y', swapType, .02, fixedFreqType, fixedDCCType, calendarType=cal, busDayAdjustType=FinBusDayAdjustTypes.MODIFIED_FOLLOWING,dateGenRuleType=FinDateGenRuleTypes.BACKWARD)

]

_validateInputs(swaps)

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHubhttps://github.com/domokane/FinancePy/issues/69#issuecomment-774205262, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ABJ73PITWUOBX6PVOBMK4XDS5QZTTANCNFSM4XDCEV6Q.

mwalkup55 commented 3 years ago

Reflecting on this a bit more, and I think that the EOM determination might need to be set based on the effective date, rather than termination date. (the below from https://quant.stackexchange.com/questions/33748/a-question-about-dates-generation)

With the effective date, the maturity date is the effective date + tenor of the swap. To do this date calculation, you also need to understand the "roll day." In this case, the roll day should be determined from the effective date: If "end-of-month roll" is off: If the effective date is MM-28-YYYY, then the roll day is 28, and the maturity date should also fall on the 28th. If the effective date is MM-31-YYYY, then the roll day is 31, and the maturity date should also fall on the 31st (if that's not possible, move it BACK to the end of the month). If "end-of-month roll" is on (default for US Treasuries): If effective date is month end, all dates in the schedule should also fall on month end. Otherwise, use the same rules above.

domokane commented 3 years ago

Agree. All swaps will share the same effective date. If you are aware of an ISDA schedule generation guide then let me know. I have been unable to find one. I will implement this tomorrow. Regards D

From: mwalkup55mailto:notifications@github.com Sent: 05 February 2021 21:06 To: domokane/FinancePymailto:FinancePy@noreply.github.com Cc: Dominic O'Kanemailto:quant@financepy.com; State changemailto:state_change@noreply.github.com Subject: Re: [domokane/FinancePy] Schedule generation bug (#69)

Reflecting on this a bit more, and I think that the EOM determination might need to be set based on the effective date, rather than termination date. (the below from https://quant.stackexchange.com/questions/33748/a-question-about-dates-generation)

With the effective date, the maturity date is the effective date + tenor of the swap. To do this date calculation, you also need to understand the "roll day." In this case, the roll day should be determined from the effective date: If "end-of-month roll" is off: If the effective date is MM-28-YYYY, then the roll day is 28, and the maturity date should also fall on the 28th. If the effective date is MM-31-YYYY, then the roll day is 31, and the maturity date should also fall on the 31st (if that's not possible, move it BACK to the end of the month). If "end-of-month roll" is on (default for US Treasuries): If effective date is month end, all dates in the schedule should also fall on month end. Otherwise, use the same rules above.

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHubhttps://github.com/domokane/FinancePy/issues/69#issuecomment-774259636, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ABJ73PP7ZGFXBOG3POZBBG3S5RFTXANCNFSM4XDCEV6Q.

mwalkup55 commented 3 years ago

This R package has you include an EOM flag, I think this is a decent way to go about it. I'll look for the ISDA document

https://github.com/imanuelcostigan/fmdates

mwalkup55 commented 3 years ago

I've sent the ISDA definitions document to the email listed on the main page