← 記事一覧に戻る
June 12, 2026
読了時間: 5分

リスクを考慮したポジションサイジングのためのコンフォーマル予測

リスクを考慮したポジションサイジングのためのコンフォーマル予測
#uncertainty
#conformal-prediction
#risk
#position-sizing
#statistics
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

あらゆるポジションサイジングの公式は、不確実性の推定を必要とします。ケリー基準には勝率とペイオフ比が必要です(戦略のためのケリー基準を参照)。平均分散最適化には共分散行列が必要です。VaRにはリターン分布が必要です。これらすべては、データ生成プロセスに関する仮定を必要とします。そして金融市場は、こうした仮定を日常的に破ります。

コンフォーマル予測は、これとは異なるものを提供します。すなわち、いかなるパラメトリックな分布の仮定もなしに、有限サンプルでのカバレッジ保証を持つ予測区間です。90%のカバレッジを求めれば、少なくとも90%のカバレッジが得られます。リターンがガウス分布であろうと、ファットテールであろうと、歪んでいようと、不均一分散であろうと関係ありません。唯一の要件は交換可能性(あるいは、後述するように、より弱い条件)です。

本記事では、その理論、主要なバリアント、そしてPythonでのポジションサイジングのための実践的な実装を扱います。

中核となるアイデア: 非適合スコア

分位点しきい値が示された非適合(残差)スコアの分布 — コンフォーマル較正の中核

コンフォーマル予測は、新しい観測値が過去のデータと比べてどれほど「奇妙」であるかを測定することで機能します。その奇妙さは、非適合スコアによって定量化されます。これは、データ点が残りのデータに見られるパターンにどれほど適合していないかを測定する任意の関数です。

回帰(リターンのような連続値の予測)の場合、最も単純な非適合スコアは絶対残差です。

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}) が交換可能であれば、R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} の中での Rn+1R_{n+1} の順位は {1,,n+1}\{1, \ldots, n+1\} 上で一様分布します。これは純粋に組合せ論的な事実であり、XXYY の分布に関するいかなる仮定も必要としません。

この順位の一様性から、有限サンプルでのカバレッジを持つ予測区間を構築できます。

Split Conformal予測

Split conformal予測: データを学習用と較正用の分割に分け、較正残差が予測区間を生成する

Split conformal予測(Papadopoulos et al., 2002; Lei et al., 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} がひどい予測器であれば、区間が単に広くなるだけです。カバレッジ保証は依然として成り立ちます。

スコアに同順位がない場合には上界も存在します。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} となるため、カバレッジは無駄に保守的にはなりません。

これがトレーディングにとって重要な理由

たとえば線形回帰から得られる伝統的な予測区間は、ガウス誤差を仮定します。データの大部分で較正されたガウス区間は、真の残差がヘビーテール(たとえば自由度の小さいスチューデント tt)である場合、テール部分をひどく見誤ることがあります。中心部の質量はガウス分布より薄いため、分散を合わせたガウス区間は中心付近で過剰にカバーする一方でテールでは不足し、テールに合わせたものはその逆になります。要点は単一の魔法の数値ではなく、パラメトリック区間の実現カバレッジが、あなたが検証していない分布の仮定に依存するということです。

コンフォーマル予測区間はこれを回避します。モデルが不確実なときには自動的に広がり、真の誤差分布に関わらず周辺カバレッジを維持します。トレーダーにとって、これは次を意味します。

  • 区間幅に反比例してポジションをサイジングすれば、モデルが不確実なときに自動的にエクスポージャーを減らすことになります。
  • カバレッジ保証は、リスク推定が正直であることを意味します。「実現リターンの90%はこの区間に収まる」と言うなら、その記述は統計的に妥当です(交換可能性のもとで、周辺的に)。

Full ConformalとJackknife+

Split conformalは単純ですが、データを無駄にします。較正集合は学習に使えません。これに対処する2つの代替案があります。

Full Conformal予測

Full conformal予測(Vovk et al., 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 についてモデルを再フィットしなければなりません。リターン予測の場合、これは1回の予測あたり何千回もの再フィットを意味しうります。

Jackknife+ (Barber et al., 2021)

jackknife+はバランスを取ります。leave-one-out(LOO)残差を使いますが、LOO分割をまたいだフィット済みモデルのばらつきを考慮します。

μ^i\hat{\mu}_{-i} を、点 ii を除くすべてのデータで学習したモデルとします。単一の絶対残差を用いてLOO非適合スコアを定義します。

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

jackknife+の予測区間は、テスト点でのLOO予測から構築され、これらの残差によって広げられます。

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)は、分布シフトと非交換可能なデータを扱うために**適応的コンフォーマル推論(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)に類似していますが、決定的な違いがあります。コンフォーマル区間幅は分布フリーの不確実性尺度であり、パラメトリックなボラティリティ推定ではありません。それは単なるリターン分散ではなく、カバレッジ保証のもとでの予測的不確実性を捉えます。

エッジ比サイジングとノートレードフィルタ

純粋な幅の逆数によるサイジングは、シグナル自体の強さを無視します。自然な改良は、エッジ比、すなわち区間幅に対する点予測でスケーリングすることです。

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 のときだからです)。θ\theta は、バックテストを通じて実際の et=μ^/wte_t = |\hat{\mu}|/w_t のスケール上で選んでください。日次リターン残差の場合、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の MapieTimeSeriesRegressormethod="enbpi"(Xu & Xie, 2021)で使うと、時間的依存性のために設計されたブロックブートストラップと残差更新が用いられます。これは本記事自身の主張に合致するツールです。

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 を維持し、それを加法的に更新し、各ステップで残差集合から分位点を再導出します。2つの有限サンプルの詳細が重要です。

  • 補間された分位点ではなく、順序統計量を使ってください。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 et al., 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} は陳腐化しうります。2つのアプローチがあります。

  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 という分母を使っています。

伝統的手法との比較

性質 ガウスCI ブートストラップCI コンフォーマルPI
分布の仮定 正規誤差 i.i.d. + 漸近的 なし(交換可能性)
有限サンプル保証 なし なし あり
任意のモデルで機能 なし あり あり
不均一分散に適応 なし 部分的に CQR / 正規化スコアあり
分布シフトを処理 なし なし ACI / EnbPIバリアント
計算コスト Split: 低; jackknife+: O(n)O(n) 回の再フィット; full: 法外

ブートストラップが「漸近的」なのはその保証においてだけです。それでもなお 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. 区間幅は確率密度ではない。 コンフォーマル区間は、確率 1α1-\alphaYY がどこに収まるかを教えてくれますが、区間の分布については何も語りません。それは完全な予測分布の代替ではありません。これこそが、そこからケリーの ppbb を導出するには、追加の明示的な仮定が必要となる理由です。

  4. ゴミを入れれば、より広いものが出る。 質の悪いベースモデルは広い区間を生成します。コンフォーマル予測はカバレッジを保証しますが、有用性は保証しません。モデルに予測力がなければ、区間が非常に広くなり、ポジションサイザーは決してトレードしません。

まとめ

コンフォーマル予測は、ポジションサイジングに自然に組み込まれる、厳密で分布フリーの不確実性定量化のフレームワークを提供します。

  • Split conformal: 有限サンプルカバレッジを持つ、静的でオフラインの較正のために。
  • Jackknife+: 較正データが希少で、すべての観測を使いたいとき(nn 回の再フィットというコストで)。
  • 適応的コンフォーマル推論 / EnbPI: 非定常な市場でのオンライントレーディングのために。
  • ポジションサイジング: 区間幅の逆数とエッジ比ノートレードフィルタによって。そしてケリーに進むなら、正当化されない乗数によってではなく、ppbb を正直に導出した後でのみ。

パラメトリックな代替案に対する主要な利点は、パラメトリックな分布の仮定を指定したり検証したりする必要が決してないことです。区間は構成上正直です(交換可能性のもとで、周辺的に)。システマティックトレーダーにとって、これはモデルリスクの源が1つ少なくなることを意味します。そしてモデルリスクが死活問題となるビジネスにおいて、それは重要なことです。


参考文献:

  • 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.
blog.disclaimer

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取引の洞察、市場分析、プラットフォームの更新情報を受け取りましょう。

プライバシーを尊重します。いつでも配信停止可能です。