In [ ]:
#@title Install dependencies

!pip install --upgrade tensorflow
!pip install tff-nightly

In [1]:
#@title Imports
import tensorflow as tf
import tf_quant_finance as tff
import numpy as np
import datetime
import pandas as pd

Date Tensor Essentials

Constructing DateTensors

There are 5 possible ways of constructing Date Tensors using tff.datetime.

  1. Sequence of datetime.datetime, datetime.date, or any other object with attributes or properties called year, month and day.
  2. A numpy array of datetime64 type.
  3. Sequence of (year, month, day) tuples. Months are 1-based (with January as 1) and tff.datetime.Month enum may be used instead of ints. Days are also 1-based.
  4. A tuple of three int32 Tensors containing year, month and date as positive integers in that order.
  5. A single int32 Tensor containing ordinals (i.e. number of days since 31 Dec 0 with 1 being 1 Jan 1.)

In [3]:
#@title (1) Constructing Dates: sequence of `datetime.datetime`
# Use Python's datetime library to construct a date as datetime.date(year, month, day).
dates = [datetime.date(2015, 4, 15), datetime.date(2017, 12, 30)]
# Then, convert this into a date tensor.
date_tensor = tff.datetime.dates_from_datetimes(dates)
date_tensor


Out[3]:
DateTensor: shape=(2,), contents=array([[2015,    4,   15],
       [2017,   12,   30]], dtype=int32)

In [9]:
#@title (2) Constructing Dates: a numpy array
# You can also use a numpy array of dtype datetime64 (in this case, generated via Python's datetime library).
dates_np = np.array(
  [[datetime.date(2019, 3, 25), datetime.date(2020, 6, 2)],
   [datetime.date(2020, 9, 15), datetime.date(2020, 12, 27)]],
   dtype=np.datetime64)
# Again, convert this into a date tensor.
date_tensor = tff.datetime.dates_from_np_datetimes(dates_np)
date_tensor


Out[9]:
DateTensor: shape=(2, 2), contents=array([[[2019,    3,   25],
        [2020,    6,    2]],

       [[2020,    9,   15],
        [2020,   12,   27]]], dtype=int32)

In [10]:
#@title (3) Constructing Dates: sequence of tuples
# You can start instead with a sequence of tuples.
date_tensor = tff.datetime.dates_from_tuples([(2020, 2, 25), (2020, 3, 2)])
date_tensor


Out[10]:
DateTensor: shape=(2,), contents=array([[2020,    2,   25],
       [2020,    3,    2]], dtype=int32)

In [11]:
#@title (4) Constructing Dates: a tuple of three tensors
# Another way of using tuples is to first create a tuple of three tensors for the respective Day, Month and Year. You can do this by using TensorFlow's 'constant' function as follows:
year = tf.constant([2015, 2017], dtype=tf.int32)
month = tf.constant([4, 12], dtype=tf.int32)
day = tf.constant([1, 30], dtype=tf.int32)
date_tensor = tff.datetime.dates_from_year_month_day(year, month, day)
date_tensor


Out[11]:
DateTensor: shape=(2,), contents=array([[2015,    4,    1],
       [2017,   12,   30]], dtype=int32)

In [12]:
# Note that if the days don't represent valid dates with their respective months or vice versa, you will get an `InvalidArgumentError`, e.g.:
try:
  year = tf.constant([2015, 2017], dtype=tf.int32)
  month = tf.constant([4, 12], dtype=tf.int32)
  day = tf.constant([31, 30], dtype=tf.int32)
  date_tensor = tff.datetime.dates_from_year_month_day(year, month, day)
except tf.errors.InvalidArgumentError as e:
  print (e)


Invalid day-month pairing.
Condition x <= y did not hold.
Indices of first 1 different values:
[[0]]
Corresponding x values:
[31]
Corresponding y values:
[30]
First 2 elements of x:
[31 30]
First 2 elements of y:
[30 31]

In [13]:
#@title (5) Constructing Dates: a single tensor containing ordinals
# And finally, you can create date tensors using ordinals. The ordinal value is
# defined as the number of days since 1 Jan 0001. 
# So, for example, 1 Jan 0001 has the ordinal value of 1.
ordinals = tf.constant([1], dtype=tf.int32)
date_tensor = tff.datetime.dates_from_ordinals(ordinals)
date_tensor


Out[13]:
DateTensor: shape=(1,), contents=array([[1, 1, 1]], dtype=int32)

In [14]:
# We can create more meaningful and numerous dates as follows:
ordinals = tf.constant([
    735703, 736693, 683219, 773829, 698473], dtype=tf.int32)
date_tensor = tff.datetime.dates_from_ordinals(ordinals)
date_tensor


Out[14]:
DateTensor: shape=(5,), contents=array([[2015,    4,   15],
       [2017,   12,   30],
       [1871,    8,    4],
       [2119,    9,    3],
       [1913,    5,   10]], dtype=int32)

In [15]:
# You can identify the ordinal value of a date by computing the number of days since 1 Jan 0001.
delta = datetime.date(2017,12,30) - datetime.date(1,1,1)
delta.days + 1


Out[15]:
736693

Generating random dates

To generate random dates from date tensors between specific start_dates (inclusive) and end_dates (exclusive), we can use tff.datetime.random_dates. The end_dates must be a tensor of a shape compatible with start_dates. In this case we've started with a pair and requested a size of 10, meaning that our tensor will be the shape (10, 2).


In [16]:
# Generate random dates
start_dates = tff.datetime.dates_from_tuples([
    (2020, 5, 16),
    (2020, 6, 13)
  ])
end_dates = tff.datetime.dates_from_tuples([(2021, 5, 21)])
size = 10  # Generate 10 dates for each pair of (start, end date).
random_dates = tff.datetime.random_dates(start_date=start_dates, end_date=end_dates, size=size)
random_dates


Out[16]:
DateTensor: shape=(10, 2), contents=array([[[2020,    7,   12],
        [2020,   12,    5]],

       [[2020,    7,    1],
        [2021,    4,   15]],

       [[2020,    8,   19],
        [2021,    2,   28]],

       [[2020,    9,   29],
        [2021,    4,   14]],

       [[2020,    7,   16],
        [2021,    1,   20]],

       [[2021,    1,   11],
        [2021,    3,   26]],

       [[2020,   11,   27],
        [2020,    8,    8]],

       [[2020,   10,   27],
        [2020,   10,    7]],

       [[2020,    6,   29],
        [2020,    9,    8]],

       [[2020,    6,    7],
        [2021,    5,   16]]], dtype=int32)

In [17]:
# In the following case, the start_dates shape (4) and end_dates shape (2) don't 
# broadcast, producing an error.
try:
  start_dates = tff.datetime.dates_from_tuples([
    (2020, 5, 16),
    (2020, 6, 13),
    (2020, 10, 31),
    (2020, 12, 1)
  ])
  end_dates = tff.datetime.dates_from_tuples([(2021, 5, 21), (2021, 10, 20)])
  size = 4  # Generate 4 dates for each (start, end date).
  random_dates = tff.datetime.random_dates(start_date=start_dates, end_date=end_dates, size=size)
  random_dates
except tf.errors.InvalidArgumentError:
  print('Invalid Argument Error, Incompatible shapes')


Invalid Argument Error, Incompatible shapes

Broadcasting


In [18]:
# Instead, match the end_dates by using a scalar (single date), or a matching shape of (4)
start_dates = tff.datetime.dates_from_tuples([
    (2020, 5, 16),
    (2020, 6, 13),
    (2020, 10, 31),
    (2020, 12, 1)
  ])
end_dates = tff.datetime.dates_from_tuples([
    (2021, 5, 21), 
    (2021, 10, 20), 
    (2021, 12, 5), 
    (2021, 11, 20)
  ])
size = 4  # Generate 4 dates for each (start, end date).
random_dates = tff.datetime.random_dates(
    start_date=start_dates, end_date=end_dates, size=size
    )
random_dates


Out[18]:
DateTensor: shape=(4, 4), contents=array([[[2020,   10,   31],
        [2020,    6,   16],
        [2021,    2,   25],
        [2021,    8,   10]],

       [[2021,    3,   26],
        [2020,    8,   14],
        [2021,    7,   23],
        [2021,    9,   16]],

       [[2021,    4,   16],
        [2021,    6,   18],
        [2021,    5,   17],
        [2020,   12,   11]],

       [[2020,   11,   14],
        [2021,    5,   31],
        [2021,    4,    7],
        [2021,    8,   29]]], dtype=int32)

Dates Exploration

Now that we've constructed our dates, let's see what we can do with them.


In [19]:
# Return the day, the month or the year from date tensors.
random_dates.day()


Out[19]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[31, 16, 25, 10],
       [26, 14, 23, 16],
       [16, 18, 17, 11],
       [14, 31,  7, 29]], dtype=int32)>

In [20]:
random_dates.month()


Out[20]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[10,  6,  2,  8],
       [ 3,  8,  7,  9],
       [ 4,  6,  5, 12],
       [11,  5,  4,  8]], dtype=int32)>

In [21]:
random_dates.year()


Out[21]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[2020, 2020, 2021, 2021],
       [2021, 2020, 2021, 2021],
       [2021, 2021, 2021, 2020],
       [2020, 2021, 2021, 2021]], dtype=int32)>

In [22]:
# Or the ordinals of date tensors.
random_dates.ordinal()


Out[22]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[737729, 737592, 737846, 738012],
       [737875, 737651, 737994, 738049],
       [737896, 737959, 737927, 737770],
       [737743, 737941, 737887, 738031]], dtype=int32)>

In [23]:
# We can then use the days() function to return any multiple of days. For example,
# what is the date 10 days from our date tensors?
new_dates = random_dates + tff.datetime.day()*10
new_dates


Out[23]:
DateTensor: shape=(4, 4), contents=array([[[2020,   11,   10],
        [2020,    6,   26],
        [2021,    3,    7],
        [2021,    8,   20]],

       [[2021,    4,    5],
        [2020,    8,   24],
        [2021,    8,    2],
        [2021,    9,   26]],

       [[2021,    4,   26],
        [2021,    6,   28],
        [2021,    5,   27],
        [2020,   12,   21]],

       [[2020,   11,   24],
        [2021,    6,   10],
        [2021,    4,   17],
        [2021,    9,    8]]], dtype=int32)

In [24]:
# You can also identify the corresponding day of the week of your date tensors, 
# whereby Monday is "0" and Sunday is "6", according to Python dates convention.
random_dates.day_of_week()


Out[24]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[5, 1, 3, 1],
       [4, 4, 4, 3],
       [4, 4, 0, 4],
       [5, 0, 2, 6]], dtype=int32)>

In [25]:
# To make this more intuitive, we can create a TF table with the assigned values 
# to then look up and print the corresponding day of the week.
table = tf.lookup.StaticHashTable(
    initializer=tf.lookup.KeyValueTensorInitializer(
        keys=tf.constant([0, 1, 2, 3, 4, 5, 6]),
        values=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
    ),
    default_value='Monday',
    name='days_in_week'
)

input_tensor = random_dates.day_of_week()
out = table.lookup(input_tensor)
print(out)


tf.Tensor(
[[b'Saturday' b'Tuesday' b'Thursday' b'Tuesday']
 [b'Friday' b'Friday' b'Friday' b'Thursday']
 [b'Friday' b'Friday' b'Monday' b'Friday']
 [b'Saturday' b'Monday' b'Wednesday' b'Sunday']], shape=(4, 4), dtype=string)

In [26]:
# What about the day of the year?
random_dates.day_of_year()


Out[26]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[305, 168,  56, 222],
       [ 85, 227, 204, 259],
       [106, 169, 137, 346],
       [319, 151,  97, 241]], dtype=int32)>

In [27]:
# We can also calculate the number of days until a target date
target = tff.datetime.dates_from_tuples([(2022, 3, 5)])
random_dates.days_until(target)


Out[27]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[490, 627, 373, 207],
       [344, 568, 225, 170],
       [323, 260, 292, 449],
       [476, 278, 332, 188]], dtype=int32)>

In [28]:
# Or multiple target dates, but the shapes of the dates & targets tensors must broadcast.
targets = tff.datetime.dates_from_tuples([(2020, 3, 5), (2022, 4, 5), (2023, 4, 6), (2024, 6, 8)])
random_dates.days_until(targets)


Out[28]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[-240,  658,  770, 1033],
       [-386,  599,  622,  996],
       [-407,  291,  689, 1275],
       [-254,  309,  729, 1014]], dtype=int32)>

In [29]:
# Let's now shift our dates to the end of their respective months.
random_dates.to_end_of_month()


Out[29]:
DateTensor: shape=(4, 4), contents=array([[[2020,   10,   31],
        [2020,    6,   30],
        [2021,    2,   28],
        [2021,    8,   31]],

       [[2021,    3,   31],
        [2020,    8,   31],
        [2021,    7,   31],
        [2021,    9,   30]],

       [[2021,    4,   30],
        [2021,    6,   30],
        [2021,    5,   31],
        [2020,   12,   31]],

       [[2020,   11,   30],
        [2021,    5,   31],
        [2021,    4,   30],
        [2021,    8,   31]]], dtype=int32)

Periods

Now, let's think about periods. A PeriodType can be any of the following: day, days, week, weeks, month, months, year or years. Often, this is used in conjunction with 'quantity' to calculate the quantity of periods within another period (i.e. how many months in a year).


In [30]:
# You can compute the number of days in specific periods, in this case the period
# is months: How many days are in each month in our date tensors?
random_dates.period_length_in_days(tff.datetime.month())


Out[30]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[30, 30, 28, 31],
       [31, 31, 31, 30],
       [30, 30, 31, 31],
       [30, 30, 30, 31]], dtype=int32)>

In [31]:
# What about using years as the period?
random_dates.period_length_in_days(tff.datetime.year())


Out[31]:
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
array([[365, 365, 365, 365],
       [365, 365, 365, 365],
       [365, 365, 365, 365],
       [365, 365, 365, 365]], dtype=int32)>

In [32]:
# Looks like there aren't any leap years in our dates, let's confirm. This
# function is in the 'utils' part of our library.
years = random_dates.period_length_in_days(tff.datetime.year())
tff.datetime.utils.is_leap_year(years)


Out[32]:
<tf.Tensor: shape=(4, 4), dtype=bool, numpy=
array([[False, False, False, False],
       [False, False, False, False],
       [False, False, False, False],
       [False, False, False, False]])>

In [38]:
# We can also specify the period. For example, how many days are there up to the 
# 4th and 5th month in 2020?
dates = tff.datetime.dates_from_tuples([(2020, 2, 25), (2020, 3, 2)])
periods = tff.datetime.months([4, 5])
dates.period_length_in_days(periods)


Out[38]:
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([121, 153], dtype=int32)>

Holiday Calendar

Up to this point we've been using a standard year for our dates. We can also create Holiday Calendars in order to calculate dates taking business days and holidays into account.

Creating Holiday Calendars - with Pandas

The first step will be to create our own Holiday Calendar. This can be done completely manually, however, it would then be necessary to provide holidays for each year and also adjust the holidays that fall on weekends if required. To avoid that, we can use AbstractHolidayCalendar from Pandas.


In [39]:
# Start with the necessary imports.
from pandas.tseries.holiday import AbstractHolidayCalendar
from pandas.tseries.holiday import Holiday
from pandas.tseries.holiday import nearest_workday

# Define the rules (i.e. holidays) for the Calendar.
class MyCalendar(AbstractHolidayCalendar):
    rules = [
        Holiday('NewYear', month=1, day=1, observance=nearest_workday),
        Holiday('Christmas', month=12, day=25,
                  observance=nearest_workday)
    ]
calendar = MyCalendar()
holidays_index = calendar.holidays(
    start=datetime.date(2020, 1, 1),
    end=datetime.date(2030, 12, 31))
holidays = np.array(holidays_index.to_pydatetime(), dtype="<M8[D]")
holidays


Out[39]:
array(['2020-01-01', '2020-12-25', '2021-01-01', '2021-12-24',
       '2021-12-31', '2022-12-26', '2023-01-02', '2023-12-25',
       '2024-01-01', '2024-12-25', '2025-01-01', '2025-12-25',
       '2026-01-01', '2026-12-25', '2027-01-01', '2027-12-24',
       '2027-12-31', '2028-12-25', '2029-01-01', '2029-12-25',
       '2030-01-01', '2030-12-25'], dtype='datetime64[D]')

In [40]:
# As you can see, all of the holidays have been adjusted to a week day, as would be the case for that year's Holiday Calendar.
date_tensor = tff.datetime.dates_from_np_datetimes(holidays)
input_tensor = date_tensor.day_of_week()
out = table.lookup(input_tensor)
print(out)


tf.Tensor(
[b'Wednesday' b'Friday' b'Friday' b'Friday' b'Friday' b'Monday' b'Monday'
 b'Monday' b'Monday' b'Wednesday' b'Wednesday' b'Thursday' b'Thursday'
 b'Friday' b'Friday' b'Friday' b'Friday' b'Monday' b'Monday' b'Tuesday'
 b'Tuesday' b'Wednesday'], shape=(22,), dtype=string)

Creating Holiday Calendars - manually

Let's now create our own Holiday Calendar using the TFF Library. To do this, we need to specify:

  • A Weekend Mask: Boolean Tensor of 7 elements one for each day of the week starting with Monday at index 0. A True value indicates the day is considered a weekend day and a False value implies a week day. Default value: None which means no weekends are applied. The following enums for common weekend patterns are also accepted: SATURDAY_SUNDAY, FRIDAY_SATURDAY, SUNDAY_ONLY, NONE.
  • Holidays: In this case it will be necessary to provide holidays for each year, and also adjust the holidays to that fall on weekdays if necessary.
  • Start Year: the earliest year this calendar includes
  • End Year: the latest year this calendar includes

In [41]:
# Create a calendar
cal = tff.datetime.create_holiday_calendar(weekend_mask=tff.datetime.WeekendMask.SATURDAY_SUNDAY,
                                           holidays=[(2020, 2, 25), (2020, 2, 26), (2019, 12, 25), (2019, 12, 26)], start_year=2019, end_year=2020)

In [42]:
# Now, let's test it. Is 'dates' a business day?
dates = tff.datetime.dates_from_tuples([(2020, 2, 25), (2020, 3, 20)])
cal.is_business_day(dates)


Out[42]:
<tf.Tensor: shape=(2,), dtype=bool, numpy=array([False,  True])>

In [43]:
# Rather than using a WeekendMask Enum, let's create our own for 4-day weekends.
new_cal = tff.datetime.create_holiday_calendar(weekend_mask = (0, 0, 0, 1, 1, 1, 1),
                                           holidays=[(2020, 2, 25), (2020, 2, 26), (2019, 12, 25), (2019, 12, 26)], start_year=2019, end_year=2020)

In [44]:
# Let's see if the same holds true - is 'dates' a business day?
dates = tff.datetime.dates_from_tuples([(2020, 2, 25), (2020, 3, 20)])
new_cal.is_business_day(dates)


Out[44]:
<tf.Tensor: shape=(2,), dtype=bool, numpy=array([False, False])>

In [45]:
# Great, now we have both days off!

Roll Conventions

Now that we have our holiday calendar and know how to work with dates, we can apply roll conventions to determine where the business days fall. The main argument is a BusinessDayConvention enum which determines how to roll a date that falls on a holiday (including weekends):

  • NONE: No adjustment.
  • FOLLOWING: Choose the first business day after the given holiday.
  • MODIFIED_FOLLOWING: Choose the first business day after the given holiday unless that day falls in the next calendar month, in which case choose the first business day before the holiday.
  • PRECEDING: Choose the first business day before the given holiday.
  • MODIFIED_PRECEDING: Choose the first business day before the given holiday unless that day falls in the previous calendar month, in which case choose the first business day after the holiday.

In [46]:
# Based on our four-day weekend holiday calendar, let's see what the next
# business days are according to the `FOLLOWING` Convention:
new_cal.roll_to_business_day(dates, roll_convention=tff.datetime.BusinessDayConvention.FOLLOWING)


Out[46]:
DateTensor: shape=(2,), contents=array([[2020,    3,    2],
       [2020,    3,   23]], dtype=int32)

In [47]:
# Since the first date transitions us to the following months, let's see how
# `MODIFIED_FOLLOWING` works.
new_cal.roll_to_business_day(dates, roll_convention=tff.datetime.BusinessDayConvention.MODIFIED_FOLLOWING)


Out[47]:
DateTensor: shape=(2,), contents=array([[2020,    2,   24],
       [2020,    3,   23]], dtype=int32)

In [48]:
# We can also add or subtract business days using a roll convention, where 
# the second argument is the number of days we want to add/subtract, as follows:
new_cal.add_business_days(dates, 6, tff.datetime.BusinessDayConvention.FOLLOWING)


Out[48]:
DateTensor: shape=(2,), contents=array([[2020,    3,   16],
       [2020,    4,    6]], dtype=int32)

In [49]:
new_cal.subtract_business_days(dates, 6, tff.datetime.BusinessDayConvention.FOLLOWING)


Out[49]:
DateTensor: shape=(2,), contents=array([[2020,    2,   11],
       [2020,    3,    9]], dtype=int32)

Day Count Conventions

Day count conventions are a system for determining how a coupon accumulates over a coupon period. They can also be seen as a method for converting date differences to elapsed time. The functions in this module of our library are based on the commonly used day count conventions:

  • Actual (ISDA)
  • Actual 360
  • Actual 365
  • Actual 365 fixed
  • Thirty 360 (ISDA)

examples coming soon

Scaling up


In [50]:
# Let's now consider 5 years worth of dates. We'll do this by using the 
# PeriodSchedule.dates function in the library, which is useful for creating 
# dates within a range.
start_date=tff.datetime.dates_from_tuples([(2015, 1, 1)])
end_date=tff.datetime.dates_from_tuples([(2020, 1, 1)])
tenor = tff.datetime.day()
date_range = tff.datetime.PeriodicSchedule(start_date=start_date, end_date=end_date, tenor=tenor)
date_range.dates()


Out[50]:
DateTensor: shape=(1, 1827), contents=array([[[2015,    1,    1],
        [2015,    1,    2],
        [2015,    1,    3],
        ...,
        [2019,   12,   30],
        [2019,   12,   31],
        [2020,    1,    1]]], dtype=int32)

In [71]:
# We can do this for an even bigger date range. Let's see how long that takes.
start_date=tff.datetime.dates_from_tuples([(1001, 1, 1)])
end_date=tff.datetime.dates_from_tuples([(2020, 1, 1)])
tenor = tff.datetime.day()
date_range = tff.datetime.PeriodicSchedule(start_date=start_date_alt, end_date=end_date, tenor=tenor)

In [72]:
%%timeit
date_range = tff.datetime.PeriodicSchedule(start_date=start_date, end_date=end_date, tenor=tenor)
date_range.dates()


10 loops, best of 3: 53.3 ms per loop

In [73]:
dates = large_date_range.dates()
dates


Out[73]:
DateTensor: shape=(1, 372183), contents=array([[[1001,    1,    1],
        [1001,    1,    2],
        [1001,    1,    3],
        ...,
        [2019,   12,   30],
        [2019,   12,   31],
        [2020,    1,    1]]], dtype=int32)

In [74]:
# How many leap years are within these dates?
years = dates.year()
leap_years_boolean = tff.datetime.utils.is_leap_year(years)
tf.reduce_sum(tf.cast(leap_years_boolean, tf.float32)) # Count the number of 'True' values by casting the values to floats.#


Out[74]:
<tf.Tensor: shape=(), dtype=float32, numpy=90403.0>

In [ ]:
# 90,403 leap years out of the 372,183 years we provided, sounds about right!