from typing import Sequence from .lending_ltv_types import LtvBand, LtvResult from .math_round_div import round_div from .money_amount import Money #: Above this, loan * 10000 is no longer exact as a JavaScript number. _MAX_LOAN = 900719925474 def loan_to_value(loan: Money, property_value: Money, bands: Sequence[LtvBand]) -> LtvResult: """Loan-to-value, and the band from the lender's table. The band is chosen on the exact ratio, loan * 10000 <= ceiling * value, never on a rounded percentage, so 75.004% is not "75%". The reported basis points are rounded up, which agrees: a whole ceiling is met exactly when the rounded-up ratio is within it. """ if loan.currency != property_value.currency: raise ValueError("currency mismatch: %s and %s" % (loan.currency, property_value.currency)) if property_value.minor <= 0: raise ValueError("propertyValue must be greater than zero, received %r" % (property_value.minor,)) if loan.minor < 0: raise ValueError("loan must not be negative, received %r" % (loan.minor,)) if loan.minor > _MAX_LOAN: raise ValueError("loan must be at most %d minor units" % (_MAX_LOAN,)) if len(bands) == 0: raise ValueError("bands must not be empty") for i, band in enumerate(bands): ceiling = band.max_basis_points if isinstance(ceiling, bool) or not isinstance(ceiling, int) or ceiling < 1: raise ValueError("band ceilings must be whole basis points of 1 or more, received %r" % (ceiling,)) if i > 0 and ceiling <= bands[i - 1].max_basis_points: raise ValueError("band ceilings must be in strictly ascending order") basis_points = round_div(loan.minor * 10000, property_value.minor, "up") for band in bands: if basis_points <= band.max_basis_points: return LtvResult(basis_points=basis_points, band=band.label) return LtvResult(basis_points=basis_points, band=None)