from .math_round_div import RoundingMode, round_div from .money_amount import Money, money #: A hundred years of monthly payments, sixty of weekly ones. MAX_PAYMENTS = 3000 def _is_int(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def payment_count(annual_rate_basis_points: int, term_months: int, payments_per_year: int) -> int: """The number of payments a term holds, after checking the arguments every lending capability shares.""" if not _is_int(annual_rate_basis_points) or annual_rate_basis_points < 0 or annual_rate_basis_points > 100000: raise ValueError( "annualRateBasisPoints must be between 0 and 100000, received %r" % (annual_rate_basis_points,) ) if not _is_int(payments_per_year) or payments_per_year < 1 or payments_per_year > 52: raise ValueError("paymentsPerYear must be between 1 and 52, received %r" % (payments_per_year,)) if not _is_int(term_months) or term_months < 1: raise ValueError("termMonths must be at least 1, received %r" % (term_months,)) if (term_months * payments_per_year) % 12 != 0: raise ValueError( "a term of %d months is not a whole number of payments at %d a year" % (term_months, payments_per_year) ) n = term_months * payments_per_year // 12 if n > MAX_PAYMENTS: raise ValueError("%d payments is more than the %d payment limit" % (n, MAX_PAYMENTS)) return n def round_wide(numerator: int, denominator: int, mode: RoundingMode) -> int: """Round the exact quotient numerator / denominator (both positive) with a math.round-div mode, however large they are. Only the integer part and where the remainder sits against one half matter, so the decision is handed to round_div as a small fraction with the same integer part and the same side of the half: q, q + 1/4, q + 1/2 or q + 3/4. """ q, remainder = divmod(numerator, denominator) twice = remainder * 2 if twice == 0: return round_div(q, 1, mode) if twice == denominator: return round_div(2 * q + 1, 2, mode) return round_div(4 * q + (1 if twice < denominator else 3), 4, mode) def loan_payment( principal: Money, annual_rate_basis_points: int, term_months: int, payments_per_year: int, mode: RoundingMode, ) -> Money: """The level payment that repays ``principal`` over the term at a nominal annual rate divided equally between the periods: payment = P * r / (1 - (1 + r)^-n), r = rate / payments_per_year With r = b / D (b basis points, D = 10000 * payments_per_year) this is the exact fraction P * b * (D + b)^n / (D * ((D + b)^n - D^n)), evaluated in integers of whatever size it takes and rounded once. A zero rate is P / n. """ if not _is_int(principal.minor) or principal.minor <= 0: raise ValueError("principal must be greater than zero, received %r" % (principal.minor,)) n = payment_count(annual_rate_basis_points, term_months, payments_per_year) if annual_rate_basis_points == 0: return money(round_div(principal.minor, n, mode), principal.currency) b = annual_rate_basis_points d = 10000 * payments_per_year grown = (d + b) ** n numerator = principal.minor * b * grown denominator = d * (grown - d ** n) return money(round_wide(numerator, denominator, mode), principal.currency)