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
There are 5 possible ways of constructing Date Tensors using tff.datetime.
datetime.datetime
, datetime.date
, or any other object with attributes or properties called year
, month
and day
.datetime64
type. Tensors
containing year, month and date as positive integers in that order.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]:
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]:
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]:
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]:
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)
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]:
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]:
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]:
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]:
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')
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]:
In [19]:
# Return the day, the month or the year from date tensors.
random_dates.day()
Out[19]:
In [20]:
random_dates.month()
Out[20]:
In [21]:
random_dates.year()
Out[21]:
In [22]:
# Or the ordinals of date tensors.
random_dates.ordinal()
Out[22]:
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]:
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]:
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)
In [26]:
# What about the day of the year?
random_dates.day_of_year()
Out[26]:
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]:
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]:
In [29]:
# Let's now shift our dates to the end of their respective months.
random_dates.to_end_of_month()
Out[29]:
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]:
In [31]:
# What about using years as the period?
random_dates.period_length_in_days(tff.datetime.year())
Out[31]:
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]:
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]:
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]:
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)
Let's now create our own Holiday Calendar using the TFF Library. To do this, we need to specify:
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
.
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]:
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]:
In [45]:
# Great, now we have both days off!
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]:
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]:
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]:
In [49]:
new_cal.subtract_business_days(dates, 6, tff.datetime.BusinessDayConvention.FOLLOWING)
Out[49]:
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:
examples coming soon
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]:
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()
In [73]:
dates = large_date_range.dates()
dates
Out[73]:
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]:
In [ ]:
# 90,403 leap years out of the 372,183 years we provided, sounds about right!