HKUDS/Vibe-Trading · error · ValueError

performance must be 2-D, got shape {matrix.shape}

Error message

performance must be 2-D, got shape {matrix.shape}

What it means

probability_of_backtest_overfitting converts the performance input to a 2-D (rows x strategies) matrix via pandas and requires exactly two dimensions. Passing a flat list of returns, a 1-D array, or a 3-D structure yields a matrix whose ndim != 2, which the CSCV splitting logic cannot index, so it raises this error naming the offending shape.

Source

Thrown at agent/src/quantlib/multipletesting.py:483

        ddof: Delta degrees of freedom for the Sharpe standard deviation,
            forwarded to :func:`sharpe_ratio`.

    Returns:
        A :class:`CSCVResult`.

    Raises:
        ValueError: If ``n_splits`` is odd or below 4, if fewer than 2
            strategies are supplied (a rank needs competitors), if the sample
            cannot give each subset at least 2 rows, or if every strategy has
            zero variance so no Sharpe is defined.
    """
    if n_splits < 4 or n_splits % 2 != 0:
        raise ValueError(f"n_splits must be an even number >= 4, got {n_splits}")

    frame = pd.DataFrame(performance)
    matrix = frame.to_numpy(dtype=float)
    if matrix.ndim != 2:
        raise ValueError(f"performance must be 2-D, got shape {matrix.shape}")

    n_rows, n_strategies = matrix.shape
    if n_strategies < 2:
        raise ValueError(
            f"CSCV ranks strategies against each other and needs at least 2, "
            f"got {n_strategies}"
        )

    subset_size = n_rows // n_splits
    if subset_size < 2:
        raise ValueError(
            f"{n_rows} rows split {n_splits} ways gives {subset_size} row(s) per "
            "subset; each subset needs at least 2 for a Sharpe"
        )

    used_rows = subset_size * n_splits
    dropped = n_rows - used_rows
    trimmed = matrix[:used_rows]

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Reshape to (n_rows, n_strategies): np.asarray(performance).reshape(len(performance), -1) or pass a wide DataFrame with one column per strategy.
  2. If you truly have one strategy, PBO is undefined (see the >= 2 strategies error) — add competitors or skip the analysis.
  3. Check performance.squeeze() to drop accidental singleton dimensions before calling.

Example fix

# before
pbo = probability_of_backtest_overfitting(returns_1d, n_splits=16)  # raises

# after
wide = np.asarray(returns_list_of_strategies).T  # shape (rows, strategies)
pbo = probability_of_backtest_overfitting(wide, n_splits=16)
Defensive patterns

Strategy: type-guard

Validate before calling

perf = np.asarray(performance)
if perf.ndim != 2:
    perf = perf.reshape(-1, perf.shape[-1]) if perf.ndim == 1 else perf.squeeze()

Type guard

def is_2d_performance(p) -> bool:
    return np.asarray(p).ndim == 2

Try / catch

try:
    pbo = probability_of_backtest_overfitting(performance, n_splits)
except ValueError as e:
    if 'must be 2-D' in str(e):
        pbo = probability_of_backtest_overfitting(np.asarray(performance).reshape(-1, 1 if np.asarray(performance).ndim == 1 else -1).T, n_splits)
    else:
        raise

Prevention

When it happens

Trigger: Passing a 1-D list/array of returns for a single strategy, or a stacked 3-D array (trials x strategies x metrics), or a dict whose DataFrame conversion collapses to one dimension.

Common situations: Running PBO for a single strategy while prototyping; passing a list-of-lists-of-lists of per-trade returns; converting from a numpy tensor or xarray object that keeps extra dimensions.

Related errors


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