Functional Weave
Code in Rust

invest.drawdown-sustainability@1.0.1

impl/python.py

2,901 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 List

from .invest_drawdown_sustainability_types import Drawdown, DrawdownYear
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_apply_rate import apply_rate  ← from money.apply-rate ^1.0.0 · built alongside by fune


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def drawdown_sustainability(
    pot: Money,
    annual_withdrawal: Money,
    annual_growth_basis_points: int,
    withdrawal_increase_basis_points: int,
    max_years: int,
) -> Drawdown:
    """Draw an income from a pot year by year, withdrawal first and growth on
    what is left, until the pot runs out or max_years is reached."""
    currency = pot.currency
    if annual_withdrawal.currency != currency:
        raise ValueError("currency mismatch: %s and %s" % (currency, annual_withdrawal.currency))
    if not _is_int(pot.minor) or pot.minor < 0:
        raise ValueError("pot must be whole minor units, 0 or more; received %s" % (pot.minor,))
    if not _is_int(annual_withdrawal.minor) or annual_withdrawal.minor <= 0:
        raise ValueError(
            "annualWithdrawal must be whole minor units greater than zero; received %s" % (annual_withdrawal.minor,)
        )
    g = annual_growth_basis_points
    if not _is_int(g) or g < -10000:
        raise ValueError("annualGrowthBasisPoints must be a whole number, -10000 or more; received %s" % (g,))
    inc = withdrawal_increase_basis_points
    if not _is_int(inc) or inc < -10000:
        raise ValueError("withdrawalIncreaseBasisPoints must be a whole number, -10000 or more; received %s" % (inc,))
    if not _is_int(max_years) or max_years < 1 or max_years > 100:
        raise ValueError("maxYears must be a whole number from 1 to 100; received %s" % (max_years,))
    balance = pot.minor
    planned = annual_withdrawal.minor
    full_years = 0
    total = 0
    exhausted = False
    schedule: List[DrawdownYear] = []
    for year in range(1, max_years + 1):
        opening = balance
        withdrawal = min(planned, opening)
        if withdrawal == planned:
            full_years += 1
        total += withdrawal
        remaining = opening - withdrawal
        growth = apply_rate(money(remaining, currency), g, "half-up").minor
        balance = remaining + growth
        schedule.append(
            DrawdownYear(
                year=year,
                opening=money(opening, currency),
                withdrawal=money(withdrawal, currency),
                growth=money(growth, currency),
                closing=money(balance, currency),
            )
        )
        if balance == 0:
            exhausted = True
            break
        planned += apply_rate(money(planned, currency), inc, "half-up").minor
    return Drawdown(
        full_years=full_years,
        exhausted=exhausted,
        total_withdrawn=money(total, currency),
        closing_balance=money(balance, currency),
        schedule=schedule,
    )