Functional Weave
Code in Python

invest.portfolio-weights@1.0.1

impl/python.py

2,789 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, Sequence, Set

from .invest_portfolio_weights_types import HoldingWeight, PortfolioHolding, PortfolioWeights
from .money_amount import money  ← from money.amount ^1.0.0 · built alongside by fune


def _round_half_away(n: int, d: int) -> int:
    """n / d rounded half away from zero; d > 0."""
    a = -n if n < 0 else n
    q, r = divmod(a, d)
    if 2 * r >= d:
        q += 1
    return -q if n < 0 else q


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def portfolio_weights(holdings: Sequence[PortfolioHolding]) -> PortfolioWeights:
    """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."""
    if len(holdings) == 0:
        raise ValueError("holdings must not be empty")
    currency = holdings[0].value.currency
    seen: Set[str] = set()
    total = 0
    targets = 0
    for h in holdings:
        if h.id in seen:
            raise ValueError('duplicate holding id "%s"' % h.id)
        seen.add(h.id)
        if h.value.currency != currency:
            raise ValueError("currency mismatch: %s and %s" % (currency, h.value.currency))
        if not _is_int(h.value.minor) or h.value.minor < 0:
            raise ValueError(
                'holding values must be whole minor units, 0 or more; received %s for "%s"' % (h.value.minor, h.id)
            )
        t = h.target_basis_points
        if not _is_int(t) or t < 0 or t > 10000:
            raise ValueError(
                'targetBasisPoints must be a whole number from 0 to 10000; received %s for "%s"' % (t, h.id)
            )
        total += h.value.minor
        targets += t
    if targets != 10000:
        raise ValueError("targets must sum to 10000 basis points, received %d" % targets)
    if total == 0:
        raise ValueError("the portfolio total must be greater than zero")

    max_abs = 0
    rows: List[HoldingWeight] = []
    for h in holdings:
        value = h.value.minor
        target = h.target_basis_points
        drift = _round_half_away(value * 10000 - target * total, total)
        target_value = _round_half_away(total * target, 10000)
        max_abs = max(max_abs, abs(drift))
        rows.append(
            HoldingWeight(
                id=h.id,
                value=money(value, currency),
                weight_basis_points=_round_half_away(value * 10000, total),
                target_basis_points=target,
                drift_basis_points=drift,
                target_value=money(target_value, currency),
                drift_value=money(value - target_value, currency),
            )
        )
    return PortfolioWeights(total=money(total, currency), holdings=rows, max_abs_drift_basis_points=max_abs)