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,
};
}