Functional Weave
Code in Python

lending.ltv@1.0.1

impl/python.py

1,965 bytes · the Python implementation · view raw

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 .lending_ltv_types import LtvBand, LtvResult
from .math_round_div import round_div  ← 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

#: 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)