← 返回文章列表
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 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。