Source code for finpricing.utils.day_count

from enum import Enum
import calendar
import datetime
from .date import Date
from .error import NotSupportedError
from typing import Union


# reference: http://www.eclipsesoftware.biz/DayCountConventions.html#x3_01a
# reference: https://en.wikipedia.org/wiki/Day_count_convention
# ISDA: https://www.rbccm.com/assets/rbccm/docs/legal/doddfrank/Documents/ISDALibrary/2006%20ISDA%20Definitions.pdf
# OpenGamma: https://quant.opengamma.io/Interest-Rate-Instruments-and-Market-Conventions.pdf


[docs] class DayCountTypes(Enum): ACT_360 = 0 ACT_365 = 1 # ISDA 4.16(f) THIRTY_360 = 2 ACT_ACT_ISDA = 3 # ISDA 4.16(g) Thirty_E_360 = 4 # ISDA 4.16(h) Thirty_E_360_ISDA = 5
[docs] class DayCount: def __init__(self, dccType: DayCountTypes) -> None: self._dccType = dccType
[docs] def days_between(self, start_date: Date, end_date: Date, include_end=False) -> tuple: """Return the number of days and year fraction using a specific day count convention Returns: tuple: (days, fraction of year) """ if isinstance(start_date, datetime.date): start_date = Date.from_datetime(start_date) if isinstance(end_date, datetime.date): end_date = Date.from_datetime(end_date) if self._dccType == DayCountTypes.ACT_360: if include_end: days = end_date - start_date + 1 else: days = end_date - start_date denominator = 360 frac = days / denominator elif self._dccType == DayCountTypes.ACT_365: if include_end: days = end_date - start_date + 1 else: days = end_date - start_date denominator = 365 frac = days / denominator # This follows the ISDA 4.16(f) definition elif self._dccType == DayCountTypes.THIRTY_360: y1, m1, d1 = start_date.to_tuple() y2, m2, d2 = end_date.to_tuple() d1 = 30 if d1 == 31 else d1 d2 = 30 if d2 == 31 and (d1 in [30, 31]) else d2 days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1) denominator = 360 frac = days / denominator # This follows the ISDA 4.16(g) definition elif self._dccType == DayCountTypes.Thirty_E_360: y1, m1, d1 = start_date.to_tuple() y2, m2, d2 = end_date.to_tuple() d1 = 30 if d1 == 31 else d1 d2 = 30 if d2 == 31 else d2 days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1) denominator = 360 frac = days / denominator # elif self._dccType == DayCountTypes.Thirty_E_360_ISDA: # y1, m1, d1 = start_date.to_tuple() # y2, m2, d2 = end_date.to_tuple() # d1 = 30 if d1 == 31 else d1 # d2 = 30 if d2 == 31 else d2 # days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1) # denominator = 360 # frac = days / denominator # This considers leap years in partial year calculation elif self._dccType == DayCountTypes.ACT_ACT_ISDA: denom1 = 365 + calendar.isleap(start_date.year) denom2 = 365 + calendar.isleap(end_date.year) if start_date.year == end_date.year: days = end_date - start_date frac = days / denom1 else: days1 = Date(start_date.year + 1, 1, 1) - start_date frac = days1 / denom1 days2 = end_date - Date(end_date.year, 1, 1) frac += days2 / denom2 days = end_date - start_date frac += end_date.year - start_date.year - 1 else: raise NotSupportedError("DayCountTypes is not supported") return (days, frac)
[docs] def year_fraction(self, start_date: Union[Date, datetime.date], end_date: Union[Date, datetime.date], next_coupon_date: Date = None) -> float: """Return the fraction of year between two dates using a specific day count convention The actual calculation is done in days_between() method. Args: start_date (Date): start date of the accrual period end_date (Date): end date of the accrual period. For a bond trade, it is the settlement date of the trade. next_coupon_date (Date): next coupon date of the bond / maturity date of the bond if no more coupon payment """ frac = self.days_between(start_date, end_date)[1] return frac
[docs] def convert_dates_to_times(self, anchor_date: Date, dates: list[Date]) -> list[float]: """Return a list of time fraction (year fraction) between anchor date and each date in the list Args: anchor_date (Date): anchor date dates (list[Date]): list of dates Returns: list[float]: list of time fraction (year fraction) """ return [self.year_fraction(anchor_date, date) for date in dates]
[docs] def __repr__(self) -> str: """Return the string representation of the object""" return f"DayCount({self._dccType})"
# create an alias for year_fraction year_frac = year_fraction