{"record":{"id":"dd7db604f8a9268c","repo":"HKUDS/Vibe-Trading","slug":"portfolio-and-benchmark-weights-must-sum-to-the-sa","errorCode":null,"errorMessage":"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})","messagePattern":"portfolio and benchmark weights must sum to the same total for the Brinson identity to hold; got (.+?) and (.+?) \\(difference (.+?) exceeds (.+?)\\)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/src/quantlib/attribution.py","lineNumber":260,"sourceCode":"        Loosen the tolerance and it becomes visible in basis points -- a 2%\n        weight-sum gap against a 5% benchmark return is a 10bp residual -- so\n        loosen it only to absorb rounding in the weights, never to force through\n        two vectors that genuinely disagree.\n\n    Raises:\n        ValueError: If no sectors were supplied, if the two weight vectors do not\n            sum to the same total within ``weight_sum_tolerance``, or if a sector\n            carries a non-zero weight on a side but no return on that side.\n    \"\"\"\n    ordered: list[str] = list(portfolio_weights)\n    ordered.extend(sector for sector in benchmark_weights if sector not in portfolio_weights)\n    if not ordered:\n        raise ValueError(\"brinson_fachler needs at least one sector\")\n\n    portfolio_total = math.fsum(portfolio_weights.values())\n    benchmark_total = math.fsum(benchmark_weights.values())\n    if abs(portfolio_total - benchmark_total) > weight_sum_tolerance:\n        raise ValueError(\n            \"portfolio and benchmark weights must sum to the same total for the Brinson \"\n            f\"identity to hold; got {portfolio_total!r} and {benchmark_total!r} \"\n            f\"(difference {portfolio_total - benchmark_total!r} exceeds {weight_sum_tolerance!r})\"\n        )\n\n    resolved: list[tuple[str, float, float, float, float]] = []\n    for sector in ordered:\n        w_p = float(portfolio_weights.get(sector, 0.0))\n        w_b = float(benchmark_weights.get(sector, 0.0))\n        r_p = portfolio_returns.get(sector)\n        r_b = benchmark_returns.get(sector)\n        if r_p is None:\n            if w_p != 0.0:\n                raise ValueError(f\"sector {sector!r} has portfolio weight {w_p!r} but no portfolio return\")\n            if r_b is None:\n                raise ValueError(f\"sector {sector!r} has no portfolio return and no benchmark return\")\n            r_p = r_b\n        if r_b is None:","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/quantlib/attribution.py#L242-L278","documentation":"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.","triggerScenarios":"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).","commonSituations":"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).","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"],"exampleFix":"# before\nbrinson_fachler({\"tech\": 1.0}, {\"tech\": 0.9, \"cash\": 0.1}, rp, rb)\n\n# after\npw = {k: v / sum(portfolio_weights.values()) for k, v in portfolio_weights.items()}\nbw = {k: v / sum(benchmark_weights.values()) for k, v in benchmark_weights.items()}\nbrinson_fachler(pw, bw, rp, rb)","handlingStrategy":"validation","validationCode":"pt, bt = math.fsum(portfolio_weights.values()), math.fsum(benchmark_weights.values())\nif abs(pt - bt) > 1e-6:\n    pw = {k: v / pt for k, v in portfolio_weights.items()}\n    bw = {k: v / bt for k, v in benchmark_weights.items()}","typeGuard":"def weights_match(pw: Mapping[str, float], bw: Mapping[str, float], tol: float = 1e-6) -> bool:\n    return abs(math.fsum(pw.values()) - math.fsum(bw.values())) <= tol","tryCatchPattern":"try:\n    effects = brinson_fachler(pw, bw, rp, rb)\nexcept ValueError as e:\n    if 'sum to the same total' in str(e):\n        effects = brinson_fachler(normalize(pw), normalize(bw), rp, rb)\n    else:\n        raise","preventionTips":["Normalize both weight vectors to sum 1 before attribution","Unit-test the weight-total invariant whenever ingestion code changes"],"tags":["quantlib","attribution","weights-validation"],"backgroundTag":"weights-total-mismatch","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}