from typing import List from .invest_drawdown_sustainability_types import Drawdown, DrawdownYear from .money_amount import Money, money from .money_apply_rate import apply_rate 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, )