Source code for dcf.daycount

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

# dcf
# ---
# A Python library for generating discounted cashflows.
#
# Author:   sonntagsgesicht
# Version:  1.0, copyright Monday, 14 October 2024
# Website:  https://github.com/sonntagsgesicht/dcf
# License:  Apache License 2.0 (see LICENSE file)


from datetime import date, datetime
from inspect import getsource
from typing import Any as DateType  # noqa E401 E402


class _DayCount:
    r""" day count function for rate period calculation

    :param start: period start date $t_s$
    :param end: period end date $t_e$
    :returns: year fraction $\tau(t_s, t_e)$
        from **start** to **end** as a float

    this default **day_count** function calculates the number of days
    between $t_s$ and $t_e$ expressed as a fraction of a year, i.e.
    $$\tau(t_s, t_e) = \frac{t_e-t_s}{365.25}$$
    as an average year has nearly $365.25$ days.

    Since different date packages have differnet concepts to derive
    the number of days between two dates, **day_count** tries to adopt
    at least some of them. As there are:

    * dates given already as year fractions as a
      `float <https://docs.python.org/3/library/functions.html?#float>`_
      so $\tau(t_s, t_e) = t_e - t_s$.

    * `datetime <https://docs.python.org/3/library/datetime.html>`_
      the native Python package, so $\delta = t_e - t_s$ is a **timedelta**
      object with attribute **days** which is used.

    * `businessdate <https://pypi.org/project/businessdate/>`_
      a specialised package for banking business calendar
      and time period calculations,
      so the **BusinessDate** object **start** has a method
      **start.diff_in_days** which is used.

    """

    DAYS_IN_YEAR = 365.24
    DAY_COUNT = None
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(_DayCount, cls).__new__(cls)
        return cls._instance

    def __call__(self, start, end):
        if not start:
            if type(end) == datetime:  # noqa E721
                start = datetime.now()
            elif type(end) == date:  # noqa E721
                start = date.today()
            else:
                start = type(end)()
        if self.DAY_COUNT is not None:
            return self.DAY_COUNT(start, end)
        if hasattr(start, 'diff_in_days'):
            # duck typing businessdate.BusinessDate.diff_in_days
            return float(start.diff_in_days(end)) / self.DAYS_IN_YEAR
        diff = end - start
        if hasattr(diff, 'days'):
            # assume datetime.date or finance.BusinessDate (else days as float)
            return float(diff.days) / self.DAYS_IN_YEAR
        # use year fraction directly
        return float(diff)

    def set(self, day_count=None):
        self.DAY_COUNT = day_count

    def __repr__(self):
        if self.DAY_COUNT is not None:
            if isinstance(self.DAY_COUNT, type(lambda: None)):
                return 'dcf.' + getsource(self.DAY_COUNT)
            dc = getattr(self.DAY_COUNT, '__qualname__', str(self.DAY_COUNT))
            return f"dcf.day_count.set({dc})"
        return 'dcf.day_count'


day_count = _DayCount()


[docs] def origin(curve, default=0.0): return getattr(curve, 'origin', default)
[docs] def year_fraction(curve, default=None): if hasattr(curve, 'year_fraction'): return curve.year_fraction if hasattr(curve, 'day_count'): return lambda x: curve.day_count(origin(curve), x) if default is None: return lambda x: day_count(origin(curve), x) return default