HKUDS/Vibe-Trading · error · ValueError

portfolio_exposures and factor_returns share no factor…

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.

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.

Appendix: 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)