Functional Weave
Code in Rust

invest.drawdown-sustainability@1.0.1

impl/typescript.ts

2,778 bytes · the TypeScript 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.

import { applyRate } from "./money_apply_rate.ts";  ← from money.apply-rate ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type Drawdown, type DrawdownYear } from "./invest_drawdown_sustainability_types.ts";

/**
 * Draw an income from a pot year by year, withdrawal first and growth on
 * what is left, until the pot runs out or maxYears is reached.
 */
export function drawdownSustainability(
  pot: Money,
  annualWithdrawal: Money,
  annualGrowthBasisPoints: number,
  withdrawalIncreaseBasisPoints: number,
  maxYears: number
): Drawdown {
  const currency = pot.currency;
  if (annualWithdrawal.currency !== currency) throw new RangeError(`currency mismatch: ${currency} and ${annualWithdrawal.currency}`);
  if (!Number.isInteger(pot.minor) || pot.minor < 0) throw new RangeError(`pot must be whole minor units, 0 or more; received ${pot.minor}`);
  if (!Number.isInteger(annualWithdrawal.minor) || annualWithdrawal.minor <= 0) {
    throw new RangeError(`annualWithdrawal must be whole minor units greater than zero; received ${annualWithdrawal.minor}`);
  }
  if (!Number.isInteger(annualGrowthBasisPoints) || annualGrowthBasisPoints < -10000) {
    throw new RangeError(`annualGrowthBasisPoints must be a whole number, -10000 or more; received ${annualGrowthBasisPoints}`);
  }
  if (!Number.isInteger(withdrawalIncreaseBasisPoints) || withdrawalIncreaseBasisPoints < -10000) {
    throw new RangeError(`withdrawalIncreaseBasisPoints must be a whole number, -10000 or more; received ${withdrawalIncreaseBasisPoints}`);
  }
  if (!Number.isInteger(maxYears) || maxYears < 1 || maxYears > 100) {
    throw new RangeError(`maxYears must be a whole number from 1 to 100; received ${maxYears}`);
  }
  let balance = pot.minor;
  let planned = annualWithdrawal.minor;
  let fullYears = 0;
  let total = 0;
  let exhausted = false;
  const schedule: DrawdownYear[] = [];
  for (let year = 1; year <= maxYears; year++) {
    const opening = balance;
    const withdrawal = Math.min(planned, opening);
    if (withdrawal === planned) fullYears += 1;
    total += withdrawal;
    const remaining = opening - withdrawal;
    const growth = applyRate(money(remaining, currency), annualGrowthBasisPoints, "half-up").minor;
    balance = remaining + growth;
    schedule.push({
      year,
      opening: money(opening, currency),
      withdrawal: money(withdrawal, currency),
      growth: money(growth, currency),
      closing: money(balance, currency),
    });
    if (balance === 0) {
      exhausted = true;
      break;
    }
    planned += applyRate(money(planned, currency), withdrawalIncreaseBasisPoints, "half-up").minor;
  }
  return {
    fullYears,
    exhausted,
    totalWithdrawn: money(total, currency),
    closingBalance: money(balance, currency),
    schedule,
  };
}