image1

Day Counter, Roll Conventions and Schedules

[19]:
import os, sys, holidays
from datetime import date, timedelta, datetime
from dateutil.relativedelta import relativedelta

# Get the current working directory
current_dir = os.getcwd()

# Get the grandparent directory
grandparent_dir = os.path.abspath(os.path.join(current_dir, "..", ".."))

# Add the grandparent directory to sys.path
if grandparent_dir not in sys.path:
    sys.path.insert(0, grandparent_dir)

print("Grandparent directory containing Rivapy module added to sys.path:", grandparent_dir)
from rivapy.tools.enums import RollConvention
from rivapy.tools.datetools import Period, Schedule, roll_day
Grandparent directory containing Rivapy module added to sys.path: c:\Users\DrHansNguyen\Documents

The Basics

For financial instrument valuation some basic ingredients are needed that are easily neglected when looking at the overall valuation concepts, instrument types, and models. When being precise one has to properly deal with dates at which e.g. payments are due, periods such as accrual periods start and end, etc. Here we briefly introduce

  • calenders

  • business day conventions

  • roll conventions

  • schedules, and

  • day count conventions.

Holiday calendar

Holiday calenders are at the heart of any financial library. They are used to determine whether a specific date corresponds to a bank holiday. Which calendar to use depends on the context in which the date is used. For financial instruments it is usually the instrument’s settlement systen, the papying agent’s system, or the clearing house that governs the applicable calender. Across the EUR-zone the relevant calendar is often the TARGET2 calender.

RIVAPY uses the Python library holidays as a Holiday calendar for determining business days. The following example uses the holiday calendar of the Euopean Central Bank below (i.e. TARGET2).

[20]:
ecb_holidays = holidays.ECB(2024)       # Optionally a year or a list of years can be provided.
                                        # By default the years are automatically extended when needed, see below.

for d,n in ecb_holidays.items():
    print(d,n)

print(ecb_holidays.is_working_day("2023-1-1"))  # This can be used to check for working day (holidays and weekends considered)
                                                # and adds 2023 to ecb_holidays

for d,n in ecb_holidays.items():
    print(d,n)
2024-01-01 New Year's Day
2024-03-29 Good Friday
2024-04-01 Easter Monday
2024-05-01 Labour Day
2024-12-25 Christmas Day
2024-12-26 Christmas Holiday
False
2024-01-01 New Year's Day
2024-03-29 Good Friday
2024-04-01 Easter Monday
2024-05-01 Labour Day
2024-12-25 Christmas Day
2024-12-26 Christmas Holiday
2023-01-01 New Year's Day
2023-04-07 Good Friday
2023-04-10 Easter Monday
2023-05-01 Labour Day
2023-12-25 Christmas Day
2023-12-26 Christmas Holiday

Date Rolling: Business day conventions and Roll Conventions

Dealing with financial instruments and payment unambiguous information is required on how to ‘roll’ dates that fall on a bank holiday or that need to be moved to the start date of a subsequent or previous period when building schedules for a given start or end date.

Business Day Conventions

Business day conventions (bdc) determine how dates are adjusted if they fall on non-business days (e.g., weekends or holidays). These conventions are essential in determining the exact timing of payments or other financial events. Common business day conventions include:

  1. Unadjusted
    The date is not adjusted, even if it falls on a non-business day.
  2. Following
    If the date falls on a non-business day, it is rolled forward to the next business day.
  3. Modified Following
    Similar to Following, but if rolling forward moves the date to the next month, it is rolled backward to the preceding business day.
  4. Preceding
    If the date falls on a non-business day, it is rolled backward to the previous business day.
  5. Modified Preceding
    Similar to Preceding, but if rolling backward moves the date to the previous month, it is rolled forward to the next business day.
[21]:
# Example of selecting a business day convention in rivapy
business_day_convention = [
                    RollConvention.UNADJUSTED,
                    RollConvention.FOLLOWING,
                    RollConvention.MODIFIED_FOLLOWING,
                    RollConvention.PRECEDING,
                    RollConvention.MODIFIED_PRECEDING,
                    ]
[22]:
# 01.01.2021 is a friday, notice how MODIFIED_PRECEDING is the following business day
for bdc in business_day_convention:
    rolled_day = roll_day(day = date(2021,1,1),
                         calendar = holidays.ECB(),
                         business_day_convention = bdc,
                         start_day = None,
                         settle_days = 0)
    print(rolled_day.date(), "\t ", bdc.name)

2021-01-01        UNADJUSTED
2021-01-04        FOLLOWING
2021-01-04        MODIFIED_FOLLOWING
2020-12-31        PRECEDING
2021-01-04        MODIFIED_PRECEDING

Roll Conventions

Roll Conventions (aka ‘roll rules’) govern the rolling forward (or backward) of dates when building schedules for financial instruments. Common roll rules include

  1. End-of-Month (EOF) Rolls to the last calender day of a month, adjusting for shorter month. E.g. if the start date is March 15 and frequency is set to monthly then future dates are April 15, May 15, etc.

  2. Day-of-Month (DOF) Rolls to the same calender day each period. E. g. if the start date is Jan 31 and frequency is set to monthly then next dates are Feb 28 (or Feb 29 in leap years), March 31, etc.

  3. IMM Dates Rolls to the 3rd Wednesday of March, June, Sept, dec (used in futures/swaps).

Other roll rules may be defined by individual contracts, e.g. rules that link dates to a specific day of the week (“the first Monday of each month in year 2020”).

Schedules

For different financial instruments such as bonds or swaps a vector for certain dates is needed. These vectors are usually called schedules. They may describe the dates where certain payments or other events (such as fixings) occur. Such schedules are normally based on some construction logic. The generation of schedules is therefore often governed by algorithms, which are called schedule generators. To create such a schedule with a generator, we have three main ingredients:

  • A holiday calendar since most often schedules contain only business days,

  • Periods describing the frequency of the dates such as monthly, yearly, quarterly etc.,

  • a role convention that may adjust period end or start dates,

  • a business day convention which defines what happens if the algorithm ends at a holiday.

Periods

A period is described by the number of years/months/days and is the basis in the schedule generation.

[23]:
period_1yr = Period(1,0,0) # create a period of 1 year (first argument of this method describes years, second month and last days)
period_3m = Period(0,3,0)
period_30days = Period(0,0,30)

Schedule generation

To create a schedule we first have to create a specification containing the information described above (holidays, periods, roll rules, business day conventions). We may then call the generate method to create a list of dates defining the schedule.

Additional Options:

  • ``backwards`` (bool, optional): Defines the direction for rolling out the schedule.

    • True: The schedule will be rolled out backwards (from the end day to the start day).

    • False: The schedule will be rolled out forwards (from the start day to the end day).

    • Defaults to True.

  • ``stub`` (bool, optional): Defines the behavior for the first/last period when it is shorter than the others.

    • True: The first/last period is accepted even if it is shorter.

    • False: The remaining days of the shorter period are added to the neighboring period.

    • Defaults to True.

[24]:
#no business day adjustments
unadjusted_schedule = Schedule._roll_out(from_=date(2024,1,1), to_=date(2025,1,1), term=period_3m, backwards=False,
                  long_stub=False)
# Show undajusted schedule
print("Unadjusted:")
for s in unadjusted_schedule:
    print(s.date())
print()

# adjust for business days (holidays and weekends)
schedule_2024_3m_period = Schedule(start_day = date(2024,1,1),
                                    end_day = date(2025,1,1),
                                    time_period = period_3m,
                                    backwards = False,
                                    business_day_convention = RollConvention.MODIFIED_FOLLOWING,
                                    calendar = holidays.ECB())
adjusted_schedule = schedule_2024_3m_period.generate_dates(ends_only=False)
print("Adjusted for Working Day:")
for s in adjusted_schedule:
    print(s.date())

Unadjusted:
2024-01-01
2024-04-01
2024-07-01
2024-10-01
2025-01-01

Adjusted for Working Day:
2024-01-02
2024-04-02
2024-07-01
2024-10-01
2025-01-02

Day Count Conventions

Let’s imagine we have a financial product (e.g. Bond) which pays us interest of 5% p.a. each month over a year. Additionally let’s ignore business days and assume we receive a coupon every month with the schedule being 01.01.2025, 01.02.2025, …, 01.01.2026. Without further information, it is not clear how the coupon (as a year fraction of 5% times Nominal) is calculated for each monthly period:

  • Should the the coupon for the first period in January (01.01.2025 - 01.02.2025, 31 days) be the same as for the second period in February (01.02.2025 - 01.03.2025, 28 days)?

  • Are the period start days e.g. 01.01.2025 or the period end days e.g. 01.01.2026 included or excluded in the calculation?

Day Count Conventions (or Date Conventions, Day Count Fractions) give answers to these questions by defining how the time between two dates is measured for financial calculations. They determine how interest accrues between dates. An official source on day count conventions as well as other market conventions can be found here. For an overview you can check out this.

Actual/Actual (ISDA)

This Day Count Convention counts the actual days between period start and period end date divided by the actual number of days in the year (365 or 366 in a leap year). If the period spans over a leap year and a normal year, the days are splitted and the year fraction is computed as:

\[\frac{Days\; in\; non leap\; year}{365} + \frac{Days\; in\; leap\; year}{366}\]

The first day in the period is included, the last day is excluded.

Actual/Actual (ISDA) is primarily used in derivatives markets and swaps.

[25]:
from rivapy.tools.datetools import DayCounter as dc
# Here you see an example using rivapy
# The period contains 31 days in the leap year 2024 and 31 days in the nonleap year 2025
print(dc("ActAct").yf(date(2024,12,1), date(2025,2,1)))   #last day is excluded
print(31/366+31/365) # Should be the same result
0.16963096040122763
0.16963096040122763

Actual/365 (Fixed)

This can be seen as a simplification of the Actual/Actual (ISDA) Convention, where the denominator is always 365.

Actual/360

Same as the Convention above, but the denominator is always 360.

[26]:
d1 = date(2024,1,1)
d2 = date(2025,1,1)
print("Days in period:",(d2 - d1).days)

print("Act365Fixed:\t",dc("Act365Fixed").yf(d1, d2)) # equal to 366/365
print("Act360:\t\t",dc("Act360").yf(d1, d2))         # equal to 366/360
Days in period: 366
Act365Fixed:     1.0027397260273974
Act360:          1.0166666666666666

ACT/ACT (ICMA)

This Day Count Convention is commonly used in bond markets to calculate the day count fraction, particularly for determining accrued interest and bond yields. It provides a standardized way to calculate the fraction of a year represented by a given period, based on actual calendar days and the bond’s coupon schedule.

\[\text{Day Count Fraction} = \sum_{c\in \text{C}}\frac{\mathbb{I}\left(d_1 \leq c_{\text{end}} \land d_2 \geq c_{\text{start}}\right)\cdot\text{days}\left(\min(d_2, c_{\text{end}})-\max(d_1, c_{\text{start}})\right)}{\text{days}\left(d_2 - d_1\right)\cdot f_\text{Coupon}}\]

Where:

  • :math:`C`: is the sorted set of coupon payment dates.

  • :math:`c_{text{start}}`: is the start date of the coupon period.

  • :math:`c_{text{end}}`: is the end date of the coupon period.

  • :math:`mathbb{I}left(dotsright)`: is an indicator function is returns 1 if the expression inside renders true, otherwise it returns 0.

  • :math:`text{days}(dots)`: returns the actual days between to dates.

  • :math:`f_text{Coupon}`: annual coupon frequency (1 for annualy coupon payment, 2 for semi annual etc.)

The first day in the period is included, the last day is excluded.

Example: Semi-annual bond with last coupon on 1st May, next coupon on 1 November (number of days: 184). On 31st May, the year franction is calculated as:

\[\frac{30}{184 \times 2} = \frac{30}{368}\]
[27]:
# ACT/ACT ICMA is used for bonds and requires additional arguments for coupon payment dates and frequency
coupon_schedule = [date(2024, 5, 1), date(2024, 11, 1)]
print(dc.yf_ActActICMA(date(2024,5,1), date(2024,5,31), coupon_schedule=coupon_schedule, coupon_frequency=2))
print(30/368) # Should be the same result
0.08152173913043478
0.08152173913043478

30/360 (ISDA) or “Bond Basis”

This convention assumes that each month has 30 days and that the year has 360 days. The formula is therefore given by:

\[\frac{360 (Y_{2} - Y_{1}) + 30(M_{2}-M_{1}) + D_{2}-D_{1}}{360}\]

where

  • \(Y_{1}\) is the year of the period start date.

  • \(Y_{2}\) is the year of the period end date (day immeadiately following last day included).

  • \(M_{1}\) is the month (expressed as a number) of the period start date.

  • \(M_{2}\) is the month (expressed as a number) of the period end date (day immeadiately following last day included).

  • \(D_{1}\) is the calendar day (expressed as a number) of the period start date, unless this number is 31, in which case \(D_{1}\) is set to 30.

  • \(D_{2}\) is the calendar day (expressed as a number) of the period end date (day immeadiately following last day included), unless this number is 31 and \(D_{1}>29\), in which case \(D_{2}\) is set to 30.

[36]:
# in this example we have 31 included days, because the period end date is excluded
print(dc.yf_30360ISDA(date(2025,1,1), date(2025,2,1)))
# Y1=Y2=2025, M1=1, M2=2, D1=D2=1
# the result should be 30/360=1/12=0,083333...

# now we move the period one day back (still 31 included days)
print(dc.yf_30360ISDA(date(2024,12,31), date(2025,1,31)))
# Y1=2024, Y2=2025, M1=12, M2=1, D1=D2=30
# the result should be (360+30*(-11))/360=1/12=0,083333...
# and same as picking one or both of the calendar days to 30th e.g. date(2024,12,30) and date(2025,1,31)

# we pick a period where D1 and D2 is not adjusted (still 31 included days)
print(dc.yf_30360ISDA(date(2025,4,29), date(2025,5,30)))
# Y1=Y2=2025, M1=4, M2=5, D1=29, D2=30
# the result should be (30+1)/360=0,08611111...

# moving the period one day forward changes D1 but not D2 (still 31 included days)
print(dc.yf_30360ISDA(date(2025,4,30), date(2025,5,31)))
# Y1=Y2=2025, M1=4, M2=5, D1=30, D2=30 (because D1>29)
# the result should be 30/360=1/12=0,083333...
0.08333333333333333
0.08333333333333337
0.08611111111111111
0.08333333333333333

30E/360 or “Eurobond Basis”

This convention follows the same formula as the previous 30/360 (ISDA) convention, but has a simpler adjustment to \(D_{2}\):

\[\frac{360 (Y_{2} - Y_{1}) + 30(M_{2}-M_{1}) + D_{2}-D_{1}}{360}\]

where

  • \(Y_{1}\) is the year of the period start date.

  • \(Y_{2}\) is the year of the period end date (day immeadiately following last day included).

  • \(M_{1}\) is the month (expressed as a number) of the period start date.

  • \(M_{2}\) is the month (expressed as a number) of the period end date (day immeadiately following last day included).

  • \(D_{1}\) is the calendar day (expressed as a number) of the period start date, unless this number is 31, in which case \(D_{1}\) is set to 30.

  • \(D_{2}\) is the calendar day (expressed as a number) of the period end date (day immeadiately following last day included), unless this number is 31, in which case :math:`D_{2}` is set to 30.

[29]:
# all of the examples below have the same result
print(dc.yf_30E360(date(2024,12,31), date(2025,1,31)))
print(dc.yf_30E360(date(2024,12,31), date(2025,1,30)))
print(dc.yf_30E360(date(2024,12,30), date(2025,1,31)))
print(dc.yf_30E360(date(2024,12,30), date(2025,1,30)))
# Y1=2024, Y2=2025, M1=12, M2=1, D1=D2=30
# the result should be (360+30*(-11))/360=1/12=0,083333...
0.08333333333333337
0.08333333333333337
0.08333333333333337
0.08333333333333337

30U/360 or 30/360 US

This convention is also known as the 30/360 U.S. Treasury method and is an extension the 30/360 (ISDA) convention. The only difference is the handling of the last day in february:

\[\frac{360 (Y_{2} - Y_{1}) + 30(M_{2}-M_{1}) + D_{2}-D_{1}}{360}\]

where

  • \(Y_{1}\) is the year of the period start date.

  • \(Y_{2}\) is the year of the period end date (day immeadiately following last day included).

  • \(M_{1}\) is the month (expressed as a number) of the period start date.

  • \(M_{2}\) is the month (expressed as a number) of the period end date (day immeadiately following last day included).

  • \(D_{1}\) is the calendar day (expressed as a number) of the period start date, unless:

    • if \(D_{1}=31\), set \(D_{1}=30\).

    • if :math:`D_{1}` is the last day of february (\(M_{1}=2\), \(D_{1}=29\) in leap years or \(D_{1}=28\) in nonleap years), set :math:`D_{1}=30`

  • \(D_{2}\) is the calendar day (expressed as a number) of the period end date (day immeadiately following last day included), unless:

    • if \(D_{2}=31\) and \(D_{1}>29\), set \(D_{2}=30\).

    • if :math:`D_{1}` and :math:`D_{2}` are the last days of february, set :math:`D_{2}=30`.

[30]:
print(dc.yf_30U360(date(2024,2,29), date(2025,2,28)))
#Y1=2024, Y2=2025, M1=M2=2, D1=D2=30

print(dc.yf_30U360(date(2024,2,28), date(2025,2,28)))
#Y1=2024, Y2=2025, M1=M2=2, D1=D2=28

print(dc.yf_30U360(date(2023,2,28), date(2024,2,28))) # (360+28-30)/360
#Y1=2023, Y2=2024, M1=M2=2, D1=30, D2=28
1.0
1.0
0.9944444444444445

Below you can find a function printing all the introduced Day Count Conventions at once for a specified period:

[31]:
from rivapy.tools.datetools import DayCounter as dc

# this is how you can get the year fraction based on a date count convention in rivapy:
# Options:
#
#   - "ActActICMA"
#   - "Act365Fixed"
#   - "ActAct"
#   - "Act360"
#   - "30U360"
#   - "30E360"
#   - "30360ISDA"
d1 = date(2023,12,31)
d2 = date(2024,7,1)
coupon_schedule = [date(2024, 1, 1), date(2024, 7, 1)]
dc("ActAct").yf(d1, d2)


print(dc.yf_ActActICMA(date(2024,5,1), date(2024,5,31), coupon_schedule=coupon_schedule, coupon_frequency=2))

#printing all implemented types in rivapy

def show_day_counter(d1,d2,dc_names):
    if isinstance(dc_names, str):
        dc_names = [dc_names]
    print("d1"+":", d1)
    print("d2"+":", d2)
    for dc_name in dc_names:
        if dc_name != "ActActICMA":
            print(dc_name + ":",dc(dc_name).yf(d1,d2))
        else:
            print(dc_name + ":",dc.yf_ActActICMA(d1, d2, coupon_schedule=coupon_schedule, coupon_frequency=2))

show_day_counter(date(2024,1,1),date(2024,7,1),["ActActICMA","30360ISDA","30E360", "30U360","ActAct", "Act360","Act365Fixed"])
0.08241758241758242
d1: 2024-01-01
d2: 2024-07-01
ActActICMA: 0.5
30360ISDA: 0.5
30E360: 0.5
30U360: 0.5
ActAct: 0.4972677595628415
Act360: 0.5055555555555555
Act365Fixed: 0.4986301369863014
[32]:
d1 = date(2024,2,29)
delta = relativedelta(years=0, months = 1, days=0)
d2 = d1 + delta
print("d1:", d1)
print("d2:", d2)

d1: 2024-02-29
d2: 2024-03-29