HKUDS/Vibe-Trading · error · ValueError

holdings is empty

Error message

holdings is empty

What it means

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.

Source

Thrown at agent/src/quantlib/factormodel.py:506

            used as supplied, so a book that is 60% invested reports a 60%-scaled
            exposure, which is the honest reading.
        exposures: Exposure matrix, rows indexed by asset.
        benchmark: Benchmark weights by asset. When supplied, the result is the
            ACTIVE exposure (portfolio minus benchmark), which is what an
            attribution conversation is about.

    Returns:
        Exposure per factor. Assets held but absent from ``exposures`` are
        reported through the ``unmatched_weight`` entry rather than dropped, so
        a portfolio half of whose weight had no exposure data cannot read as a
        clean measurement.

    Raises:
        ValueError: If ``holdings`` is empty or ``exposures`` has no columns.
    """
    weights = pd.Series(holdings, dtype=float).dropna()
    if weights.empty:
        raise ValueError("holdings is empty")
    if exposures.shape[1] == 0:
        raise ValueError("exposures has no factor columns")

    matched = weights.index.intersection(exposures.index)
    unmatched_weight = float(weights.drop(matched).abs().sum())
    result = exposures.loc[matched].mul(weights.loc[matched], axis=0).sum()

    if benchmark is not None:
        bench = pd.Series(benchmark, dtype=float).dropna()
        bench_matched = bench.index.intersection(exposures.index)
        unmatched_weight += float(bench.drop(bench_matched).abs().sum())
        result = result - exposures.loc[bench_matched].mul(
            bench.loc[bench_matched], axis=0
        ).sum()

    result["unmatched_weight"] = unmatched_weight
    return result

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Check the holdings source: log its keys/values right before the call
  2. Skip or short-circuit reporting when the portfolio has no positions
  3. Coerce holdings with pd.Series(holdings, dtype=float).dropna() yourself and branch on .empty

Example fix

// before
exp = portfolio_style_exposure(holdings, exposures)
// after
w = pd.Series(holdings, dtype=float).dropna()
exp = portfolio_style_exposure(w, exposures) if not w.empty else None
Defensive patterns

Strategy: validation

Validate before calling

w = pd.Series(holdings, dtype=float).dropna()
assert not w.empty, 'no positions to analyze'

Prevention

When it happens

Trigger: Calling portfolio_style_exposure({}, exposures) or with a dict/Series whose values are all NaN; any holdings input that becomes an empty float Series.

Common situations: Upstream filtering removed all tickers before the call; an empty portfolio object passed straight into risk reporting; all-NaN weights from bad data joins.

Related errors


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