Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .finance_compound_interest_types import CompoundInterestResult
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .money_add import subtract_money ← from money.add ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
#: A thousand years of daily compounding is already past the point where this
#: is modelling anything; beyond it a caller has passed the wrong unit and is
#: asking for a loop that will not finish.
MAX_PERIODS = 120000
def compound_interest(
principal: Money,
annual_rate_basis_points: int,
periods_per_year: int,
years: int,
) -> CompoundInterestResult:
"""Compound interest accrued period by period, in whole minor units.
The loop is the point. A statement is built by posting a real, rounded entry
each period and letting the next period earn on the posted balance, so that
is what happens here. ``principal * (1 + r/n) ** (n*y)`` evaluated once at
the end gives a slightly different answer and cannot be reconciled against
the statement it disagrees with.
The division by ``periods_per_year`` stays inside the same rounding step as
the interest itself: a nominal 4.5% compounded monthly is 37.5 basis points
per period, and rounding that rate to 37 or 38 first would distort the
result far more than rounding the accrued pennies ever does.
"""
if isinstance(annual_rate_basis_points, bool) or not isinstance(annual_rate_basis_points, int):
raise TypeError(
"annual_rate_basis_points must be an integer, received %r" % (annual_rate_basis_points,)
)
if isinstance(periods_per_year, bool) or not isinstance(periods_per_year, int) or periods_per_year < 1:
raise ValueError("periods_per_year must be at least 1, received %r" % (periods_per_year,))
if isinstance(years, bool) or not isinstance(years, int) or years < 0:
raise ValueError("years must not be negative, received %r" % (years,))
periods = periods_per_year * years
if periods > MAX_PERIODS:
raise ValueError("%d periods is beyond the %d period limit" % (periods, MAX_PERIODS))
balance = money(principal.minor, principal.currency)
for _ in range(periods):
accrued = round_div(balance.minor * annual_rate_basis_points, 10000 * periods_per_year, "half-up")
balance = money(balance.minor + accrued, balance.currency)
return CompoundInterestResult(
principal=money(principal.minor, principal.currency),
interest=subtract_money(balance, principal),
total=balance,
periods=periods,
)