{"record":{"id":"da160acbfbc01cd7","repo":"HKUDS/Vibe-Trading","slug":"holdings-is-empty","errorCode":null,"errorMessage":"holdings is empty","messagePattern":"holdings is empty","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/src/quantlib/factormodel.py","lineNumber":506,"sourceCode":"            used as supplied, so a book that is 60% invested reports a 60%-scaled\n            exposure, which is the honest reading.\n        exposures: Exposure matrix, rows indexed by asset.\n        benchmark: Benchmark weights by asset. When supplied, the result is the\n            ACTIVE exposure (portfolio minus benchmark), which is what an\n            attribution conversation is about.\n\n    Returns:\n        Exposure per factor. Assets held but absent from ``exposures`` are\n        reported through the ``unmatched_weight`` entry rather than dropped, so\n        a portfolio half of whose weight had no exposure data cannot read as a\n        clean measurement.\n\n    Raises:\n        ValueError: If ``holdings`` is empty or ``exposures`` has no columns.\n    \"\"\"\n    weights = pd.Series(holdings, dtype=float).dropna()\n    if weights.empty:\n        raise ValueError(\"holdings is empty\")\n    if exposures.shape[1] == 0:\n        raise ValueError(\"exposures has no factor columns\")\n\n    matched = weights.index.intersection(exposures.index)\n    unmatched_weight = float(weights.drop(matched).abs().sum())\n    result = exposures.loc[matched].mul(weights.loc[matched], axis=0).sum()\n\n    if benchmark is not None:\n        bench = pd.Series(benchmark, dtype=float).dropna()\n        bench_matched = bench.index.intersection(exposures.index)\n        unmatched_weight += float(bench.drop(bench_matched).abs().sum())\n        result = result - exposures.loc[bench_matched].mul(\n            bench.loc[bench_matched], axis=0\n        ).sum()\n\n    result[\"unmatched_weight\"] = unmatched_weight\n    return result\n","sourceCodeStart":488,"sourceCodeEnd":524,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/quantlib/factormodel.py#L488-L524","documentation":"Thrown by portfolio_style_exposure when the holdings mapping converts to an empty weight series after dropping NaNs. The function cannot compute a weighted sum of factor exposures with no positions, so it refuses immediately.","triggerScenarios":"Calling portfolio_style_exposure({}, exposures) or with a dict/Series whose values are all NaN; any holdings input that becomes an empty float Series.","commonSituations":"Upstream filtering removed all tickers before the call; an empty portfolio object passed straight into risk reporting; all-NaN weights from bad data joins.","solutions":["Check the holdings source: log its keys/values right before the call","Skip or short-circuit reporting when the portfolio has no positions","Coerce holdings with pd.Series(holdings, dtype=float).dropna() yourself and branch on .empty"],"exampleFix":"// before\nexp = portfolio_style_exposure(holdings, exposures)\n// after\nw = pd.Series(holdings, dtype=float).dropna()\nexp = portfolio_style_exposure(w, exposures) if not w.empty else None","handlingStrategy":"validation","validationCode":"w = pd.Series(holdings, dtype=float).dropna()\nassert not w.empty, 'no positions to analyze'","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Short-circuit empty portfolios before any analytics call","Centralize a portfolio_sanity_check(weights) helper used by every report"],"tags":["quantlib","factormodel","empty-input","validation"],"backgroundTag":"empty-input-validation","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}