Functional Weave
Code in Rust

invest.portfolio-weights@1.0.1

impl/typescript.ts

2,756 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 { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type HoldingWeight, type PortfolioHolding, type PortfolioWeights } from "./invest_portfolio_weights_types.ts";

/** n / d rounded half away from zero; d > 0. */
function roundHalfAway(n: bigint, d: bigint): bigint {
  const negative = n < 0n;
  const a = negative ? -n : n;
  let q = a / d;
  if (2n * (a - q * d) >= d) q += 1n;
  return negative ? -q : q;
}

/**
 * Each holding's weight, target and drift. Everything is derived from the
 * integer values and rounded once at the end, so a weight is never computed
 * from another rounded figure.
 */
export function portfolioWeights(holdings: readonly PortfolioHolding[]): PortfolioWeights {
  if (holdings.length === 0) throw new RangeError("holdings must not be empty");
  const currency = holdings[0].value.currency;
  const seen = new Set<string>();
  let total = 0n;
  let targets = 0;
  for (const h of holdings) {
    if (seen.has(h.id)) throw new RangeError(`duplicate holding id "${h.id}"`);
    seen.add(h.id);
    if (h.value.currency !== currency) throw new RangeError(`currency mismatch: ${currency} and ${h.value.currency}`);
    if (!Number.isInteger(h.value.minor) || h.value.minor < 0) {
      throw new RangeError(`holding values must be whole minor units, 0 or more; received ${h.value.minor} for "${h.id}"`);
    }
    if (!Number.isInteger(h.targetBasisPoints) || h.targetBasisPoints < 0 || h.targetBasisPoints > 10000) {
      throw new RangeError(`targetBasisPoints must be a whole number from 0 to 10000; received ${h.targetBasisPoints} for "${h.id}"`);
    }
    total += BigInt(h.value.minor);
    targets += h.targetBasisPoints;
  }
  if (targets !== 10000) throw new RangeError(`targets must sum to 10000 basis points, received ${targets}`);
  if (total === 0n) throw new RangeError("the portfolio total must be greater than zero");

  let maxAbs = 0;
  const rows: HoldingWeight[] = holdings.map((h) => {
    const value = BigInt(h.value.minor);
    const target = BigInt(h.targetBasisPoints);
    const weight = Number(roundHalfAway(value * 10000n, total));
    const drift = Number(roundHalfAway(value * 10000n - target * total, total));
    const targetValue = Number(roundHalfAway(total * target, 10000n));
    maxAbs = Math.max(maxAbs, Math.abs(drift));
    return {
      id: h.id,
      value: money(h.value.minor, currency),
      weightBasisPoints: weight,
      targetBasisPoints: h.targetBasisPoints,
      driftBasisPoints: drift,
      targetValue: money(targetValue, currency),
      driftValue: money(h.value.minor - targetValue, currency),
    };
  });
  const totalMoney: Money = money(Number(total), currency);
  return { total: totalMoney, holdings: rows, maxAbsDriftBasisPoints: maxAbs };
}