Functional Weave
Code in Python

invest.cagr@1.0.1

impl/python.py

2,113 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 .math_fractional_power import FIXED_SCALE, fractional_power_fixed  ← from math.fractional-power ^1.0.0 · built alongside by fune
from .money_amount import Money  ← from money.amount ^1.0.0 · built alongside by fune

#: The largest integer every language holds exactly (2^53 - 1).
_MAX_SAFE = 9007199254740991


def _whole_minor(amount: Money) -> int:
    minor = amount.minor
    if isinstance(minor, bool) or not isinstance(minor, int):
        raise ValueError("amounts must be whole minor units, received %r" % (minor,))
    return minor


def _check_years(name: str, value: int) -> None:
    if isinstance(value, bool) or not isinstance(value, int) or value < 1 or value > 100000:
        raise ValueError("%s must be a whole number from 1 to 100000, received %r" % (name, value))


def _round_half_away(n: int, d: int) -> int:
    """n / d rounded half away from zero; d > 0."""
    magnitude = (abs(n) * 2 + d) // (2 * d)
    return -magnitude if n < 0 else magnitude


def cagr(start_value: Money, end_value: Money, years_numerator: int, years_denominator: int) -> int:
    """Compound annual growth rate, (end / start)^(1 / years) - 1, in basis
    points rounded half away from zero. Years are years_numerator /
    years_denominator so no float ever enters the power."""
    start = _whole_minor(start_value)
    end = _whole_minor(end_value)
    if start_value.currency != end_value.currency:
        raise ValueError("currency mismatch: %s and %s" % (start_value.currency, end_value.currency))
    _check_years("yearsNumerator", years_numerator)
    _check_years("yearsDenominator", years_denominator)
    if start <= 0:
        raise ValueError("startValue must be greater than zero, received %d" % start)
    if end < 0:
        raise ValueError("endValue must not be negative, received %d" % end)
    if end == 0:
        return -10000
    ratio = end * FIXED_SCALE // start
    # Exponent 1 / years = years_denominator / years_numerator.
    growth = fractional_power_fixed(ratio, years_denominator, years_numerator)
    bp = _round_half_away((growth - FIXED_SCALE) * 10000, FIXED_SCALE)
    if bp > _MAX_SAFE:
        raise ValueError("the growth rate is too large to express in basis points")
    return bp