← 返回文章列表
June 12, 2026
5 分钟阅读

用于风险感知仓位管理的保形预测

用于风险感知仓位管理的保形预测
#不确定性
#conformal-prediction
#风险
#仓位管理
#statistics
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

每一个仓位管理公式都需要对不确定性进行估计。凯利准则需要胜率和赔率(见 策略的凯利准则)。均值-方差优化需要协方差矩阵。VaR 需要收益率分布。所有这些都需要对数据生成过程做出假设——而这些假设正是金融市场经常违背的。

保形预测(conformal prediction)提供了一种不同的东西:带有有限样本覆盖率保证的预测区间,且不需要任何参数化的分布假设。如果你要求 90% 的覆盖率,你就能得到至少 90% 的覆盖率——无论收益率是高斯分布、厚尾分布、偏斜分布还是异方差分布。唯一的要求是可交换性(exchangeability,或者如我们将看到的更弱的条件)。

本文将介绍其理论、关键变体,以及在 Python 中用于仓位管理的实用实现。

核心思想:非一致性分数

带有分位数阈值标记的非一致性(残差)分数分布——这是保形校准的核心

保形预测的工作原理是衡量一个新观测值相对于过去数据有多么“奇怪”。这种奇怪程度由**非一致性分数(nonconformity score)**来量化——它可以是任何衡量某个数据点与其余数据所呈现模式的吻合程度有多差的函数。

对于回归(预测一个像收益率这样的连续值),最简单的非一致性分数是绝对残差:

Ri=Yiμ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)|

其中 μ^\hat{\mu} 是任意点预测器(线性回归、随机森林、神经网络——都无所谓),而 (Xi,Yi)(X_i, Y_i) 是一个数据点。

关键洞察在于:如果数据点 (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) 是可交换的,那么 Rn+1R_{n+1}R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} 中的排名在 {1,,n+1}\{1, \ldots, n+1\} 上是均匀分布的。这是一个纯粹的组合学事实——它不需要关于 XXYY 分布的任何假设。

从这种排名均匀性出发,我们可以构造出带有有限样本覆盖率的预测区间。

Split Conformal 预测

Split conformal 预测:数据被划分为训练折和校准折,由校准残差产生预测区间

Split conformal 预测(Papadopoulos 等,2002;Lei 等,2018)是最实用的变体。算法很简单:

步骤 1。 将数据划分为训练集 Dtrain\mathcal{D}_{\text{train}} 和校准集 Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\}

步骤 2。Dtrain\mathcal{D}_{\text{train}} 上拟合任意模型 μ^\hat{\mu}

步骤 3。 在校准集上计算非一致性分数:

Ri=Yiμ^(Xi),i=1,,nR_i = |Y_i - \hat{\mu}(X_i)|, \quad i = 1, \ldots, n

步骤 4。 对于期望的失覆盖水平 α(0,1)\alpha \in (0, 1),取 q^\hat{q}R1,,RnR_1, \ldots, R_n 的第 (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n} 经验分位数。具体来说,这就是第 (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil 小的残差(并且每当 (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n 时,即对于非常小的 nn,取 q^=+\hat{q} = +\infty)。

步骤 5。 对于新点 Xn+1X_{n+1} 的预测区间为:

C(Xn+1)=[μ^(Xn+1)q^,  μ^(Xn+1)+q^]C(X_{n+1}) = \left[\hat{\mu}(X_{n+1}) - \hat{q}, \; \hat{\mu}(X_{n+1}) + \hat{q}\right]

覆盖率保证

在校准数据与新测试点可交换的前提下:

P(Yn+1C(Xn+1))1α\mathbb{P}\left(Y_{n+1} \in C(X_{n+1})\right) \geq 1 - \alpha

这是一个有限样本保证——而非渐近近似。它对任意模型 μ^\hat{\mu}、任意数据分布以及任意样本量 nn 都成立。如果 μ^\hat{\mu} 是一个糟糕的预测器,区间只会变得更宽。覆盖率保证依然成立。

当分数没有并列(ties)时,还存在一个上界:P(Yn+1C(Xn+1))1α+1n+1\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \leq 1 - \alpha + \frac{1}{n+1},因此覆盖率并不会浪费性地过于保守。

这对交易为何重要

来自比如线性回归的传统预测区间会假设高斯误差。一个在数据主体上校准的高斯区间,当真实残差是厚尾的时候(例如自由度很少的 Student-tt),可能会严重误判尾部:中心质量比高斯分布更薄,所以一个方差匹配的高斯区间在中心附近会过度覆盖,而在尾部却覆盖不足,而一个尾部拟合的区间则恰好相反。重点并不是某个单一的神奇数字——而是一个参数化区间的实际覆盖率取决于一个你尚未验证的分布假设。

保形预测区间绕开了这一点。它们在模型不确定时会自动变宽,并且无论真实误差分布如何,都能维持边际覆盖率。对于交易者而言,这意味着:

  • 如果你按区间宽度的反比来确定仓位规模,那么当模型不确定时你会自动降低敞口
  • 覆盖率保证意味着你的风险估计是诚实的——如果你说“90% 的实际收益率会落在这个区间内”,那么这个陈述在统计上是有效的(边际地,在可交换性下)。

Full Conformal 与 Jackknife+

Split conformal 很简单,但浪费数据:校准集无法用于训练。有两种替代方案可以解决这个问题。

Full Conformal 预测

Full conformal 预测(Vovk 等,2005)将所有数据同时用于训练和校准。对于 Yn+1Y_{n+1} 的每一个候选值 yy

  1. (Xn+1,y)(X_{n+1}, y) 扩充数据集。
  2. 在扩充后的数据集上重新拟合模型。
  3. 计算所有非一致性分数。
  4. 如果 (Xn+1,y)(X_{n+1}, y) 的分数不太极端,就把 yy 纳入预测集。

预测集为:

C(Xn+1)={y:{i:RiyRn+1y}n+1>α}C(X_{n+1}) = \left\{y : \frac{|\{i : R_i^y \geq R_{n+1}^y\}|}{n+1} > \alpha \right\}

其中 RiyR_i^y 是用扩充后数据集计算出的非一致性分数。

Full conformal 给出最紧的区间,但对大多数模型而言计算上是难以承受的——你必须为网格上的每个候选 yy 重新拟合模型。对于一次收益率预测,这可能意味着每次预测要重新拟合数千次。

Jackknife+(Barber 等,2021)

Jackknife+ 达成了一种平衡。它使用留一法(LOO)残差,但同时考虑了拟合模型在各个 LOO 折之间的变异性。

μ^i\hat{\mu}_{-i} 表示在除点 ii 之外的所有数据上训练的模型。用单个绝对残差定义 LOO 非一致性分数:

Ri=Yiμ^i(Xi)R_i = |Y_i - \hat{\mu}_{-i}(X_i)|

然后由测试点处的 LOO 预测构建 jackknife+ 预测区间,并用这些残差加以扩宽:

C(Xn+1)=[qα ⁣{μ^i(Xn+1)Ri},    q1α+ ⁣{μ^i(Xn+1)+Ri}]C(X_{n+1}) = \left[\, q_{\alpha}^{-}\!\left\{\hat{\mu}_{-i}(X_{n+1}) - R_i\right\}, \;\; q_{1-\alpha}^{+}\!\left\{\hat{\mu}_{-i}(X_{n+1}) + R_i\right\} \right]

这里 q1α+{vi}q_{1-\alpha}^{+}\{v_i\} 表示集合 {vi}i=1n\{v_i\}_{i=1}^n 中第 (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil 小的值,而 qα{vi}q_{\alpha}^{-}\{v_i\} 表示第 α(n+1)\lfloor \alpha(n+1)\rfloor 小的值。下界从每个 LOO 预测中减去残差;上界则加上残差。这种不对称性正是关键所在——将两个边界都坍缩为 μ^i+Ri\hat{\mu}_{-i} + R_i 会使下界高于预测值,那是错误的。

Jackknife+ 提供至少 12α1 - 2\alpha 的覆盖率保证(比 split conformal 的 1α1-\alpha 略弱),但它将所有数据同时用于训练和校准。在实践中,覆盖率通常接近 1α1-\alpha

对于在有限数据上训练的交易模型(例如只有几百个观测值的特定市场状态模型),jackknife+ 往往是最佳选择——它不会为校准而牺牲稀缺的数据。其代价是 nn 次模型重新拟合。

金融时间序列的问题:非可交换性

金融时间序列中的非可交换性:一个带有市场状态切换的非平稳序列,覆盖率随分布漂移而失效

标准的保形保证要求可交换性:(Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) 的联合分布在排列下保持不变。对于 i.i.d. 数据,这显然成立。

金融时间序列不是可交换的。收益率表现出:

  • 波动率聚集: 高波动率时期会跟随高波动率时期(GARCH 效应)。
  • 动量与均值回归: 收益率或收益率平方中的自相关。
  • 市场状态变化: 分布随时间漂移(牛市与熊市)。

如果你天真地把 split conformal 应用于时间序列——使用随机的校准划分——就会破坏时间结构。来自平静的 2017 年的校准分数无法反映波动剧烈的 2020 年的不确定性。你的覆盖率保证就此失效。

自适应保形推断(ACI)

自适应保形推断:通过一个跟踪实际覆盖率趋向目标的反馈回路而变宽和变窄的预测区间

Gibbs 与 Candes(2021,NeurIPS)引入了**自适应保形推断(Adaptive Conformal Inference,ACI)**来处理分布漂移和非可交换数据。其思路十分优雅:不使用固定的覆盖水平,而是基于近期区间是否覆盖了真实结果,在线地自适应调整目标失覆盖水平,并在每一步从分数分布中重新推导分位数。

ACI 算法

ACI 并不直接微调区间宽度。它维护一个自适应的失覆盖参数 αt\alpha_t,并据此重新计算保形分位数。在每个时间步 tt

  1. 将保形阈值计算为当前残差集合(校准分数,加上到目前为止任何已实现的分数)的经验 (1αt)(1 - \alpha_t) 分位数:q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\})
  2. 观测特征 XtX_t,产生区间 Ct(Xt)=[μ^(Xt)q^t,  μ^(Xt)+q^t]C_t(X_t) = [\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t]
  3. 观测真实值 YtY_t 并计算误差指示器 errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}
  4. 更新水平(而非宽度):

αt+1=clip ⁣(αt+γ(αerrt),  0,  1)\alpha_{t+1} = \text{clip}\!\left(\alpha_t + \gamma\,(\alpha - \text{err}_t),\; 0,\; 1\right)

其中 γ>0\gamma > 0 是步长,α\alpha 是目标失覆盖水平。如果某个区间未命中(errt=1\text{err}_t = 1),αt\alpha_t 会缩小,这会把下一个分位数推得更高并使区间变宽;如果命中了,αt\alpha_t 会增大,区间则收紧。关键在于,这里的 γ\gamma 是以概率单位计的——它微调的是 [0,1][0,1] 中的一个水平,而不是以原始收益率单位计的阈值——因此无论残差是否在 10310^{-3} 量级,同一个 γ\gamma 的表现都是合理的。

ACI 的覆盖率保证

ACI 提供了一个不依赖于分布模型的长期覆盖率保证:

1Tt=1TerrtααT+1α1γT\left|\frac{1}{T}\sum_{t=1}^{T} \text{err}_t - \alpha\right| \leq \frac{|\alpha_{T+1} - \alpha_1|}{\gamma T}

因为 αt\alpha_t 被裁剪到 [0,1][0,1],分子被一个常数乘以 1/γ1/\gamma 所界定,所以右侧是 O(1/T)O(1/T),经验失覆盖频率收敛到 α\alpha。精确的表述是:只要自适应水平保持有界(裁剪正是强制了这一点),ACI 就保证经验失覆盖频率对任意(包括对抗性的)序列都收敛到 α\alpha。这是对覆盖频率的保证,而不是对区间信息量的保证:在真正对抗性的序列下,区间可能在仍然命中覆盖目标的同时变得宽到毫无信息量。

动态调优的 ACI(DtACI)

Gibbs 与 Candes(2024,JMLR)通过对步长 γ\gamma 进行动态调优改进了 ACI。他们不固定 γ\gamma,而是维护一个候选集 Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\},并通过一种专家聚合规则将它们组合起来,偏向于那些近期覆盖率最接近目标的 γ\gamma

这解决了一个实际问题:较大的 γ\gamma 能快速适应市场状态变化,但会产生波动剧烈的区间宽度;较小的 γ\gamma 稳定,但适应缓慢。DtACI 会自动在两者之间进行权衡。

这对交易为何重要

设想一个使用收益率预测模型的做市策略。在平静的市场中,保形区间很紧——模型很有信心,你可以建立更大的仓位。当波动率飙升时(财报季、FOMC 公告、地缘政治冲击),ACI 的水平会在几个时间步内自适应,区间随之变宽。你的仓位规模随之缩小,无需任何显式的波动率模型或市场状态检测逻辑。

这是把不确定性量化当作一等信号来对待,而不是事后才考虑的东西。

用保形区间进行仓位管理

将校准后的不确定性区间映射到仓位规模:紧区间驱动大仓位,宽区间驱动小仓位

现在让我们把保形预测与具体的仓位管理联系起来。关键变量是相对于对称绝对残差情形的预测区间半宽。对于对称区间 [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t],完整宽度为 wt=2q^tw_t = 2\hat{q}_t。为了让公式和代码保持一致,我们自始至终都以完整宽度 wtw_t 为基准来衡量一切。

反比于宽度的仓位管理

最简单的方法:仓位规模与区间宽度成反比。

position_sizet=kwt\text{position\_size}_t = \frac{k}{w_t}

其中 kk 是根据你的风险预算校准的缩放常数。当模型有信心(区间窄)时,你建立更大的仓位。当不确定(区间宽)时,你建立更小的仓位。

这类似于波动率目标法(size1/σ\text{size} \propto 1/\sigma),但有一个关键差异:保形区间宽度是一个无分布假设的不确定性度量,而不是参数化的波动率估计。它在覆盖率保证下捕捉的是预测不确定性,而不仅仅是收益率方差。

优势比仓位管理与不交易过滤器

纯粹的反比于宽度的仓位管理忽略了信号本身的强度。一个自然的改进是按**优势比(edge ratio)**进行缩放——即点预测相对于区间宽度的比值:

et=μ^(Xt)wte_t = \frac{|\hat{\mu}(X_t)|}{w_t}

这是信噪比的一个保形类比:预期收益除以一个无分布假设的不确定性度量。我们将它同时用于仓位管理和不交易过滤器。

这个过滤器是有原则的。如果区间跨越了零,

lowert<0<uppert,\text{lower}_t < 0 < \text{upper}_t,

那么 (1α)(1-\alpha) 预测区间同时包含正收益和负收益——实际收益完全可能与你预测的符号相反。定义一个最小优势阈值 θ\theta,仅在 et>θe_t > \theta 时交易。注意,“ete_t 大到使区间不再跨越零”这一几何含义恰好是 et>1/2e_t > 1/2(因为当 μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2 时区间就避开了零)。通过回测,在 et=μ^/wte_t = |\hat{\mu}|/w_t 的实际尺度上选择 θ\theta;对于日收益率残差,ete_t 通常远低于 1/21/2,因此一个极小的 θ\theta 可能放行几乎所有交易,而一个较大的 θ\theta 则可能一笔都不放行。请根据你的数据来校准它。

关于“保形凯利”

把保形区间硬塞进凯利分数 f=pb(1p)bf^* = \frac{pb - (1-p)}{b} 是很有诱惑力的。但 ff^* 本身已经是一个完整的、有界的分数,由胜率 pp 和赔率 bb 推导而来;把它乘以一个像 μ^/q^\hat{\mu}/\hat{q} 这样的无界比值在决策论上没有任何依据——它可能超过 1 或独立于 ff^* 翻转符号,并且会重复计算 ff^* 已经编码的优势。所以我们提出“保形凯利”乘数。

如果你想用区间来驱动凯利,你就必须真正从中推导出 ppbb,而这需要对区间内部的分布做一个显式的假设(保形区间故意对此只字不提——见“局限性”)。例如,在假定的区间内形状下,你可以近似 pP(return>0)p \approx \mathbb{P}(\text{return} > 0),并从区间几何中得到一个赔率——但要大声地标明这个假设,因为它恰恰重新引入了保形预测本想避免的那个参数化承诺。

诚实、轻假设的替代方案是把优势比 et=μ^/wte_t = |\hat{\mu}|/w_t 用作一种分数凯利收缩:当预期收益相对于区间较大时加仓,较小时减仓,并在一个硬上限之上应用它——明确地作为一种启发式方法,而不是“凯利分数”。

Python 实现

下面是一个实用的实现。我们同时展示 split/prefit 路径和时序(EnbPI)路径,因为本文的全部要点就在于金融数据是非可交换的。

配置与数据准备

import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import KFold

from mapie.regression import MapieRegressor, MapieTimeSeriesRegressor
from mapie.subsample import BlockBootstrap


def prepare_features(prices: pd.Series, lookback: int = 20) -> pd.DataFrame:
    """Create features from a price series."""
    df = pd.DataFrame()
    returns = prices.pct_change()

    for lag in range(1, lookback + 1):
        df[f"ret_lag_{lag}"] = returns.shift(lag)

    for window in [5, 10, 20]:
        df[f"ret_mean_{window}"] = returns.rolling(window).mean().shift(1)
        df[f"ret_std_{window}"] = returns.rolling(window).std().shift(1)
        df[f"ret_skew_{window}"] = returns.rolling(window).skew().shift(1)

    df["target"] = returns.shift(-1)

    return df.dropna()

使用 MAPIE 的 Split Conformal(Prefit)

对于 split/prefit 保形,cv="prefit" 要求 method="base"(朴素的 split-conformal 估计器)。method="plus" 选项是 CV+/jackknife+ 估计器,与 cv="prefit" 不兼容——它需要的是一个交叉验证对象。我们在这里使用正确的组合,并将仓位管理统一标准化到完整宽度上。

def split_conformal_sizing(
    prices: pd.Series,
    alpha: float = 0.1,
    k: float = 1e-3,          # scaling constant, in width units
    max_position: float = 1.0,
    min_edge: float = 0.05,   # threshold on |pred| / width
) -> pd.DataFrame:
    """
    Position sizing using split (prefit) conformal prediction intervals.

    Sizing rule (consistent with the prose):
        edge_t = |pred_t| / width_t
        size_t = clip(k / width_t, 0, max_position)   # inverse-width
        size_t = 0 if edge_t < min_edge               # no-trade filter
    """
    df = prepare_features(prices)
    X = df.drop(columns=["target"])
    y = df["target"]

    n_train = int(len(X) * 0.6)
    n_cal = int(len(X) * 0.2)

    X_train, y_train = X.iloc[:n_train], y.iloc[:n_train]
    X_cal, y_cal = X.iloc[n_train:n_train + n_cal], y.iloc[n_train:n_train + n_cal]
    X_test, y_test = X.iloc[n_train + n_cal:], y.iloc[n_train + n_cal:]

    base_model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )
    base_model.fit(X_train, y_train)

    mapie = MapieRegressor(estimator=base_model, cv="prefit", method="base")
    mapie.fit(X_cal, y_cal)

    y_pred, y_intervals = mapie.predict(X_test, alpha=alpha)
    lower = y_intervals[:, 0, 0]
    upper = y_intervals[:, 1, 0]
    width = upper - lower

    raw_size = k / np.where(width > 0, width, np.inf)
    position_size = np.clip(raw_size, 0.0, max_position)

    edge_ratio = np.abs(y_pred) / np.where(width > 0, width, np.inf)
    position_size = np.where(edge_ratio < min_edge, 0.0, position_size)

    position_size = position_size * np.sign(y_pred)

    return pd.DataFrame({
        "prediction": y_pred,
        "lower": lower,
        "upper": upper,
        "width": width,
        "edge_ratio": edge_ratio,
        "position_size": position_size,
        "actual": y_test.values,
    }, index=X_test.index)

使用 EnbPI 的时序保形

由于收益率是非可交换的,上面的随机/prefit 划分只是一个基线。MAPIE 的 MapieTimeSeriesRegressor 配合 method="enbpi"(Xu 与 Xie,2021)使用块自助法(block bootstrap)和残差更新,专为时间依赖性而设计。这正是与本文自身论点相匹配的工具。

def enbpi_sizing(
    prices: pd.Series,
    alpha: float = 0.1,
    k: float = 1e-3,
    max_position: float = 1.0,
    min_edge: float = 0.05,
) -> pd.DataFrame:
    """Position sizing with EnbPI (block-bootstrap, time-series conformal)."""
    df = prepare_features(prices)
    X = df.drop(columns=["target"])
    y = df["target"]

    n_train = int(len(X) * 0.7)
    X_train, y_train = X.iloc[:n_train], y.iloc[:n_train]
    X_test, y_test = X.iloc[n_train:], y.iloc[n_train:]

    base_model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )

    cv = BlockBootstrap(n_resamplings=30, length=20, overlapping=False, random_state=42)
    mapie_ts = MapieTimeSeriesRegressor(base_model, method="enbpi", cv=cv, agg_function="mean")
    mapie_ts.fit(X_train, y_train)

    y_pred, y_intervals = mapie_ts.predict(X_test, alpha=alpha, ensemble=True)
    lower = y_intervals[:, 0, 0]
    upper = y_intervals[:, 1, 0]
    width = upper - lower

    raw_size = k / np.where(width > 0, width, np.inf)
    position_size = np.clip(raw_size, 0.0, max_position)
    edge_ratio = np.abs(y_pred) / np.where(width > 0, width, np.inf)
    position_size = np.where(edge_ratio < min_edge, 0.0, position_size)
    position_size = position_size * np.sign(y_pred)

    return pd.DataFrame({
        "prediction": y_pred, "lower": lower, "upper": upper,
        "width": width, "edge_ratio": edge_ratio,
        "position_size": position_size, "actual": y_test.values,
    }, index=X_test.index)

自适应保形推断(在线)

对于实时交易,我们从零实现真正的 ACI:维护失覆盖水平 αt\alpha_t,对其进行加法式更新,并在每一步从残差集合中重新推导分位数。有两个有限样本的细节很重要:

  • 使用顺序统计量,而不是插值分位数。np.quantile 默认会插值,这可能恰好落在所需排名之下而导致覆盖不足;请传入 method="higher"(等价于 "inverted_cdf")。
  • 当所需排名超过 nn(小 nn、高目标覆盖率)时,正确的阈值是 ++\infty(区间 = 整条实数线),而不是钳制到最大残差。钳制会悄悄破坏 1αt\ge 1-\alpha_t 的保证。
class AdaptiveConformalSizer:
    """
    Online position sizing with Adaptive Conformal Inference (Gibbs & Candes,
    2021). Updates the miscoverage LEVEL alpha_t and re-derives the quantile
    from the residual set each step -- gamma is in probability units.
    """

    def __init__(self, base_model, alpha=0.1, gamma=0.02,
                 max_position=1.0, min_edge=0.05, k=1e-3):
        self.base_model = base_model
        self.alpha_target = alpha     # target miscoverage
        self.alpha_t = alpha          # adaptive miscoverage level
        self.gamma = gamma            # step size, in [0, 1] units
        self.max_position = max_position
        self.min_edge = min_edge
        self.k = k
        self.residuals = []
        self.q_hat = np.inf
        self.coverage_history = []

    @staticmethod
    def _conformal_quantile(residuals, alpha_t):
        """(1 - alpha_t) conformal quantile via the order statistic."""
        n = len(residuals)
        if n == 0:
            return np.inf
        rank = int(np.ceil((1.0 - alpha_t) * (n + 1)))
        if rank > n:                  # required order statistic does not exist
            return np.inf             # -> interval is the whole line
        level = rank / n
        return float(np.quantile(residuals, min(level, 1.0), method="higher"))

    def calibrate(self, X_cal, y_cal):
        preds = self.base_model.predict(X_cal)
        self.residuals = list(np.abs(np.asarray(y_cal) - preds))
        self.q_hat = self._conformal_quantile(self.residuals, self.alpha_t)

    def predict_and_size(self, X_t) -> dict:
        mu_hat = self.base_model.predict(np.asarray(X_t).reshape(1, -1))[0]
        lower, upper = mu_hat - self.q_hat, mu_hat + self.q_hat
        width = upper - lower                      # = 2 * q_hat

        edge_ratio = abs(mu_hat) / width if np.isfinite(width) and width > 0 else 0.0

        if edge_ratio < self.min_edge:
            size = 0.0
        else:
            size = min(self.k / width, self.max_position) if width > 0 else 0.0

        return {
            "prediction": mu_hat, "lower": lower, "upper": upper,
            "width": width, "edge_ratio": edge_ratio,
            "position_size": size * np.sign(mu_hat),
            "alpha_t": self.alpha_t, "q_hat": self.q_hat,
        }

    def update(self, X_t, y_t: float):
        """ACI update: adapt the LEVEL, then re-derive the quantile."""
        mu_hat = self.base_model.predict(np.asarray(X_t).reshape(1, -1))[0]
        residual = abs(y_t - mu_hat)

        covered = int(residual <= self.q_hat)
        err_t = 1 - covered

        self.alpha_t = float(np.clip(
            self.alpha_t + self.gamma * (self.alpha_target - err_t), 0.0, 1.0
        ))

        self.residuals.append(residual)
        self.q_hat = self._conformal_quantile(self.residuals, self.alpha_t)

        self.coverage_history.append(covered)

    @property
    def running_coverage(self) -> float:
        if not self.coverage_history:
            return float("nan")
        return float(np.mean(self.coverage_history))

把它们组合起来:回测循环

def backtest_aci_sizing(prices: pd.Series, alpha=0.1, gamma=0.02) -> pd.DataFrame:
    """Backtest position sizing with Adaptive Conformal Inference."""
    df = prepare_features(prices)
    X = df.drop(columns=["target"]).values
    y = df["target"].values
    index = df.index

    n_train = int(len(X) * 0.5)
    n_cal = int(len(X) * 0.2)

    X_train, y_train = X[:n_train], y[:n_train]
    X_cal, y_cal = X[n_train:n_train + n_cal], y[n_train:n_train + n_cal]
    X_test, y_test = X[n_train + n_cal:], y[n_train + n_cal:]
    test_index = index[n_train + n_cal:]

    model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )
    model.fit(X_train, y_train)

    sizer = AdaptiveConformalSizer(base_model=model, alpha=alpha, gamma=gamma)
    sizer.calibrate(X_cal, y_cal)

    records = []
    for i in range(len(X_test)):
        result = sizer.predict_and_size(X_test[i])
        result["actual"] = y_test[i]
        result["pnl"] = result["position_size"] * y_test[i]
        records.append(result)
        sizer.update(X_test[i], y_test[i])   # online residual + level update

    results = pd.DataFrame(records, index=test_index)
    results["cumulative_pnl"] = results["pnl"].cumsum()
    results["running_coverage"] = (
        ((results["lower"] <= results["actual"]) &
         (results["actual"] <= results["upper"])).expanding().mean()
    )
    return results

评估结果

def evaluate(results: pd.DataFrame, alpha: float = 0.1):
    """Print evaluation metrics for conformal position sizing."""
    covered = ((results["actual"] >= results["lower"]) &
               (results["actual"] <= results["upper"]))

    print(f"Target coverage:      {1 - alpha:.1%}")
    print(f"Empirical coverage:   {covered.mean():.1%}")
    print(f"Mean interval width:  {results['width'].mean():.6f}")
    print(f"Median position size: {results['position_size'].abs().median():.4f}")
    print(f"Fraction no-trade:    {(results['position_size'] == 0).mean():.1%}")
    print(f"Total PnL (bps):      {results['pnl'].sum() * 10000:.1f}")
    sd = results['pnl'].std()
    sharpe = results['pnl'].mean() / sd * np.sqrt(252) if sd > 0 else float('nan')
    print(f"Sharpe (annualized):  {sharpe:.2f}")

实践考量

选择非一致性分数

绝对残差 Yμ^(X)|Y - \hat{\mu}(X)| 是默认选择,但它假设预测区间应当关于点预测对称。对于金融收益率,非对称区间往往更有意义:

  • 保形化分位数回归(CQR):α/2\alpha/21α/21 - \alpha/2 水平上拟合分位数回归器,然后进行保形化(Romano 等,2019)。区间会使其形状适应局部分布——在回撤期间下行侧更宽,在反弹期间上行侧更宽。(使用 CQR 时区间不再对称,所以 wtw_t 是真正的上界减下界的宽度——在所有地方都继续用 wtw_t 作为分母。)
  • 归一化分数: Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i),其中 σ^\hat{\sigma} 是局部波动率估计。这会产生条件自适应的区间——在低波动率状态下更紧,在高波动率状态下更宽——同时维持边际覆盖率。

校准集大小

Split conformal 的覆盖率保证对任意校准集大小 nn 都成立,但区间宽度会随 nn 增大而减小。对于非常小的 nn,所需的顺序统计量可能不存在,此时诚实的阈值是 ++\infty(一个无信息但有效的区间)——钳制到最大残差会悄悄使保证失效。对于实际交易:

  • n100n \geq 100 个校准点能给出合理偏紧的区间。
  • n500n \geq 500 更适合稳定的分位数估计。
  • 在 ACI 中,校准集只用于初始化;其余的由在线水平更新来处理。

重新训练频率

基础模型 μ^\hat{\mu} 可能会过时。有两种方法:

  1. 定期重新训练(例如每月一次)并重新校准保形分位数。
  2. 使用 ACI,让自适应水平来补偿模型的过时——随着模型残差增大,区间会自动变宽。

方法 2 更简单,而且效果出奇地好。保形层充当一张安全网:即使模型退化,长期的 ACI 覆盖频率仍会收敛到目标。

交易成本

保形区间以一种有用的方式与交易成本相互作用。当区间宽(高不确定性)时,仓位小,因此换手率低。当区间收窄(模型有信心)时,仓位增大——但模型更可能是对的,所以这份换手率是值得支付的。

你也可以把交易成本直接纳入不交易过滤器:

trade only if μ^(Xt)cost>θwt\text{trade only if } |\hat{\mu}(X_t)| - \text{cost} > \theta \cdot w_t

这确保了你只在预期净优势超过一个按保形宽度缩放的阈值时才交易——使用与其他地方相同的 wtw_t 分母。

与传统方法的比较

性质 高斯置信区间 Bootstrap 置信区间 保形预测区间
分布假设 正态误差 i.i.d. + 渐近 无(可交换性)
有限样本保证
适用于任意模型
适应异方差 部分 配合 CQR / 归一化分数
处理分布漂移 ACI / EnbPI 变体
计算成本 Split:低;jackknife+:O(n)O(n) 次重拟合;full:难以承受

Bootstrap 仅在其保证上是“渐近的”;它仍然假设 i.i.d./可交换数据和光滑性,所以它的分布假设那一格并非“无假设”。而单独一列“保形”掩盖了各变体之间差异巨大的成本,成本那一行现在把这一点讲清楚了。

局限性

保形预测并非魔法。对交易而言的关键局限:

  1. 边际覆盖,而非条件覆盖。 保证是 P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha 在边际意义上成立——即在 XXYY 二者的随机性上取平均。对于某个特定的条件取值 X=xX = x,局部覆盖率可能高于或低于 1α1 - \alpha。保形化分位数回归部分地解决了这一点。

  2. 可交换性是一个真实的要求。 对于 split conformal,校准数据和测试点必须是可交换的。金融数据不是。ACI 和 EnbPI 将其放宽为长期保证,但短期覆盖率可能偏离。

  3. 区间宽度不是概率密度。 一个保形区间告诉你 YY 将以概率 1α1-\alpha 落在何处,但它对区间内部的分布只字不提。它不能替代一个完整的预测分布——这正是为什么从中推导凯利的 ppbb 需要一个额外的、显式的假设。

  4. 垃圾进,更宽出。 一个糟糕的基础模型会产生很宽的区间。保形预测保证覆盖率,但不保证有用性。如果你的模型没有预测能力,区间会宽到使仓位管理器永远不交易。

小结

保形预测提供了一个严谨的、无分布假设的不确定性量化框架,它能自然地嵌入仓位管理:

  • Split conformal 用于静态、离线校准,带有有限样本覆盖率。
  • Jackknife+ 用于校准数据稀缺、且你想用上所有观测值的情形(代价是 nn 次重拟合)。
  • 自适应保形推断 / EnbPI 用于面向非平稳市场的在线交易。
  • 仓位管理 通过反比于区间宽度以及一个优势比不交易过滤器来实现——并且,如果你要走到凯利那一步,只能在诚实地推导出 ppbb 之后,而不是通过一个没有依据的乘数。

相对于参数化替代方案的关键优势在于:你永远不需要去指定或验证一个参数化的分布假设。这些区间在构造上就是诚实的(边际地,在可交换性下)。对于一名系统化交易者而言,这意味着少了一个模型风险来源——而在一个模型风险攸关生死的行当里,这一点很重要。


参考文献:

  • Papadopoulos, H., Proedrou, K., Vovk, V., Gammerman, A. (2002). Inductive confidence machines for regression. ECML.
  • Vovk, V., Gammerman, A., Shafer, G. (2005). Algorithmic Learning in a Random World. Springer.
  • Lei, J., G'Sell, M., Rinaldo, A., Tibshirani, R.J., Wasserman, L. (2018). Distribution-free predictive inference for regression. JASA.
  • Xu, C., Xie, Y. (2021). Conformal prediction interval for dynamic time-series (EnbPI). ICML.
  • Barber, R.F., Candes, E.J., Ramdas, A., Tibshirani, R.J. (2021). Predictive inference with the jackknife+. Annals of Statistics.
  • Gibbs, I., Candes, E.J. (2021). Adaptive conformal inference under distribution shift. NeurIPS.
  • Gibbs, I., Candes, E.J. (2024). Conformal inference for online prediction with arbitrary distribution shifts. JMLR.
  • Romano, Y., Patterson, E., Candes, E.J. (2019). Conformalized quantile regression. NeurIPS.
  • Cordier, T. et al. (2022). MAPIE: an open-source library for distribution-free uncertainty quantification. arXiv:2207.12274.
免责声明:本文提供的信息仅用于教育和参考目的,不构成财务、投资或交易建议。加密货币交易涉及重大损失风险。

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

紧跟市场步伐

订阅我们的时事通讯,获取独家 AI 交易见解、市场分析和平台更新。

我们尊重您的隐私。您可以随时退订。