HKUDS/Vibe-Trading · error · ValueError

portfolio_exposures and factor_returns share no factor; expo

Error message

portfolio_exposures and factor_returns share no factor; exposures={sorted(exposures.index)} returns={sorted(returns.index)}

What it means

factor_return_attribution multiplies portfolio factor exposures by factor returns element-wise on shared factor names; if the two Series' indexes are disjoint there is no overlap to attribute, and the error names both sets to make the mismatch visible.

Source

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

        portfolio_return: The realised portfolio return being explained.

    Returns:
        Contribution per factor (exposure times factor return), plus a
        ``specific`` entry holding the unexplained remainder and a ``total``
        entry equal to ``portfolio_return``. The parts sum to the total by
        construction: the residual is defined as what is left, never estimated
        separately, so no reconciliation gap can appear.

    Raises:
        ValueError: If the two inputs share no factor.
    """
    exposures = pd.Series(portfolio_exposures, dtype=float).drop(
        labels=["unmatched_weight"], errors="ignore"
    )
    returns = pd.Series(factor_returns, dtype=float)
    shared = exposures.index.intersection(returns.index)
    if shared.empty:
        raise ValueError(
            "portfolio_exposures and factor_returns share no factor; "
            f"exposures={sorted(exposures.index)} returns={sorted(returns.index)}"
        )

    contributions = exposures.loc[shared] * returns.loc[shared]
    explained = float(contributions.sum())
    contributions["specific"] = portfolio_return - explained
    contributions["total"] = portfolio_return
    return contributions


def factor_risk_decomposition(
    portfolio_weights: pd.Series | Mapping[str, float],
    exposures: pd.DataFrame,
    factor_cov: pd.DataFrame,
    specific_variances: pd.Series | Mapping[str, float] | None = None,
) -> FactorRiskDecomposition:
    """Decompose portfolio risk into systematic factor and idiosyncratic components.

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Compare sorted indexes of both Series as the message displays
  2. Normalize/uppercase factor names on both sides before the call
  3. Reindex factor_returns to the exposure index after verifying the mapping

Example fix

# before
attr = factor_return_attribution(exp, rets)
# after
rets = rets.rename(dict(zip(rets.index, exp.index)))  # fix mapping
attr = factor_return_attribution(exp, rets)
Defensive patterns

Strategy: validation

Validate before calling

shared = exposures.index.intersection(returns.index)
assert not shared.empty

Try / catch

try:
    attr = factor_return_attribution(exp, rets)
except ValueError as e:
    if 'share no factor' in str(e):
        logger.error('factor taxonomy mismatch: %s', e)
    raise

Prevention

When it happens

Trigger: portfolio_exposures indexed by {'value','momentum'} while factor_returns is indexed by {'Value','MOM'} (case/name mismatch), or completely disjoint factor taxonomies.

Common situations: Factor name normalization differs between the risk model and the returns feed; one side uses prefixed names like 'fctr.value'; renaming after a merge dropped shared names.

Related errors


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