Functional Weave
Code in Rust

invest.fee-drag@1.0.1

impl/typescript.ts

2,500 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 FeeDrag, type FeeDragYear } from "./invest_fee_drag_types.ts";

/**
 * The same investment grown twice, with and without an annual charge taken
 * on the year-end value, rounding to minor units every year as a statement
 * would.
 */
export function feeDrag(startValue: Money, annualGrowthBasisPoints: number, annualFeeBasisPoints: number, years: number): FeeDrag {
  if (!Number.isInteger(startValue.minor) || startValue.minor < 0) {
    throw new RangeError(`startValue must be whole minor units, 0 or more; received ${startValue.minor}`);
  }
  if (!Number.isInteger(annualGrowthBasisPoints) || annualGrowthBasisPoints < -10000) {
    throw new RangeError(`annualGrowthBasisPoints must be a whole number, -10000 or more; received ${annualGrowthBasisPoints}`);
  }
  if (!Number.isInteger(annualFeeBasisPoints) || annualFeeBasisPoints < 0 || annualFeeBasisPoints > 10000) {
    throw new RangeError(`annualFeeBasisPoints must be a whole number from 0 to 10000; received ${annualFeeBasisPoints}`);
  }
  if (!Number.isInteger(years) || years < 0 || years > 100) {
    throw new RangeError(`years must be a whole number from 0 to 100; received ${years}`);
  }
  const currency = startValue.currency;
  let without = startValue.minor;
  let withFee = startValue.minor;
  let fees = 0;
  const schedule: FeeDragYear[] = [];
  for (let year = 1; year <= years; year++) {
    without += applyRate(money(without, currency), annualGrowthBasisPoints, "half-up").minor;
    const grown = withFee + applyRate(money(withFee, currency), annualGrowthBasisPoints, "half-up").minor;
    const fee = applyRate(money(grown, currency), annualFeeBasisPoints, "half-up").minor;
    withFee = grown - fee;
    fees += fee;
    schedule.push({
      year,
      valueWithoutFee: money(without, currency),
      valueWithFee: money(withFee, currency),
      fee: money(fee, currency),
    });
  }
  const drag = without - withFee;
  let dragBasisPoints = 0;
  if (without > 0) {
    const n = BigInt(drag) * 10000n;
    const d = BigInt(without);
    const a = n < 0n ? -n : n;
    let q = a / d;
    if (2n * (a - q * d) >= d) q += 1n;
    dragBasisPoints = Number(n < 0n ? -q : q);
  }
  return {
    valueWithoutFee: money(without, currency),
    valueWithFee: money(withFee, currency),
    drag: money(drag, currency),
    dragBasisPoints,
    feesPaid: money(fees, currency),
    schedule,
  };
}