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,)
)