跳到正文

组合清仓交叉验证 (CPCV)

CPCV 通过枚举所有可能的 train/test 划分来扩展 purged K-fold,实现稳健的性能估计和 PBO 计算。

第 8 篇 · 共 9 篇 约 3 分钟

标准 purged K-fold 给你 k 个 train/test 划分。CPCV 给你所有 2^k - 1 个 k 折的非空子集作为训练集,其余作为测试集。这种穷尽性使得 PBO(回测过拟合概率)计算成为可能。

标准 Purged K-Fold 的核心问题

Purged K-fold 是比天真 K-fold 的一步升级。但它仍然有一个根本限制:k 折只能给你 k 个不同的 train/test 划分。

如果你划分数据的方式恰好与有利的市场结构对齐怎么办?你的 OOS 估计可能偏乐观——只是运气好,划分碰巧合适。

更广泛地说:一种划分不能告诉你策略表现是否在不同划分方式下稳定。你需要多种划分。

CPCV 如何工作

CPCV 选择一个数字 k,然后枚举那些 k 折的所有非空子集作为训练集。剩余的折成为测试集。

对于 k=6 折(说明性):

折组合       | 训练折    | 测试折
------------|----------|----------
{1}         | 折 1     | 折 2,3,4,5,6
{2}         | 折 2     | 折 1,3,4,5,6
{3}         | 折 3     | 折 1,2,4,5,6
{1,2}       | 折 1,2   | 折 3,4,5,6
{1,3}       | 折 1,3   | 折 2,4,5,6
...
(所有 2^6-1 = 63 种组合)

对于每个组合,你计算策略在训练部分和测试部分的 Sharpe。然后你分析关系。

数学

组合划分数: Ncomb=2k−1N_{comb} = 2^k - 1

对于 k=6:N_comb = 63。对于 k=8:N_comb = 255。计算成本随 k 指数增长。

对于每个组合 i:

  • 训练 Sharpe:SRtrain(i)SR_{train}^{(i)}
  • 测试 Sharpe:SRtest(i)SR_{test}^{(i)}

你寻找的关键关系:高 SRtrainSR_{train} 是否预测高 SRtestSR_{test}?

  • 如果是(高相关性):策略是稳健的
  • 如果否(低或负相关性):策略是过拟合的

PBO(回测过拟合概率) 从这个关系计算。它测量高训练排名变成低测试排名的频率。

Python 实现

import numpy as np
from itertools import combinations

def cpcv_partitions(n_samples, k=6, n_purge=None, pct_embargo=0.01):
    """
    生成所有 CPCV train/test 划分。
    """
    n_per_fold = n_samples // k
    n_embargo = int(n_samples * pct_embargo)
    if n_purge is None:
        n_purge = int(np.sqrt(n_samples))

    # 枚举 k 折的所有非空子集
    for r in range(1, k + 1):
        for train_folds in combinations(range(k), r):
            train_mask = np.zeros(k, dtype=bool)
            train_mask[list(train_folds)] = True
            test_mask = ~train_mask

            train_indices = []
            test_indices = []

            for fold_idx in range(k):
                start = fold_idx * n_per_fold
                end = start + n_per_fold

                if train_mask[fold_idx]:
                    train_indices.extend(range(start, end))

                if test_mask[fold_idx]:
                    test_indices.extend(range(start, end))

            # embargo 后测试折
            embargo_indices = []
            for fold_idx in range(k):
                if test_mask[fold_idx]:
                    start = fold_idx * n_per_fold + n_per_fold
                    end = min(start + n_embargo, n_samples)
                    embargo_indices.extend(range(start, end))

            embargo_set = set(embargo_indices)
            train_indices = [i for i in train_indices if i not in embargo_set]
            test_indices = [i for i in test_indices if i not in embargo_set]

            yield np.array(train_indices), np.array(test_indices)


def cpcv_evaluate(model, X, y, k=6, pct_embargo=0.01):
    """
    使用 CPCV 评估策略。
    """
    from sklearn.base import clone

    train_sharpes = []
    test_sharpes = []

    for train_idx, test_idx in cpcv_partitions(len(X), k=k, pct_embargo=pct_embargo):
        if len(train_idx) < 10 or len(test_idx) < 10:
            continue

        X_train, X_test = X[train_idx], X[test_idx]
        y_train, y_test = y[train_idx], y[test_idx]

        model_clone = clone(model)
        model_clone.fit(X_train, y_train)
        preds_test = model_clone.predict(X_test)

        test_ret = preds_test * y_test
        test_sharpe = sharpe_ratio(test_ret)

        test_sharpes.append(test_sharpe)

    return {
        'test_sharpes': np.array(test_sharpes),
        'n_partitions': len(test_sharpes)
    }


def sharpe_ratio(returns, periods_per_year=252):
    if np.std(returns) == 0:
        return 0.0
    return np.mean(returns) / np.std(returns) * np.sqrt(periods_per_year)

如何解读结果

训练-测试相关性 是你的主要诊断。高训练 Sharpe 预测高测试 Sharpe(相关性 > 0.7)——策略是稳健的。相关性接近 0 或为负意味着策略表现有结构性依赖或过拟合。

PBO(从这些结果计算)告诉你高训练/低测试结果的比例。PBO < 5% 是好的。PBO > 25% 应该是 dealbreaker。

测试 Sharpe 的分布 告诉你关于结构性敏感度。紧密分布(跨划分低标准差)意味着无论市场条件如何,策略表现相似。宽分布意味着策略对你训练和测试的时期敏感。

常见陷阱

使用的 k 太小。 k=4 只给 15 种划分——不足以可靠估计 PBO。使用 k≥6(63+)或 k=8(255),如果你的数据每折有足够样本。

在每个划分中忘记清仓。 不清仓的 CPCV 在折边界存在前视偏差。总是应用清仓间隙。

计算成本。 N_comb = 2^k - 1 增长很快。k=8 给 255 种组合,每种需要完整模型拟合。对于大型策略筛选,k=6(63 种组合)是实用的折中。

忽略划分内的 embargo。 每个测试折后应有 embargo 间隙,特别是对于高频策略。

在知识体系中的位置

CPCV 是 PBO 计算的基础。它提供了穷尽性的 train/test 划分枚举,使测量”很棒的样本内”变成”糟糕的样本外”的频率成为可能。

CPCV 还给你比标准 purged K-fold 更稳健的 OOS 估计:不是一种划分,而是 N_comb 种划分,每种给出不同的 OOS Sharpe。在所有划分上平均给出更稳定的估计。

参考文献

  • López de Prado, Advances in Financial Machine Learning, 第 12 章. Wiley, 2018. ISBN: 978-1-119-48208-6.
  • Bailey, López de Prado, “The Share of Tradeable and Non-Tradeable Strategies,” Journal of Computational Finance, 2012.
  • Bailey, Borwein, López de Prado, “Stock Return Data: A Non-Factorial Analysis,” Journal of Financial Data Science, 2017.