Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Sequence
from .dates_day_count_fraction import DayCountConvention, day_count_fraction ← from dates.day-count-fraction ^1.0.0 · built alongside by fune
from .dates_days_between import days_between ← from dates.days-between ^1.0.0 · built alongside by fune
from .lending_daily_interest_types import DailyInterest, DatedBalance
from .math_rational import add_rational, multiply_rational, rational, rational_to_integer ← from math.rational ^1.0.0 · built alongside by fune
from .math_round_div import RoundingMode ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import money ← from money.amount ^1.0.0 · built alongside by fune
def daily_interest(
balances: Sequence[DatedBalance],
from_iso: str,
to_iso: str,
annual_rate_basis_points: int,
convention: DayCountConvention,
mode: RoundingMode,
) -> DailyInterest:
"""Interest on a balance that changes during the period.
Each balance earns (or is charged) balance * rate * year fraction for the
days it was held, the year fractions come from dates.day-count-fraction,
and the pieces are added as exact fractions. Only the total is rounded,
once, so splitting a month into more balance changes never moves the
answer by a stray penny.
"""
b = annual_rate_basis_points
if isinstance(b, bool) or not isinstance(b, int) or b < -100000 or b > 100000:
raise ValueError("annualRateBasisPoints must be between -100000 and 100000, received %r" % (b,))
if len(balances) == 0:
raise ValueError("balances must not be empty")
days = days_between(from_iso, to_iso)
if days < 0:
raise ValueError("fromIso %s must not be after toIso %s" % (from_iso, to_iso))
currency = balances[0].balance.currency
for i, entry in enumerate(balances):
days_between(entry.date, entry.date)
if entry.balance.currency != currency:
raise ValueError("currency mismatch: %s and %s" % (currency, entry.balance.currency))
if i > 0 and entry.date <= balances[i - 1].date:
raise ValueError("balances must be in strictly ascending date order")
if balances[0].date > from_iso:
raise ValueError("the first balance starts on %s, after fromIso %s" % (balances[0].date, from_iso))
rate = rational(b, 10000)
total = rational(0, 1)
for i, entry in enumerate(balances):
start = entry.date if entry.date > from_iso else from_iso
following = balances[i + 1].date if i + 1 < len(balances) else to_iso
end = following if following < to_iso else to_iso
if start >= end:
continue
fraction = day_count_fraction(start, end, convention)
amount = multiply_rational(rational(entry.balance.minor, 1), rate)
total = add_rational(total, multiply_rational(amount, rational(fraction.numerator, fraction.denominator)))
return DailyInterest(interest=money(rational_to_integer(total, mode), currency), exact=total, days=days)