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,
)