HKUDS/Vibe-Trading · error · ValueError

portfolio and benchmark weights must sum to the same total f

Error message

portfolio and benchmark weights must sum to the same total for the Brinson identity to hold; got {portfolio_total!r} and {benchmark_total!r} (difference {portfolio_total - benchmark_total!r} exceeds {weight_sum_tolerance!r})

What it means

The Brinson-Fachler identity (effects summing to active return) only holds when portfolio and benchmark weights sum to the same total. The function tolerates small differences up to weight_sum_tolerance and raises beyond that, including both totals and the excess in the message.

Source

Thrown at agent/src/quantlib/attribution.py:260

        Loosen the tolerance and it becomes visible in basis points -- a 2%
        weight-sum gap against a 5% benchmark return is a 10bp residual -- so
        loosen it only to absorb rounding in the weights, never to force through
        two vectors that genuinely disagree.

    Raises:
        ValueError: If no sectors were supplied, if the two weight vectors do not
            sum to the same total within ``weight_sum_tolerance``, or if a sector
            carries a non-zero weight on a side but no return on that side.
    """
    ordered: list[str] = list(portfolio_weights)
    ordered.extend(sector for sector in benchmark_weights if sector not in portfolio_weights)
    if not ordered:
        raise ValueError("brinson_fachler needs at least one sector")

    portfolio_total = math.fsum(portfolio_weights.values())
    benchmark_total = math.fsum(benchmark_weights.values())
    if abs(portfolio_total - benchmark_total) > weight_sum_tolerance:
        raise ValueError(
            "portfolio and benchmark weights must sum to the same total for the Brinson "
            f"identity to hold; got {portfolio_total!r} and {benchmark_total!r} "
            f"(difference {portfolio_total - benchmark_total!r} exceeds {weight_sum_tolerance!r})"
        )

    resolved: list[tuple[str, float, float, float, float]] = []
    for sector in ordered:
        w_p = float(portfolio_weights.get(sector, 0.0))
        w_b = float(benchmark_weights.get(sector, 0.0))
        r_p = portfolio_returns.get(sector)
        r_b = benchmark_returns.get(sector)
        if r_p is None:
            if w_p != 0.0:
                raise ValueError(f"sector {sector!r} has portfolio weight {w_p!r} but no portfolio return")
            if r_b is None:
                raise ValueError(f"sector {sector!r} has no portfolio return and no benchmark return")
            r_p = r_b
        if r_b is None:

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Reconcile/normalize both sides: divide each by its own sum (or add the missing sector with 0 return and residual weight)
  2. Fix data joins so both come from the same universe and date
  3. If the difference is genuinely tiny, raise weight_sum_tolerance deliberately

Example fix

# before
brinson_fachler({"tech": 1.0}, {"tech": 0.9, "cash": 0.1}, rp, rb)

# after
pw = {k: v / sum(portfolio_weights.values()) for k, v in portfolio_weights.items()}
bw = {k: v / sum(benchmark_weights.values()) for k, v in benchmark_weights.items()}
brinson_fachler(pw, bw, rp, rb)
Defensive patterns

Strategy: validation

Validate before calling

pt, bt = math.fsum(portfolio_weights.values()), math.fsum(benchmark_weights.values())
if abs(pt - bt) > 1e-6:
    pw = {k: v / pt for k, v in portfolio_weights.items()}
    bw = {k: v / bt for k, v in benchmark_weights.items()}

Type guard

def weights_match(pw: Mapping[str, float], bw: Mapping[str, float], tol: float = 1e-6) -> bool:
    return abs(math.fsum(pw.values()) - math.fsum(bw.values())) <= tol

Try / catch

try:
    effects = brinson_fachler(pw, bw, rp, rb)
except ValueError as e:
    if 'sum to the same total' in str(e):
        effects = brinson_fachler(normalize(pw), normalize(bw), rp, rb)
    else:
        raise

Prevention

When it happens

Trigger: Portfolio weights summing to 1.0 while benchmark sums to 0.98 (missing a cash sleeve), or to 0.0 when no weights were loaded — any mismatch larger than the tolerance (default small epsilon).

Common situations: Benchmark missing a sector present in the portfolio; weight data loaded from different as-of dates; normalization applied to one side but not the other; percentage vs fraction unit mismatch (100 vs 1.0).

Related errors


AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28). Data as JSON: /api/errors/dd7db604f8a9268c. Report an issue: GitHub.