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
- Check the sector union is non-empty before calling: set(portfolio_weights) | set(benchmark_weights)
- Fix the upstream data pipeline that produced empty weight dicts
- 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
- Assert non-empty universe before attribution
- Test pipelines with realistic fixtures that always contain sectors
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
- holdings is empty
- exposures has no factor columns
- portfolio_weights cannot be empty
- portfolio and benchmark weights must sum to the same total f
- sector {sector!r} has portfolio weight {w_p!r} but no portfo
AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28).
Data as JSON: /api/errors/0d1eb9e5903b65cf.
Report an issue: GitHub.