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
- Reconcile/normalize both sides: divide each by its own sum (or add the missing sector with 0 return and residual weight)
- Fix data joins so both come from the same universe and date
- 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
- Normalize both weight vectors to sum 1 before attribution
- Unit-test the weight-total invariant whenever ingestion code changes
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
- brinson_fachler needs at least one sector
- sector {sector!r} has portfolio weight {w_p!r} but no portfo
- sector {sector!r} has no portfolio return and no benchmark r
- portfolio_exposures and factor_returns share no factor; expo
- a label cannot end before the observation it belongs to star
AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28).
Data as JSON: /api/errors/dd7db604f8a9268c.
Report an issue: GitHub.