Functional Weave
Code in Rust

dates.day-count-fraction@1.0.0

impl/python.py

2,495 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from .dates_add_days import days_from_civil, epoch_day_from_iso, is_leap_year, parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_day_count_fraction_types import DayCountConvention, YearFraction


def _gcd(a: int, b: int) -> int:
    while b != 0:
        a, b = b, a % b
    return a


def _reduced(numerator: int, denominator: int) -> YearFraction:
    if numerator == 0:
        return YearFraction(numerator=0, denominator=1)
    g = _gcd(numerator, denominator)
    return YearFraction(numerator=numerator // g, denominator=denominator // g)


def day_count_fraction(start_iso: str, end_iso: str, convention: DayCountConvention) -> YearFraction:
    """The year fraction from ``start_iso`` to ``end_iso`` under a day count
    convention (2006 ISDA Definitions 4.16), as an exact fraction in lowest
    terms. Kept exact so the caller multiplies principal and rate by the
    numerator and rounds once, instead of rounding a float year fraction first.
    """
    start = parse_iso_date(start_iso)
    end = parse_iso_date(end_iso)
    if start_iso > end_iso:
        raise ValueError("startIso %s must not be after endIso %s" % (start_iso, end_iso))
    first = epoch_day_from_iso(start_iso)
    last = epoch_day_from_iso(end_iso)
    actual = last - first
    if convention == "act-365f":
        return _reduced(actual, 365)
    if convention == "act-360":
        return _reduced(actual, 360)
    if convention in ("30-360", "30e-360"):
        d1 = 30 if start.day == 31 else start.day
        d2 = end.day
        # Bond basis only trims D2 when D1 is at the month end too; Eurobond always does.
        if d2 == 31 and (convention == "30e-360" or d1 == 30):
            d2 = 30
        return _reduced(360 * (end.year - start.year) + 30 * (end.month - start.month) + (d2 - d1), 360)
    if convention == "act-act-isda":
        # Split the actual days by the calendar year they fall in: [start, end).
        leap_days = 0
        other_days = 0
        for year in range(start.year, end.year + 1):
            frm = first if year == start.year else days_from_civil(year, 1, 1)
            to = last if year == end.year else days_from_civil(year + 1, 1, 1)
            if is_leap_year(year):
                leap_days += to - frm
            else:
                other_days += to - frm
        return _reduced(other_days * 366 + leap_days * 365, 365 * 366)
    raise ValueError(
        'unknown day count convention "%s": expected act-365f, act-360, 30-360, 30e-360 or act-act-isda' % (convention,)
    )