Coverage for rivapy/tools/datetools.py: 82%
265 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-05 14:27 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-05 14:27 +0000
1# -*- coding: utf-8 -*-
3from datetime import datetime, date
4from dateutil.relativedelta import relativedelta
5from calendar import monthrange
6from typing import List as _List, Union as _Union, Callable
7from holidays import \
8 HolidayBase as _HolidayBase, \
9 ECB as _ECB
10from rivapy.tools.enums import RollConvention, DayCounterType
11from rivapy.tools._validators import _string_to_calendar
12import logging
15# TODO: Switch to locally configured logger.
16logger = logging.getLogger(__name__)
17logger.setLevel(logging.INFO)
19class DayCounter:
21 def __init__(self, daycounter: _Union[str, DayCounterType]):
22 self._dc = DayCounterType.to_string(daycounter)
23 self._yf = DayCounter.get(self._dc)
25 def yf(self, d1: _Union[date, datetime], d2: _Union[_Union[date, datetime],_List[_Union[date, datetime]]]):
26 try:
27 result = [self._yf(d1, d2_) for d2_ in d2]
28 return result
29 except:
30 return self._yf(d1,d2)
33 @staticmethod
34 def get(daycounter: _Union[str, DayCounterType])->Callable[[ _Union[date, datetime], _Union[date, datetime]], float]:
35 dc = DayCounterType.to_string(daycounter)
36 if dc == DayCounterType.Act365Fixed.value:
37 return DayCounter.yf_Act365Fixed
38 raise NotImplementedError(dc + ' not yet implemented.')
40 @staticmethod
41 def yf_Act365Fixed(d1: _Union[date, datetime], d2: _Union[date, datetime])->float:
42 return ((d2-d1).total_seconds()/(365.0*24*60*60))
45class Period:
46 def __init__(self,
47 years: int = 0,
48 months: int = 0,
49 days: int = 0):
50 """
51 Time Period expressed in years, months and days.
53 Args:
54 years (int, optional): Number of years in time period. Defaults to 0.
55 months (int, optional): Number of months in time period. Defaults to 0.
56 days (int, optional): Number of days in time period. Defaults to 0.
57 """
58 self.years = years
59 self.months = months
60 self.days = days
62 @staticmethod
63 def from_string(period: str):
64 """Creates a Period from a string
66 Args:
67 period (str): The string defining the period. The string must be defined by the number of days/months/years followed by one of the letters 'Y'/'M'/'D', i.e. '6M' means 6 months.
69 Returns:
70 Period: The resulting period
72 Examples:
73 .. code-block:: python
75 >>> p = Period('6M') # period of 6 months
76 >>> p = Period('1Y') #period of 1 year
77 """
78 period_length = int(period[:-1])
79 period_type = period[1]
80 if period_type == 'Y':
81 return Period(years=period_length)
82 elif period_type == 'M':
83 return Period(months = period_length)
84 elif period_type == 'D':
85 return Period(days=period_length)
86 raise Exception(period + ' is not a valid period string. See documentation of tools.datetools.Period for deocumentation of valid strings.')
87 @property
88 def years(self) -> int:
89 """
90 Getter for years of period.
92 Returns:
93 int: Number of years for specified time period.
94 """
95 return self.__years
97 @years.setter
98 def years(self, years: int):
99 """
100 Setter for years of period.
102 Args:
103 years(int): Number of years.
104 """
105 self.__years = years
107 @property
108 def months(self) -> int:
109 """
110 Getter for months of period.
112 Returns:
113 int: Number of months for specified time period.
114 """
115 return self.__months
117 @months.setter
118 def months(self, months: int):
119 """
120 Setter for months of period.
122 Args:
123 months(int): Number of months.
124 """
125 self.__months = months
127 @property
128 def days(self) -> int:
129 """
130 Getter for number of days in time period.
132 Returns:
133 int: Number of days for specified time period.
134 """
135 return self.__days
137 @days.setter
138 def days(self, days: int):
139 """
140 Setter for days of period.
142 Args:
143 days(int): Number of days.
144 """
145 self.__days = days
148class Schedule:
149 def __init__(self,
150 start_day: _Union[date, datetime],
151 end_day: _Union[date, datetime],
152 time_period: _Union[Period, str],
153 backwards: bool = True,
154 stub: bool = False,
155 business_day_convention: _Union[RollConvention, str] = RollConvention.MODIFIED_FOLLOWING,
156 calendar: _Union[_HolidayBase, str] = None):
157 """
158 A schedule is a list of dates, e.g. of coupon payments, fixings, etc., which is defined by its fist (= start
159 day) and last (= end day) day, by its distance between two consecutive dates (= time period) and by the
160 procedure for rolling out the schedule, more precisely by the direction (backwards/forwards) and the dealing
161 with incomplete periods (stubs). Moreover, the schedule ensures to comply to business day conventions with
162 respect to a specified holiday calendar.
164 Args:
165 start_day (_Union[date, datetime]): Schedule's first day - beginning of the schedule.
166 end_day (_Union[date, datetime]): Schedule's last day - end of the schedule.
167 time_period (_Union[Period, str]): Time distance between two consecutive dates.
168 backwards (bool, optional): Defines direction for rolling out the schedule. True means the schedule will be
169 rolled out (backwards) from end day to start day. Defaults to True.
170 stub (bool, optional): Defines if the first/last period is accepted (True), even though it is shorter than
171 the others, or if it remaining days are added to the neighbouring period (False).
172 Defaults to True.
173 business_day_convention (_Union[RollConvention, str], optional): Set of rules defining the adjustment of
174 days to ensure each date being a business
175 day with respect to a given holiday
176 calendar. Defaults to
177 RollConvention.MODIFIED_FOLLOWING
178 calendar (_Union[_HolidayBase, str], optional): Holiday calendar defining the bank holidays of a country or
179 province (but not all non-business days as for example
180 Saturdays and Sundays).
181 Defaults (through constructor) to holidays.ECB
182 (= Target2 calendar) between start_day and end_day.
184 Examples:
186 .. code-block:: python
188 >>> from datetime import date
189 >>> from rivapy.tools import schedule
190 >>> schedule = Schedule(date(2020, 8, 21), date(2021, 8, 21), Period(0, 3, 0), True, False, RollConvention.UNADJUSTED, holidays_de).generate_dates(False),
191 [date(2020, 8, 21), date(2020, 11, 21), date(2021, 2, 21), date(2021, 5, 21), date(2021, 8, 21)])
192 """
193 self.start_day = start_day
194 self.end_day = end_day
195 self.time_period = time_period
196 self.backwards = backwards
197 self.stub = stub
198 self.business_day_convention = business_day_convention
199 self.calendar = calendar
201 @property
202 def start_day(self):
203 """
204 Getter for schedule's start date.
206 Returns:
207 Start date of specified schedule.
208 """
209 return self.__start_day
211 @start_day.setter
212 def start_day(self, start_day: _Union[date, datetime]):
213 self.__start_day = _date_to_datetime(start_day)
215 @property
216 def end_day(self):
217 """
218 Getter for schedule's end date.
220 Returns:
221 End date of specified schedule.
222 """
223 return self.__end_day
225 @end_day.setter
226 def end_day(self, end_day: _Union[date, datetime]):
227 self.__end_day = _date_to_datetime(end_day)
229 @property
230 def time_period(self):
231 """
232 Getter for schedule's time period.
234 Returns:
235 Time period of specified schedule.
236 """
237 return self.__time_period
239 @time_period.setter
240 def time_period(self, time_period: _Union[Period, str]):
241 self.__time_period = _term_to_period(time_period)
243 @property
244 def backwards(self):
245 """
246 Getter for schedule's roll out direction.
248 Returns:
249 True, if rolled out from end day to start day.
250 False, if rolled out from start day to end day.
251 """
252 return self.__backwards
254 @backwards.setter
255 def backwards(self, backwards: bool):
256 self.__backwards = backwards
258 @property
259 def stub(self):
260 """
261 Getter for potential existence of short periods (stubs).
263 Returns:
264 True, if a shorter period is allowed.
265 False, if only a longer period is allowed.
266 """
267 return self.__stub
269 @stub.setter
270 def stub(self, stub: bool):
271 self.__stub = stub
273 @property
274 def business_day_convention(self):
275 """
276 Getter for schedule's business day convention.
278 Returns:
279 Business day convention of specified schedule.
280 """
281 return self.__business_day_convention
283 @business_day_convention.setter
284 def business_day_convention(self, business_day_convention: _Union[RollConvention, str]):
285 self.__business_day_convention = RollConvention.to_string(business_day_convention)
287 @property
288 def calendar(self):
289 """
290 Getter for schedule's holiday calendar.
292 Returns:
293 Holiday calendar of specified schedule.
294 """
295 return self.__calendar
297 @calendar.setter
298 def calendar(self, calendar: _Union[_HolidayBase, str]):
299 if calendar is None:
300 self.__calendar = _ECB(years=range(self.__start_day.year, self.__end_day.year + 1))
301 else:
302 self.__calendar = _string_to_calendar(calendar)
304 @staticmethod
305 def _roll_out(from_: _Union[date, datetime], to_: _Union[date, datetime], term: Period, backwards: bool,
306 allow_stub: bool) -> _List[date]:
307 """
308 Rolls out dates from from_ to to_ in the specified direction applying the given term under consideration of the
309 specification for allowing shorter periods.
311 Args:
312 from_ (_Union[date, datetime]): Beginning of the roll out mechanism.
313 to_ (_Union[date, datetime]): End of the roll out mechanism.
314 term (Period): Difference between rolled out dates.
315 backwards (bool): Direction of roll out mechanism: backwards if True, forwards if False.
316 allow_stub (bool): Defines if periods shorter than term are allowed.
318 Returns:
319 Date schedule not yet adjusted to any business day convention.
320 """
321 # convert datetime to date (if necessary):
322 from_ = _date_to_datetime(from_)
323 to_ = _date_to_datetime(to_)
325 # check input consistency:
326 if (~backwards) & (from_ < to_):
327 direction = +1
328 elif backwards & (from_ > to_):
329 direction = -1
330 else:
331 raise Exception("From-date '" + str(from_) + "' and to-date '" + str(to_) +
332 "' are not consistent with roll direction (backwards = '" + str(backwards) + "')!")
334 # generates a list of dates ...
335 dates = []
336 # ... for forward rolling case or backward rolling case ...
337 while ((~backwards) & (from_ <= to_)) | (backwards & (to_ <= from_)):
338 dates.append(from_)
339 from_ += direction * relativedelta(years=term.years, months=term.months, days=term.days)
340 # ... and compete list for fractional periods ...
341 if dates[-1] != to_:
342 # ... by adding stub or ...
343 if allow_stub:
344 dates.append(to_)
345 # ... by extending last period.
346 else:
347 dates[-1] = to_
348 return dates
350 def generate_dates(self, ends_only: bool) -> _List[date]:
351 """
352 Generate list of schedule days according to the schedule specification, in particular with regards to business
353 day convention and calendar given.
355 Args:
356 ends_only (bool): Flag to indicate if period beginnings shall be included, e.g. for defining accrual
357 periods: True, if only period ends shall be included, e.g. for defining payment dates.
359 Returns:
360 List[date]: List of schedule dates (including start and end date) adjusted to rolling convention.
361 """
362 # roll out dates ignoring any business day issues
363 if self.__backwards:
364 schedule_dates = Schedule._roll_out(self.__end_day, self.__start_day, self.__time_period,
365 True, self.__stub)
366 schedule_dates.reverse()
367 else:
368 schedule_dates = Schedule._roll_out(self.__start_day, self.__end_day, self.__time_period,
369 False, self.__stub)
371 # adjust according to business day convention
372 rolled_schedule_dates = [roll_day(schedule_dates[0], self.__calendar, self.__business_day_convention,
373 schedule_dates[0])]
374 [rolled_schedule_dates.append(roll_day(schedule_dates[i], self.__calendar, self.__business_day_convention,
375 rolled_schedule_dates[i - 1])) for i in range(1, len(schedule_dates))]
377 if ends_only:
378 rolled_schedule_dates.pop(0)
380 logger.debug("Schedule dates successfully calculated from '"
381 + str(self.__start_day) + "' to '" + str(self.__end_day) + "'.")
382 return rolled_schedule_dates
387# class PowerSchedule:
388# def __init__(self,
389# start_day: _Union[date, datetime],
390# end_day: _Union[date, datetime],
391# time_period: _Union[Period, str],
392# backwards: bool = True,
393# business_day_convention: _Union[RollConvention, str] = RollConvention.MODIFIED_FOLLOWING,
394# calendar: _Union[_HolidayBase, str] = None):
395# """
397# Args:
398# start_day (_Union[date, datetime]): Schedule's first day - beginning of the schedule.
399# end_day (_Union[date, datetime]): Schedule's last day - end of the schedule.
400# time_period (_Union[Period, str]): Time distance between two consecutive dates.
401# backwards (bool, optional): Defines direction for rolling out the schedule. True means the schedule will be
402# rolled out (backwards) from end day to start day. Defaults to True.
403# stub (bool, optional): Defines if the first/last period is accepted (True), even though it is shorter than
404# the others, or if it remaining days are added to the neighbouring period (False).
405# Defaults to True.
406# business_day_convention (_Union[RollConvention, str], optional): Set of rules defining the adjustment of
407# days to ensure each date being a business
408# day with respect to a given holiday
409# calendar. Defaults to
410# RollConvention.MODIFIED_FOLLOWING
411# calendar (_Union[_HolidayBase, str], optional): Holiday calendar defining the bank holidays of a country or
412# province (but not all non-business days as for example
413# Saturdays and Sundays).
414# Defaults (through constructor) to holidays.ECB
415# (= Target2 calendar) between start_day and end_day.
417# Examples:
419# .. code-block:: python
421# >>> from datetime import date
422# >>> from rivapy.tools import schedule
423# >>> schedule = Schedule(date(2020, 8, 21), date(2021, 8, 21), Period(0, 3, 0), True, False, RollConvention.UNADJUSTED, holidays_de).generate_dates(False),
424# [date(2020, 8, 21), date(2020, 11, 21), date(2021, 2, 21), date(2021, 5, 21), date(2021, 8, 21)])
425# """
426# self.start_day = start_day
427# self.end_day = end_day
428# self.time_period = time_period
429# self.backwards = backwards
430# self.business_day_convention = business_day_convention
431# self.calendar = calendar
435# @property
436# def start_day(self):
437# """
438# Getter for schedule's start date.
440# Returns:
441# Start date of specified schedule.
442# """
443# return self.__start_day
445# @start_day.setter
446# def start_day(self, start_day: _Union[date, datetime]):
447# self.__start_day = _date_to_datetime(start_day)
449# @property
450# def end_day(self):
451# """
452# Getter for schedule's end date.
454# Returns:
455# End date of specified schedule.
456# """
457# return self.__end_day
459# @end_day.setter
460# def end_day(self, end_day: _Union[date, datetime]):
461# self.__end_day = _date_to_datetime(end_day)
463# @property
464# def time_period(self):
465# """
466# Getter for schedule's time period.
468# Returns:
469# Time period of specified schedule.
470# """
471# return self.__time_period
473# @time_period.setter
474# def time_period(self, time_period: _Union[Period, str]):
475# self.__time_period = _term_to_period(time_period)
477# @property
478# def backwards(self):
479# """
480# Getter for schedule's roll out direction.
482# Returns:
483# True, if rolled out from end day to start day.
484# False, if rolled out from start day to end day.
485# """
486# return self.__backwards
488# @backwards.setter
489# def backwards(self, backwards: bool):
490# self.__backwards = backwards
492# @property
493# def stub(self):
494# """
495# Getter for potential existence of short periods (stubs).
497# Returns:
498# True, if a shorter period is allowed.
499# False, if only a longer period is allowed.
500# """
501# return self.__stub
503# @stub.setter
504# def stub(self, stub: bool):
505# self.__stub = stub
507# @property
508# def business_day_convention(self):
509# """
510# Getter for schedule's business day convention.
512# Returns:
513# Business day convention of specified schedule.
514# """
515# return self.__business_day_convention
517# @business_day_convention.setter
518# def business_day_convention(self, business_day_convention: _Union[RollConvention, str]):
519# self.__business_day_convention = RollConvention.to_string(business_day_convention)
521# @property
522# def calendar(self):
523# """
524# Getter for schedule's holiday calendar.
526# Returns:
527# Holiday calendar of specified schedule.
528# """
529# return self.__calendar
531# @calendar.setter
532# def calendar(self, calendar: _Union[_HolidayBase, str]):
533# if calendar is None:
534# self.__calendar = _ECB(years=range(self.__start_day.year, self.__end_day.year + 1))
535# else:
536# self.__calendar = _string_to_calendar(calendar)
538# @staticmethod
539# def _roll_out(from_: _Union[date, datetime], to_: _Union[date, datetime], term: Period, backwards: bool,
540# allow_stub: bool) -> _List[date]:
541# """
542# Rolls out dates from from_ to to_ in the specified direction applying the given term under consideration of the
543# specification for allowing shorter periods.
545# Args:
546# from_ (_Union[date, datetime]): Beginning of the roll out mechanism.
547# to_ (_Union[date, datetime]): End of the roll out mechanism.
548# term (Period): Difference between rolled out dates.
549# backwards (bool): Direction of roll out mechanism: backwards if True, forwards if False.
550# allow_stub (bool): Defines if periods shorter than term are allowed.
552# Returns:
553# Date schedule not yet adjusted to any business day convention.
554# """
555# # convert datetime to date (if necessary):
556# from_ = _date_to_datetime(from_)
557# to_ = _date_to_datetime(to_)
559# # check input consistency:
560# if (~backwards) & (from_ < to_):
561# direction = +1
562# elif backwards & (from_ > to_):
563# direction = -1
564# else:
565# raise Exception("From-date '" + str(from_) + "' and to-date '" + str(to_) +
566# "' are not consistent with roll direction (backwards = '" + str(backwards) + "')!")
568# # generates a list of dates ...
569# dates = []
570# # ... for forward rolling case or backward rolling case ...
571# while ((~backwards) & (from_ <= to_)) | (backwards & (to_ <= from_)):
572# dates.append(from_)
573# from_ += direction * relativedelta(years=term.years, months=term.months, days=term.days)
574# # ... and compete list for fractional periods ...
575# if dates[-1] != to_:
576# # ... by adding stub or ...
577# if allow_stub:
578# dates.append(to_)
579# # ... by extending last period.
580# else:
581# dates[-1] = to_
582# return dates
584# def generate_dates(self, ends_only: bool) -> _List[date]:
585# """
586# Generate list of schedule days according to the schedule specification, in particular with regards to business
587# day convention and calendar given.
589# Args:
590# ends_only (bool): Flag to indicate if period beginnings shall be included, e.g. for defining accrual
591# periods: True, if only period ends shall be included, e.g. for defining payment dates.
593# Returns:
594# List[date]: List of schedule dates (including start and end date) adjusted to rolling convention.
595# """
596# # roll out dates ignoring any business day issues
597# if self.__backwards:
598# schedule_dates = Schedule._roll_out(self.__end_day, self.__start_day, self.__time_period,
599# True, self.__stub)
600# schedule_dates.reverse()
601# else:
602# schedule_dates = Schedule._roll_out(self.__start_day, self.__end_day, self.__time_period,
603# False, self.__stub)
605# # adjust according to business day convention
606# rolled_schedule_dates = [roll_day(schedule_dates[0], self.__calendar, self.__business_day_convention,
607# schedule_dates[0])]
608# [rolled_schedule_dates.append(roll_day(schedule_dates[i], self.__calendar, self.__business_day_convention,
609# rolled_schedule_dates[i - 1])) for i in range(1, len(schedule_dates))]
611# if ends_only:
612# rolled_schedule_dates.pop(0)
614# logger.debug("Schedule dates successfully calculated from '"
615# + str(self.__start_day) + "' to '" + str(self.__end_day) + "'.")
616# return rolled_schedule_dates
620def _date_to_datetime(date_time: _Union[datetime, date]
621 ) -> date:
622 """
623 Converts a date to a datetime or leaves it unchanged if it is already of type datetime.
625 Args:
626 date_time (_Union[datetime, date]): Date(time) to be converted.
628 Returns:
629 date: (Potentially) Converted datetime.
630 """
631 if isinstance(date_time, datetime):
632 return date_time
633 elif isinstance(date_time, date):
634 return datetime.combine(date_time, datetime.min.time())
635 else:
636 raise TypeError("'" + str(date_time) + "' must be of type datetime or date!")
639def _datetime_to_date_list(date_times: _Union[_List[datetime], _List[date]]
640 ) -> _List[date]:
641 """
642 Converts types of date list from datetime to date or leaves it unchanged if they are already of type date.
644 Args:
645 date_times (_Union[List[datetime], List[date]]): List of date(time)s to be converted.
647 Returns:
648 List[date]: List of (potentially) converted date(time)s.
649 """
650 if isinstance(date_times, list):
651 return [_date_to_datetime(date_time) for date_time in date_times]
652 else:
653 raise TypeError("'" + str(date_times) + "' must be a list of type datetime or date!")
656def _string_to_period(term: str
657 ) -> Period:
658 """
659 Converts terms, e.g. 1D, 3M, and 5Y, into periods, i.e. Period(0, 0, 1), Period(0, 3, 0), and Period(5, 0, 0),
660 respectively.
662 Args:
663 term (str): Term to be converted into a period.
665 Returns:
666 Period: Period corresponding to the term specified.
667 """
668 unit = term[-1]
669 measure = int(term[:-1])
671 if unit.upper() == 'D':
672 period = Period(0, 0, measure)
673 elif unit.upper() == 'M':
674 period = Period(0, measure, 0)
675 elif unit.upper() == 'Y':
676 period = Period(measure, 0, 0)
677 else:
678 raise Exception("Unknown term! Please use: 'D', 'M', or 'Y'.")
680 return period
683def _term_to_period(term: _Union[Period, str]
684 ) -> Period:
685 """
686 Converts a term provided as period or string into period format if necessary.
688 Args:
689 term (_Union[Period, str]): Tenor to be converted if provided as string.
691 Returns:
692 Period: Tenor (potentially converted) in(to) period format.
693 """
694 if isinstance(term, Period):
695 return term
696 elif isinstance(term, str):
697 return _string_to_period(term)
698 else:
699 raise TypeError("The term '" + str(term) + "' must be provided as Period or string!")
702def calc_end_day(start_day: _Union[date, datetime],
703 term: str,
704 business_day_convention: _Union[RollConvention, str] = None,
705 calendar: _Union[_HolidayBase, str] = None
706 ) -> date:
707 """
708 Derives the end date of a time period based on the start day the the term given as string, e.g. 1D, 3M, or 5Y.
709 If business day convention and corresponding calendar are provided the end date is additionally rolled accordingly.
711 Args:
712 start_day (_Union[date, datetime): Beginning of the time period with length term.
713 term (str): Term defining the period from start to end date.
714 business_day_convention (_Union[RollConvention, str], optional): Set of rules defining how to adjust
715 non-business days. Defaults to None.
716 calendar (_Union[_HolidayBase, str], optional): Holiday calender defining non-business days
717 (but not Saturdays and Sundays).
718 Defaults to None.
720 Returns:
721 date: End date potentially adjusted according to the specified business day convention with respect to the given
722 calendar.
723 """
724 start_date = _date_to_datetime(start_day)
725 period = _term_to_period(term)
726 end_date = start_date + relativedelta(years=period.years, months=period.months, days=period.days)
727 if (business_day_convention is not None) & (calendar is not None):
728 end_date = roll_day(end_date, calendar, business_day_convention, start_date)
730 return end_date
733def calc_start_day(end_day: _Union[date, datetime],
734 term: str,
735 business_day_convention: _Union[RollConvention, str] = None,
736 calendar: _Union[_HolidayBase, str] = None
737 ) -> date:
738 """
739 Derives the start date of a time period based on the end day the the term given as string, e.g. 1D, 3M, or 5Y.
740 If business day convention and corresponding calendar are provided the start date is additionally rolled
741 accordingly.
743 Args:
744 end_day (_Union[date, datetime): End of the time period with length term.
745 term (str): Term defining the period from start to end date.
746 business_day_convention (_Union[RollConvention, str], optional): Set of rules defining how to adjust
747 non-business days. Defaults to None.
748 calendar (_Union[_HolidayBase, str], optional): Holiday calender defining non-business days
749 (but not Saturdays and Sundays).
750 Defaults to None.
751 Returns:
752 date: Start date potentially adjusted according to the specified business day convention with respect to the
753 given calendar.
754 """
755 end_date = _date_to_datetime(end_day)
756 period = _term_to_period(term)
757 start_date = end_date - relativedelta(years=period.years, months=period.months, days=period.days)
758 if (business_day_convention is not None) & (calendar is not None):
759 start_date = roll_day(start_date, calendar, business_day_convention)
761 return start_date
764def last_day_of_month(day: _Union[date, datetime]
765 ) -> date:
766 """
767 Derives last day of the month corresponding to the given day.
769 Args:
770 day (_Union[date, datetime]): Day defining month and year for derivation of month's last day.
772 Returns:
773 date: Date of last day of the corresponding month.
774 """
775 return date(day.year, day.month, monthrange(day.year, day.month)[1])
778def is_last_day_of_month(day: _Union[date, datetime]
779 ) -> bool:
780 """
781 Checks if a given day is the last day of the corresponding month.
783 Args:
784 day (_Union[date, datetime]): Day to be checked.
786 Returns:
787 bool: True, if day is last day of the month, False otherwise.
788 """
789 return _date_to_datetime(day) == last_day_of_month(day)
792def is_business_day(day: _Union[date, datetime],
793 calendar: _Union[_HolidayBase, str]
794 ) -> bool:
795 """
796 Checks if a given day is a business day in a given calendar.
798 Args:
799 day (_Union[date, datetime]): Day to be checked.
800 calendar (_Union[_HolidayBase, str]): List of holidays defined by the given calendar.
802 Returns:
803 bool: True, if day is a business day, False otherwise.
804 """
805 # TODO: adjust for countries with weekend not on Saturday/Sunday (http://worldmap.workingdays.org/)
806 return (day.isoweekday() < 6) & (day not in _string_to_calendar(calendar))
809def last_business_day_of_month(day: _Union[date, datetime],
810 calendar: _Union[_HolidayBase, str]
811 ) -> date:
812 """
813 Derives the last business day of a month corresponding to a given day based on the holidays set in the calendar.
815 Args:
816 day (_Union[date, datetime]): Day defining month and year for deriving the month's last business day.
817 calendar (_Union[_HolidayBase, str]): List of holidays defined by the given calendar.
819 Returns:
820 date: Date of last business day of the corresponding month.
821 """
822 check_day = date(day.year, day.month, monthrange(day.year, day.month)[1])
823 while not (is_business_day(check_day, calendar)):
824 check_day -= relativedelta(days=1)
825 return check_day
828def is_last_business_day_of_month(day: _Union[date, datetime],
829 calendar: _Union[_HolidayBase, str]
830 ) -> bool:
831 """
832 Checks it the given day is the last business day of the corresponding month.
834 Args:
835 day (_Union[date, datetime]): day to be checked
836 calendar (_Union[_HolidayBase, str]): list of holidays defined by the given calendar
838 Returns:
839 bool: True if day is last business day of the corresponding month, False otherwise.
840 """
841 return _date_to_datetime(day) == last_business_day_of_month(day, calendar)
844def nearest_business_day(day: _Union[date, datetime],
845 calendar: _Union[_HolidayBase, str],
846 following_first: bool = True
847 ) -> date:
848 """
849 Derives nearest business day from given day for a given calendar. If there are equally near days preceding and
850 following the flag following_first determines if the following day is preferred to the preceding one.
852 Args:
853 day (_Union[date, datetime]): Day for which the nearest business day is to be found.
854 calendar (_Union[_HolidayBase, str]): List of holidays given by calendar.
855 following_first (bool): Flag for deciding if following days are preferred to an equally near preceding day.
856 Default value is True.
858 Returns:
859 date: Nearest business day to given day according to given calendar.
860 """
861 distance = 0
862 if following_first:
863 direction = -1
864 else:
865 direction = +1
867 day = _date_to_datetime(day)
868 while not is_business_day(day, calendar):
869 distance += 1
870 direction *= -1
871 day += direction * relativedelta(days=distance)
872 return day
875def nearest_last_business_day_of_month(day: _Union[date, datetime],
876 calendar: _Union[_HolidayBase, str],
877 following_first: bool = True
878 ) -> date:
879 """
880 Derives nearest last business day of a month from given day for a given calendar. If there are equally near days
881 preceding and following the flag following_first determines if the following day is preferred to the preceding one.
883 Args:
884 day (_Union[date, datetime]): Day for which the nearest last business day of the month is to be found.
885 calendar (_Union[_HolidayBase, str]): List of holidays given by calendar.
886 following_first (bool, optional): Flag for deciding if following days are preferred to an equally near preceding
887 day. Defaults to True.
889 Returns:
890 date: Nearest last business day of a month to given day according to given calendar.
891 """
892 distance = 0
893 if following_first:
894 direction = -1
895 else:
896 direction = +1
898 day = _date_to_datetime(day)
899 while not is_last_business_day_of_month(day, calendar):
900 distance += 1
901 direction *= -1
902 day += direction * relativedelta(days=distance)
903 return day
906def next_or_previous_business_day(day: _Union[date, datetime],
907 calendar: _Union[_HolidayBase, str],
908 following_first: bool
909 ) -> date:
910 """
911 Derives the preceding or following business day to a given day according to a given calendar depending on the flag
912 following_first. If the day is already a business day the function directly returns the day.
914 Args:
915 day (_Union[date, datetime]): Day for which the preceding or following business day is to be found.
916 calendar (_HolidayBase): List of holidays defined by the calendar.
917 following_first (bool): Flag to determine in the following (True) or preceding (False) business day is to be
918 found.
920 Returns:
921 date: Preceding or following business day, respectively, or day itself if it is a business day.
922 """
923 if following_first:
924 direction = +1
925 else:
926 direction = -1
928 day = _date_to_datetime(day)
929 while not is_business_day(day, calendar):
930 day += direction * relativedelta(days=1)
932 return day
935def following(day: _Union[date, datetime],
936 calendar: _Union[_HolidayBase, str]
937 ) -> date:
938 """
939 Derives the (potentially) adjusted business day according to the business day convention 'Following' for a specified
940 day with respect to a specific calendar: The adjusted date is the following good business day.
942 Args:
943 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day.
944 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends.
946 Returns:
947 date: Adjusted business day according to the roll convention 'Following' with respect to calendar if the day is
948 not already a business day. Otherwise the (unadjusted) day is returned.
949 """
950 return next_or_previous_business_day(day, calendar, True)
953def preceding(day: _Union[date, datetime],
954 calendar: _Union[_HolidayBase, str]
955 ) -> date:
956 """
957 Derives the (potentially) adjusted business day according to the business day convention 'Preceding' for a specified
958 day with respect to a specific calendar: The adjusted date is the preceding good business day.
960 Args:
961 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day.
962 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends.
964 Returns:
965 date: Adjusted business day according to the roll convention 'Preceding' with respect to calendar if the day is
966 not already a business day. Otherwise the (unadjusted) day is returned.
967 """
968 return next_or_previous_business_day(day, calendar, False)
971def modified_following(day: _Union[date, datetime],
972 calendar: _Union[_HolidayBase, str]
973 ) -> date:
974 """
975 Derives the (potentially) adjusted business day according to the business day convention 'Modified Following' for a
976 specified day with respect to a specific calendar: The adjusted date is the following good business day unless the
977 day is in the next calendar month, in which case the adjusted date is the preceding good business day.
979 Args:
980 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day.
981 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends.
983 Returns:
984 date: Adjusted business day according to the roll convention 'Modified Following' with respect to calendar if
985 the day is not already a business day. Otherwise the (unadjusted) day is returned.
986 """
987 next_day = next_or_previous_business_day(day, calendar, True)
988 if next_day.month > day.month:
989 return preceding(day, calendar)
990 else:
991 return next_day
994def modified_following_eom(day: _Union[date, datetime],
995 calendar: _Union[_HolidayBase, str],
996 start_day: _Union[date, datetime]
997 ) -> date:
998 """
999 Derives the (potentially) adjusted business day according to the business day convention 'End of Month' for a
1000 specified day with respect to a specific calendar: Where the start date of a period is on the final business day of
1001 a particular calendar month, the end date is on the final business day of the end month (not necessarily the
1002 corresponding date in the end month).
1004 Args:
1005 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day.
1006 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends.
1007 start_day (_Union[date, datetime]): Day at which the period under consideration begins.
1009 Returns:
1010 date: Adjusted business day according to the roll convention 'End of Month' with respect to calendar.
1011 """
1012 if isinstance(start_day, date) | isinstance(start_day, datetime):
1013 if is_last_business_day_of_month(start_day, calendar):
1014 return nearest_last_business_day_of_month(day, calendar)
1015 else:
1016 return modified_following(day, calendar)
1017 else:
1018 raise Exception('The roll convention ' + str(RollConvention.MODIFIED_FOLLOWING_EOM)
1019 + ' cannot be evaluated without a start_day')
1022def modified_following_bimonthly(day: _Union[date, datetime],
1023 calendar: _Union[_HolidayBase, str]
1024 ) -> date:
1025 """
1026 Derives the (potentially) adjusted business day according to the business day convention 'Modified Following
1027 Bimonthly' for a specified day with respect to a specific calendar: The adjusted date is the following good business
1028 day unless that day crosses the mid-month (15th) or end of a month, in which case the adjusted date is the preceding
1029 good business day.
1031 Args:
1032 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day.
1033 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends.
1035 Returns:
1036 date: Adjusted business day according to the roll convention 'Modified Following Bimonthly' with respect to
1037 calendar if the day is not already a business day. Otherwise the (unadjusted) day is returned.
1038 """
1039 next_day = next_or_previous_business_day(day, calendar, True)
1040 if (next_day.month > day.month) | ((next_day.day > 15) & (day.day <= 15)):
1041 return preceding(day, calendar)
1042 else:
1043 return next_day
1046def modified_preceding(day: _Union[date, datetime],
1047 calendar: _Union[_HolidayBase, str]
1048 ) -> date:
1049 """
1050 Derives the (potentially) adjusted business day according to the business day convention 'Modified Preceding' for a
1051 specified day with respect to a specific calendar: The adjusted date is the preceding good business day unless the
1052 day is in the previous calendar month, in which case the adjusted date is the following good business day.
1054 Args:
1055 day (_Union[date, datetime]): Day to be adjusted according to the roll convention if not already a business day.
1056 calendar (_Union[_HolidayBase, str]): Calendar defining holidays additional to weekends.
1058 Returns:
1059 date: Adjusted business day according to the roll convention 'Modified Preceding' with respect to calendar if
1060 the day is not already a business day. Otherwise the (unadjusted) day is returned.
1061 """
1062 prev_day = next_or_previous_business_day(day, calendar, False)
1063 if prev_day.month < day.month:
1064 return following(day, calendar)
1065 else:
1066 return prev_day
1069# to be used in the switcher (identical argument list)
1070def unadjusted(day: _Union[date, datetime],
1071 _
1072 ) -> date:
1073 """
1074 Leaves the day unchanged independent from the fact if it is already a business day.
1076 Args:
1077 day (_Union[date, datetime]): Day to be adjusted according to the roll convention.
1078 _: Placeholder for calendar argument.
1080 Returns:
1081 date: Unadjusted day.
1082 """
1083 return _date_to_datetime(day)
1086def roll_day(day: _Union[date, datetime],
1087 calendar: _Union[_HolidayBase, str],
1088 business_day_convention: _Union[RollConvention, str],
1089 start_day: _Union[date, datetime] = None
1090 ) -> date:
1091 """
1092 Adjusts a given day according to the specified business day convention with respect to a given calendar or if the
1093 given day falls on a Saturday or Sunday. For some roll conventions not only the (end) day to be adjusted but also
1094 the start day of a period is relevant for the adjustment of the given (end) day.
1096 Args:
1097 day (_Union[date, datetime]): Day to be adjusted if it is a non-business day.
1098 calendar (_Union[_HolidayBase, str]): Holiday calendar defining non-business days (but not weekends).
1099 business_day_convention (_Union[RollConvention, str]): Set of rules defining how to adjust non-business days.
1100 start_day (_Union[date, datetime], optional): Period's start day that may influence the rolling of the end day.
1101 Defaults to None.
1103 Returns:
1104 date: Adjusted day.
1105 """
1106 roll_convention = RollConvention.to_string(business_day_convention)
1107 #if start_day is not None:
1108 # start_day = _date_to_datetime(start_day)
1110 switcher = {
1111 'Unadjusted': unadjusted,
1112 'Following': following,
1113 'ModifiedFollowing': modified_following,
1114 'ModifiedFollowingEOM': modified_following_eom,
1115 'ModifiedFollowingBimonthly': modified_following_bimonthly,
1116 'Nearest': nearest_business_day,
1117 'Preceding': preceding,
1118 'ModifiedPreceding': modified_preceding
1119 }
1120 # Get the appropriate roll function from switcher dictionary
1121 roll_func = switcher.get(roll_convention, lambda: "Business day convention '" + str(business_day_convention)
1122 + "' is not known!")
1123 try:
1124 result = roll_func(day, calendar)
1125 except TypeError:
1126 result = roll_func(day, calendar, start_day)
1128 return result