How to use the calendar, schedule, and day count utility functions

The Daml Finance library contains date-related utility functions for implementing industry-standard conventions. These functions are used internally in instruments like bonds and swaps, but they can also be helpful in your own custom use cases. For example, you can use these functions to:

  • understand and validate the implementation of existing Daml Finance instruments
  • compose existing Daml Finance instruments; for example, define a series of zero-coupon bonds according to a specific schedule of maturity dates
  • develop your own instruments that depend on business day shifts, schedules, or day count conventions
  • work with non-financial use cases

Calendar

The Calendar module contains various utility functions related to business days and date adjustments.

To distinguish business days and non-business days, set a holiday calendar. The HolidayCalendarData type defines the set of non-business days by specifying the days of the week that belong to the weekend, as well as the dates of designated holidays. The following example sets Saturday and Sunday as weekend days and specifies some additional holidays:

calendar = HolidayCalendarData with
  id = "USNY"
  weekend = [Saturday, Sunday]
  holidays = [date 2018 Jan 02, date 2018 Jan 31, date 2018 Feb 1]

To check whether a given date is a business day, use the isBusinessDay function:

  isBusinessDay calendar (date 2018 Jan 02) === False
  isBusinessDay calendar (date 2018 Jan 03) === True

You can also get the previous and next business day:

  previousBusinessDay calendar (date 2018 Jan 03) === date 2018 Jan 01
  nextBusinessDay calendar (date 2018 Jan 01) === date 2018 Jan 03

To find the date that corresponds to a specific number of business days in the future, use addBusinessDays:

  addBusinessDays calendar 5 (date 2018 Jan 01) === date 2018 Jan 09

You can also get the date that corresponds to a number of business days in the past, by passing in a negative offset:

  addBusinessDays calendar (-5) (date 2018 Feb 05) === date 2018 Jan 25

You can adjust a date according to a business day convention:

  adjustDate calendar NoAdjustment (date 2018 Mar 31) === (date 2018 Mar 31)
  adjustDate calendar Following (date 2018 Mar 31) === (date 2018 Apr 02)
  adjustDate calendar ModifiedFollowing (date 2018 Mar 31) === (date 2018 Mar 30)
  adjustDate calendar Preceding (date 2018 Apr 01) === (date 2018 Mar 30)
  adjustDate calendar ModifiedPreceding (date 2018 Apr 01) === (date 2018 Apr 02)

For the full list of available conventions, see the BusinessDayConventionEnum reference. Use them to specify how non-business days are adjusted, including when the next or previous business day is around the end of the month.

RollConvention

The RollConvention module provides utility functions to add a period to a given date.

For example, you can add a day, week, month, or year to a date:

  addPeriod (D.date 2018 Oct 01) (Period with periodMultiplier = 1; period = D) ===
    D.date 2018 Oct 02
  addPeriod (D.date 2018 Oct 01) (Period with periodMultiplier = 1; period = W) ===
    D.date 2018 Oct 08
  addPeriod (D.date 2018 Oct 01) (Period with periodMultiplier = 1; period = M) ===
    D.date 2018 Nov 01
  addPeriod (D.date 2018 Oct 01) (Period with periodMultiplier = 1; period = Y) ===
    D.date 2019 Oct 01

This function can also handle edge cases. For example, when you add a period of one month to a date, the function makes adjustments if the resulting month has fewer days than the starting month:

  addPeriod (D.date 2018 Oct 31) (Period with periodMultiplier = 1; period = M) ===
    D.date 2018 Nov 30

To subtract a period of time, use a negative offset:

  addPeriod (D.date 2018 Nov 30) (Period with periodMultiplier = -1; period = M) ===
    D.date 2018 Oct 30
  addPeriod (D.date 2018 Jan 30) (Period with periodMultiplier = -1; period = M) ===
    D.date 2017 Dec 30

You might need to find the start date of the next period according to a specific market convention, as described in the RollConventionEnum reference. The Roll convention specifies when the date is rolled.

For example, you can define an end-of-month roll:

  next (D.date 2018 Oct 31) (Period with periodMultiplier = 1; period = M) EOM ===
    D.date 2018 Nov 30

You can also set the roll to a specific day of the month:

  next (D.date 2018 Oct 01) (Period with periodMultiplier = 1; period = M) (DOM 1) ===
    D.date 2018 Nov 01

If the resulting month does not have enough days, the last day of the month is used instead:

  next (D.date 2018 Oct 31) (Period with periodMultiplier = 1; period = M) (DOM 31) ===
    D.date 2018 Nov 30

This function also takes leap days into account:

  next (D.date 2019 Aug 31) (Period with periodMultiplier = 6; period = M) (DOM 31) ===
    D.date 2020 Feb 29

The following example shows you how to roll to the previous period:

  previous (D.date 2020 Feb 29) (Period with periodMultiplier = 6; period = M) (DOM 31) ===
    D.date 2019 Aug 31

The RollConvention functions are often used in the context of rolling out a schedule, as explained in the next section.

Schedule

The Schedule module contains functions for creating a series of time periods. A schedule can be used to represent different aspects of a financial instrument, for example:

  • payment periods, which define when a bond coupon or a swap payment should be paid
  • calculation periods, which define how the interest amount is calculated

The following example defines a periodic 3M (3-month) schedule that rolls on the 30th of the month:

    periodicSchedule = PeriodicSchedule with
      businessDayAdjustment =
        BusinessDayAdjustment with calendarIds = ["USNY"]; convention = ModifiedFollowing
      effectiveDateBusinessDayAdjustment = None
      terminationDateBusinessDayAdjustment = None
      frequency = Periodic Frequency with
        rollConvention = DOM 30; period = Period with period = M; periodMultiplier = 3
      effectiveDate = D.date 2018 Nov 30
      firstRegularPeriodStartDate = None
      lastRegularPeriodEndDate = None
      stubPeriodType = None
      terminationDate = D.date 2019 Nov 30

Because there is one year between the start date and the end date, this periodic schedule should correspond to four periods of 3M each:

    expectedResult =
      [ SchedulePeriod with
          unadjustedStartDate = D.date 2018 Nov 30
          unadjustedEndDate = D.date 2019 Feb 28
          adjustedStartDate = D.date 2018 Nov 30
          adjustedEndDate = D.date 2019 Feb 28
          stubType = None
      , SchedulePeriod with
          unadjustedStartDate = D.date 2019 Feb 28
          unadjustedEndDate = D.date 2019 May 30
          adjustedStartDate = D.date 2019 Feb 28
          adjustedEndDate = D.date 2019 May 30
          stubType = None
      , SchedulePeriod with
          unadjustedStartDate = D.date 2019 May 30
          unadjustedEndDate = D.date 2019 Aug 30
          adjustedStartDate = D.date 2019 May 30
          adjustedEndDate = D.date 2019 Aug 30
          stubType = None
      , SchedulePeriod with
          unadjustedStartDate = D.date 2019 Aug 30
          unadjustedEndDate = D.date 2019 Nov 30
          adjustedStartDate = D.date 2019 Aug 30
          adjustedEndDate = D.date 2019 Nov 29
          stubType = None
      ]

Note the distinction between adjusted and unadjusted dates. The unadjusted date is the result of adding a specified time period to the start date of the previous schedule period. This date might fall on a non-business day. Apply a BusinessDayAdjustment, to get an adjusted date that falls on a business day.

Now you can roll out the schedule to get the specific start and end date for each period, and compare the schedule to the expected result:

  createSchedule cals periodicSchedule === expectedResult

Schedules often use the concept of stub periods. The above schedule had only regular 3M periods. The following example reveals what happens if the schedule starts one month later:

    periodicScheduleWithStub = PeriodicSchedule with
      businessDayAdjustment =
        BusinessDayAdjustment with calendarIds = ["USNY"]; convention = ModifiedFollowing
      effectiveDateBusinessDayAdjustment = None
      terminationDateBusinessDayAdjustment = None
      frequency = Periodic Frequency with
        rollConvention = DOM 30; period = Period with period = M; periodMultiplier = 3
      effectiveDate = D.date 2018 Dec 30
      firstRegularPeriodStartDate = Some $ D.date 2019 Feb 28
      lastRegularPeriodEndDate = None
      stubPeriodType = None
      terminationDate = D.date 2019 Nov 30

This schedule has only 11 months in it, which does not correspond to regular 3M periods (neither 3 nor 4 periods). To fix this, define a stub period. Using the preceding example, you could define a start date for the first regular period. (This implies a short initial stub period, in this case 2 months.) You would expect the following schedule:

    expectedResultWithStub =
      [ SchedulePeriod with
          unadjustedStartDate = D.date 2018 Dec 30
          unadjustedEndDate = D.date 2019 Feb 28
          adjustedStartDate = D.date 2018 Dec 31
          adjustedEndDate = D.date 2019 Feb 28
          stubType = Some ShortInitial
      , SchedulePeriod with
          unadjustedStartDate = D.date 2019 Feb 28
          unadjustedEndDate = D.date 2019 May 30
          adjustedStartDate = D.date 2019 Feb 28
          adjustedEndDate = D.date 2019 May 30
          stubType = None
      , SchedulePeriod with
          unadjustedStartDate = D.date 2019 May 30
          unadjustedEndDate = D.date 2019 Aug 30
          adjustedStartDate = D.date 2019 May 30
          adjustedEndDate = D.date 2019 Aug 30
          stubType = None
      , SchedulePeriod with
          unadjustedStartDate = D.date 2019 Aug 30
          unadjustedEndDate = D.date 2019 Nov 30
          adjustedStartDate = D.date 2019 Aug 30
          adjustedEndDate = D.date 2019 Nov 29
          stubType = None
      ]

To configure different stub types, use the StubPeriodTypeEnum data type. You can configure whether the stub period should be at the beginning or end of the schedule, as well as whether the stub period should be longer or shorter than a regular period.

Roll out this schedule, which includes a stub period, and compare it to the expected result:

  createSchedule cals periodicScheduleWithStub === expectedResultWithStub

DayCount

Many instruments that pay interest (often expressed as an annualized rate) require an exact definition of how many days of interest belong to each payment period. The DayCount module provides functions to support such requirements. In particular, you can calculate a day count fraction (dcf) between two dates. This indicates the fraction of a full year between the dates.

For example, consider a one-year bond that pays a quarterly coupon according to a given schedule, like the one described in the previous section.

This bond does not have the same number of days in each period, which normally results in different coupon amounts for each period. Various market conventions address this and are provided in the DayCountConventionEnum.

Consider the schedule with a short initial stub in the previous section. The following example uses the Act360 day count convention:

  calcDcf Act360 (D.date 2018 Dec 31) (D.date 2019 Feb 28) === 0.1638888889

This corresponds to the 2M short initial stub period. The day count fraction can be used to calculate a bond coupon or a swap payment for the given period, by multiplying it with the yearly interest rate of the instrument (such as 4% per annum).

You can compare this initial day count fraction with those of the second and third periods of this schedule, which are both regular (3M):

  calcDcf Act360 (D.date 2019 Feb 28) (D.date 2019 May 30) === 0.2527777778
  calcDcf Act360 (D.date 2019 May 30) (D.date 2019 Aug 30) === 0.2555555556

These day count fractions are clearly greater than the one of the first period, since they correspond to 3M instead of 2M. Note that they differ slightly: they do not contain the exact same number of days.

In addition to Act360, the DayCountConventionEnum supports several other day count conventions. You can compute some of them with calcDcf, using only a start and an end date as an input as shown above. Others, such as ActActISDA, are a bit more complicated because they require additional information. You can calculate them using the calcPeriodDcf function instead. This function takes a schedule period (containing stub information) as input, as well as the schedule end date and the payment frequency:

    period = SchedulePeriod with
      unadjustedStartDate = D.date 1999 Nov 30
      unadjustedEndDate = D.date 2000 Apr 30
      adjustedStartDate = D.date 1999 Nov 30
      adjustedEndDate = D.date 2000 Apr 30
      stubType = Some LongFinal
    maturityDate = D.date 2000 Apr 30
    useAdjustedDatesForDcf = True
    frequency = Periodic Frequency with
      rollConvention = EOM; period = Period with period = M; periodMultiplier = 3

  calcPeriodDcf ActActISDA period useAdjustedDatesForDcf maturityDate frequency === 0.4155400854

Summary

You have learned about the date-related utility functions in the Daml Finance library. Here are some pointers regarding next steps, depending on what you want to do:

  • understand and validate the implementation of existing Daml Finance instruments: the implementation of the Bond package could be a good starting point. It is explained and linked in the How to leverage Contingent Claims in custom instrument implementations tutorial.
  • develop your own instruments that depend on business day shifts, schedules, or day count conventions: the Payoff Modelling tutorials describe the Contingent Claims framework step by step. You can then create the dates for your instruments using the functions described in this tutorial.