HKUDS/Vibe-Trading · error · ValueError

brinson_fachler needs at least one sector

Error message

brinson_fachler needs at least one sector

What it means

brinson_fachler performs Brinson-Fachler performance attribution and requires at least one sector; it builds the union of portfolio and benchmark weight keys and raises ValueError when that union is empty.

Source

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

        The tie-out is exactly as good as ``weight_sum_tolerance`` allows: a
        weight-sum gap ``dW`` leaves a residual of ``-R_b * dW``, because the
        ``R_b * sum(w_p - w_b)`` term of the allocation effect no longer vanishes.
        At the default tolerance that residual is below 1e-10 and invisible.
        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:

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Check the sector union is non-empty before calling: set(portfolio_weights) | set(benchmark_weights)
  2. Fix the upstream data pipeline that produced empty weight dicts
  3. Default missing side to {} only when the other side genuinely has sectors

Example fix

# before
result = brinson_fachler({}, {}, {}, {})

# after
if not (portfolio_weights or benchmark_weights):
    raise ValueError("no sector data in period")
result = brinson_fachler(portfolio_weights, benchmark_weights, portfolio_returns, benchmark_returns)
Defensive patterns

Strategy: validation

Validate before calling

if not (set(portfolio_weights) | set(benchmark_weights)):
    raise ValueError('no sectors provided for attribution')

Type guard

def has_sectors(pw: Mapping[str, float], bw: Mapping[str, float]) -> bool:
    return bool(set(pw) | set(bw))

Prevention

When it happens

Trigger: Calling brinson_fachler({}, {}, portfolio_returns, benchmark_returns) or passing dicts whose keys are all empty; upstream code filtering sectors down to nothing before the call.

Common situations: Date-range filters or screens removing every sector; empty CSV/DataFrame slices feeding the weights; mis-wired dict comprehension producing empty mappings.

Related errors


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