Source code for businessdate.daycount

# -*- coding: utf-8 -*-

# businessdate
# ------------
# Python library for generating business dates for fast date operations
# and rich functionality.
#
# Author:   sonntagsgesicht, based on a fork of Deutsche Postbank [pbrisk]
# Version:  0.5, copyright Wednesday, 18 September 2019
# Website:  https://github.com/sonntagsgesicht/businessdate
# License:  Apache License 2.0 (see LICENSE file)


from datetime import date

from .ymd import is_leap_year


[docs] def diff_in_days(start, end): """ calculates days between start and end date """ if hasattr(start, 'to_date'): start = start.to_date() if hasattr(end, 'to_date'): end = end.to_date() return float((end-start).days)
[docs] def get_30_360(start, end): """ implements 30/360 Day Count Convention. """ # see QuantLib.Thirty360.USA start_day = min(start.day, 30) end_day = 30 if (start_day == 30 and end.day == 31) else end.day return (360 * (end.year - start.year) + 30 * (end.month - start.month) + (end_day - start_day)) / 360.0
[docs] def get_30_360b(start, end): """ implements 30/360 Bond Basis Count Convention. """ # see QuantLib.Thirty360.BondBasis start_day = min(start.day, 30) end_day = 30 if (start_day == 30 and end.day == 31) else end.day return (360 * (end.year - start.year) + 30 * (end.month - start.month) + (end_day - start_day)) / 360.0
[docs] def get_30_360_icma(start, end): """ implements 30/360 ICMA Count Convention. """ # see QuantLib.Thirty360.ISMA start_day = min(start.day, 30) end_day = 30 if (start_day == 30 and end.day == 31) else end.day return (360 * (end.year - start.year) + 30 * (end.month - start.month) + (end_day - start_day)) / 360.0
[docs] def get_30_360_isda(start, end): """ implements 30/360 ISDA Count Convention. """ # see QuantLib.Thirty360.ISDA start_day = min(start.day, 30) if (start.month == 2 and (start.day == 29 or (start.day == 28 and not is_leap_year(start.year)))): start_day = 30 end_day = min(end.day, 30) if (end.month == 2 and (end.day == 29 or (end.day == 28 and not is_leap_year(end.year)))): end_day = 30 return (360 * (end.year - start.year) + 30 * (end.month - start.month) + (end_day - start_day)) / 360.0
[docs] def get_30_360_nasd(start, end): """ implements 30/360 NASD Count Convention. """ # see QuantLib.Thirty360.NASD start_day = min(start.day, 30) end_day = 30 if (start_day == 30 and end.day == 31) else end.day return (360 * (end.year - start.year) + 30 * (end.month - start.month) + (end_day - start_day)) / 360.0
[docs] def get_30e_360(start, end): """ implements the 30E/360 Day Count Convention. """ # see QuantLib.Thirty360.European y1, m1, d1 = start.timetuple()[:3] # adjust to date immediately following the the last day y2, m2, d2 = end.timetuple()[:3] d1 = min(d1, 30) d2 = min(d2, 30) return (360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)) / 360.0
[docs] def get_30e_360b(start, end): """ implements the 30E/360 Bond Basis Day Count Convention. """ # see QuantLib.Thirty360.EurobondBasis y1, m1, d1 = start.timetuple()[:3] # adjust to date immediately following the last day y2, m2, d2 = end.timetuple()[:3] d1 = min(d1, 30) d2 = min(d2, 30) return (360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)) / 360.0
[docs] def get_30e_360g(start, end): """ implements the 30E/360 German Day Count Convention. """ # see QuantLib.Thirty360.German return get_30_360_isda(start, end)
[docs] def get_30e_360i(start, end): """ implements the 30E/360 Italian Day Count Convention. """ # see QuantLib.Thirty360.Italian y1, m1, d1 = start.timetuple()[:3] # adjust to date immediately following the last day y2, m2, d2 = end.timetuple()[:3] if (m1 == 2 and d1 >= 28) or d1 == 31: d1 = 30 if (m2 == 2 and d2 >= 28) or d2 == 31: d2 = 30 return (360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)) / 360.0
[docs] def get_act_360(start, end): """ implements Act/360 day count convention. """ return diff_in_days(start, end) / 360.0
[docs] def get_act_365(start, end): """ implements Act/365 day count convention. """ return diff_in_days(start, end) / 365.0
[docs] def get_act_36525(start, end): """ implements Act/365.25 Day Count Convention """ return diff_in_days(start, end) / 365.25
[docs] def get_act_act(start, end): """ implements Act/Act day count convention. """ return get_act_act_isda(start, end)
[docs] def get_act_act_isda(start, end): """ implements Act/Act day count convention as defined by ISDA. """ # QuantLib.ActualActual.ISDA # if the period does not lie within a year # split the days in the period as following: # remaining days of start year / years in between / days in the end year # REMARK: following the before mentioned Definition # the first day of the period is included whereas the # last day will be excluded # What remains to check now is only whether # the start and end year are leap or non-leap years. The quotients # can be easily calculated and for the years in between they are always one # (365/365 = 1; 366/366 = 1) if end.year - start.year == 0: if is_leap_year(start.year): return diff_in_days(start, end) / 366.0 # leap year: 366 days return diff_in_days(start, end) / 365.0 # non-leap year: 365 days # since the first day counts rest_year1 = diff_in_days(start, date(start.year, 12, 31)) + 1 # here the last day is automatically not counted rest_year2 = abs(diff_in_days(end, date(end.year, 1, 1))) years_in_between = end.year - start.year - 1 return (years_in_between + rest_year1 / (366.0 if is_leap_year(start.year) else 365.0) + rest_year2 / (366.0 if is_leap_year(end.year) else 365.0))
[docs] def get_act_act_bond(start, end): """ implements Act/Act day count convention known as bond basis. """ # QuantLib.ActualActual.Bond return get_act_act(start, end)
[docs] class icma: def __init__(self, frequency=1, rolling=None): """ implements ICMA day count conventions. :param frequency: coupon frequency. Either integer 1, 2, 4, 12 or string starting with 'a', 's', 'q', 'm' (optional: default is 1) :param rolling: rolling date for reference periods (optional: default is **None**, i.e. rolling date will be coupon end date ) >>> from businessdate import BusinessDate >>> today = BusinessDate() >>> from businessdate.daycount import icma >>> yf = icma(2) >>> s, e = today, today + '12m' >>> yf.get_act_act(s, e) 1.0 >>> yf.get_30_360(s, e) 1.0 >>> s, e = today, today + '9m' >>> icma(4).get_act_act(s, e) 0.75 >>> icma().get_30_360(s, e) 0.75 """ if not isinstance(frequency, int): frequency = str(frequency).lower() frequency = { '1y': 1, '4q': 1, '12m': 1, '2q': 2, '6m': 2, '1q': 4, '3m': 4, '1m': 12, }.get(frequency, frequency) if isinstance(frequency, str): frequency = { 'y': 1, 'a': 1, 's': 2, 'q': 4, 'm': 12 }.get(frequency[0]) if frequency not in (1, 2, 4, 12): msg = ("frequency must be one of 1, 2, 4 or 12 or " # "'1y', '4q', '12m', '2q', '6m', '1q', '3m' or '1m'" f"start with 'a' 'y' 's', 'q' or 'm' not {frequency!r}") raise ValueError(msg) self.frequency = frequency self.rolling = rolling
[docs] def get_act_act(self, start, end): """implements Act/Act ICMA day count convention""" if start >= end: return 0.0 if start == end else -self.get_act_act(end, start) def yf(s, e): if start <= s and e <= end: return 1 / self.frequency y = diff_in_days(max(start, s), min(end, e)) return y / diff_in_days(s, e) / self.frequency from businessdate import BusinessRange step = f"{12 // self.frequency}m" rolling = end if self.rolling is None else self.rolling r = BusinessRange(start - step, end + step, step, rolling=rolling) return sum(yf(s, e) for s, e in zip(r, r[1:]))
[docs] @staticmethod def get_30_360(start, end): """implements 30/360 ICMA day count convention""" return get_30_360_icma(start, end)
[docs] @staticmethod def gather_frequency(start, end): """gather coupon frequency from reference period :param start: period start date :param end: period end date :return: int in (1, 2, 4, 12) """ frequency = 12 // int(round(12 * diff_in_days(start, end) / 365.0)) if frequency not in (1, 2, 4, 12): frequency = 4 if frequency < 6 else 12 return frequency
[docs] def get_act_act_icma(start, end, *, frequency=None, rolling=None): """ implements Act/Act day count convention as defined by ICMA/ISMA. """ if frequency is None: frequency = icma.gather_frequency(start, end) return icma(frequency, rolling).get_act_act(start, end)
def _ql_get_act_act_icma(start, end, period_start=None, period_end=None): """ implements Act/Act ICMA day count convention. """ # QuantLib.ActualActual.Old_ISMA_Impl if start == end: return 0.0 if start > end: return -_ql_get_act_act_icma(end, start, period_start, period_end) if period_start is None: period_start = start if period_end is None: period_end = end # assert period_start < period_end period_days = diff_in_days(period_start, period_end) if period_days < 16: # QuantLib fallback period_days = diff_in_days(period_start, period_start + '1y') frequency = 12 // int(round(12 * period_days / 365.0)) # shift ref period left around end if start < end <= period_start < period_end: i = 0 while end < period_start: period_end = period_start period_start -= f"{(i + 1) * 12 // frequency}m" i += 1 # shift ref period right around start if period_start < period_end <= start < end: i = 0 while period_end < start: period_start = period_end period_end += f"{(i + 1) * 12 // frequency}m" i += 1 # calc yf def _yf(s, e, ps, pe, f): y = diff_in_days(s, e) / diff_in_days(ps, pe) / f # msg = f"QL: {s} - {e} ; {ps} - {pe} ; {f} ; {y}" # print(msg) return y # regular state if period_start <= start < end <= period_end: # regular state return _yf(start, end, period_start, period_end, frequency) # irregular start if start < period_start < end <= period_end: part = _yf(period_start, end, period_start, period_end, frequency) i = 0 new_start = period_start while True: # new_start = period_start - f"{(i + 1) * 12 // frequency}m" # new_end = period_start - f"{i * 12 // frequency}m" # perhaps wrong but meets QuantLib new_end = new_start new_start -= f"{12 // frequency}m" if new_start < start: break part += 1 / frequency i += 1 part += _yf(start, new_end, new_start, new_end, frequency) return part # irregular end if False and period_start <= start < period_end < end: part = _yf(start, period_end, period_start, period_end, frequency) i = 0 while True: new_start = period_end + f"{i * 12 // frequency}m" new_end = period_end + f"{(i + 1) * 12 // frequency}m" if end < new_end: break part += 1 / frequency i += 1 part += _yf(new_start, end, new_start, new_end, frequency) return part raise ValueError('wrong dates') def _ql_get_act_act_isma(start, end, period_start=None, period_end=None): """ implements Act/Act ICMA day count convention. """ # QuantLib.ActualActual.Old_ISMA_Impl self = _ql_get_act_act_isma if start == end: return 0.0 if start > end: return - self(end, start, period_start, period_end) if period_start is None: period_start = start if period_end is None: period_end = end period_days = diff_in_days(period_start, period_end) months = int(round(12 * period_days / 365.0)) if months == 0: period_start = start period_end = start + '1y' period_days = diff_in_days(period_start, period_end) months = 12 period = months / 12 if end <= period_end: # here period_end is a future (notional?) payment date if period_start <= start: # here period_start is the last (maybe notional) # payment date. # period_start <= start <= end <= period_end # [maybe the equality should be enforced, since # period_start < start <= end < period_end # could give wrong results] ??? return period * diff_in_days(start, end) / period_days else: # here period_start is the next (maybe notional) # payment date and period_end is the second next # (maybe notional) payment date. # start < period_start < period_end # AND end <= period_end # this case is long first coupon # the last notional payment date previous = period_start - f"{months}m" if period_start < end: return (self(start, period_start, previous, period_start) + self(period_start, end, period_start, period_end)) else: return self(start, end, previous, period_start) else: # here period_end is the last (notional?) payment date # start < period_end < end AND period_start < period_end # assert period_start <= start # now it is: period_start <= start < period_end < end # the part from start to period_end part = self(start, period_end, period_start, period_end) # the part from period_end to end # count how many regular periods are in [period_end, end], # then add the remaining time i = 0 while True: new_start = period_end + f"{months * i}m" new_end = period_end + f"{months * (i + 1)}m" if end < new_end: break else: part += period i += 1 part += self(new_start, end, new_start, new_end) return part
[docs] def get_act_act_euro(start, end): """ implements Act/Act day count convention known as euro bond. """ # QuantLib.ActualActual.Euro return get_act_act_afb(start, end)
[docs] def get_act_act_hist(start, end): """ implements Act/Act day count convention known as historical. """ # QuantLib.ActualActual.Historical return get_act_act_isda(start, end)
[docs] def get_act_act_365(start, end): """ implements Act/Act day count convention known as 365 basis. """ # QuantLib.ActualActual.Actual365 return get_act_act_isda(start, end)
[docs] def get_act_act_afb(start, end): """ implements Act/Act day count convention as defined by AFB. """ # QuantLib.ActualActual.AFB if start >= end: return 0.0 if start == end else -get_act_act_afb(end, start) _end, i = end, 0 while start < _end - f'{i + 1}y': i += 1 end = _end - f'{i}y' if end.month == 2 and end.day == 28 and is_leap_year(end.year): end = end + '1d' year_days = 365 if is_leap_year(start.year): leap_day = start.__class__(year=start.year, month=2, day=29) if start <= leap_day < end: year_days = 366 elif is_leap_year(end.year): leap_day = start.__class__(year=end.year, month=2, day=29) if start <= leap_day < end: year_days = 366 return i + diff_in_days(start, end) / year_days
[docs] def get_simple(start, end): """ implements simple day count convention as defined by QuantLib. """ """as defined in QuantLib""" return get_30_360(start, end)
[docs] def get_rational_period(start, end): """rational year fractions by ignoring days""" years = end.year - start.year months = end.month - start.month if months < 0: years -= 1 months += 12 days = end.day - start.day months += int(days / 365.25) return years + months / 12