← Retour aux articles
June 12, 2026
5 min de lecture

Prédiction conforme pour un dimensionnement de position tenant compte du risque

Prédiction conforme pour un dimensionnement de position tenant compte du risque
#uncertainty
#conformal-prediction
#risk
#position-sizing
#statistics
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

Toute formule de dimensionnement de position a besoin d'une estimation de l'incertitude. Le critère de Kelly a besoin d'une probabilité de gain et d'un ratio de gain (voir Le critère de Kelly pour les stratégies). L'optimisation moyenne-variance a besoin d'une matrice de covariance. La VaR a besoin d'une distribution des rendements. Tout cela nécessite des hypothèses sur le processus générateur des données — des hypothèses que les marchés financiers violent régulièrement.

La prédiction conforme propose autre chose : des intervalles de prédiction avec des garanties de couverture à échantillon fini, sans aucune hypothèse de distribution paramétrique. Si vous demandez une couverture de 90 %, vous obtenez au moins 90 % de couverture — que les rendements soient gaussiens, à queues épaisses, asymétriques ou hétéroscédastiques. La seule exigence est l'échangeabilité (ou des conditions plus faibles, comme nous le verrons).

Cet article couvre la théorie, les principales variantes, et une implémentation pratique pour le dimensionnement de position en Python.

L'idée centrale : les scores de non-conformité

Distribution des scores de non-conformité (résidus) avec un seuil de quantile marqué — le cœur de la calibration conforme

La prédiction conforme fonctionne en mesurant à quel point une nouvelle observation est « étrange » par rapport aux données passées. Cette étrangeté est quantifiée par un score de non-conformité — n'importe quelle fonction qui mesure à quel point un point de donnée se conforme mal au motif observé dans le reste des données.

Pour la régression (prédire une valeur continue comme des rendements), le score de non-conformité le plus simple est le résidu absolu :

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

μ^\hat{\mu} est n'importe quel prédicteur ponctuel (régression linéaire, forêt aléatoire, réseau de neurones — peu importe) et (Xi,Yi)(X_i, Y_i) est un point de donnée.

L'idée clé : si les points de données (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) sont échangeables, alors le rang de Rn+1R_{n+1} parmi R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} est uniformément distribué sur {1,,n+1}\{1, \ldots, n+1\}. C'est un fait purement combinatoire — il ne nécessite aucune hypothèse sur la distribution de XX ou YY.

À partir de cette uniformité des rangs, nous pouvons construire des intervalles de prédiction avec une couverture à échantillon fini.

Prédiction conforme par découpage (split conformal)

Prédiction conforme par découpage : données divisées en plis d'entraînement et de calibration, les résidus de calibration produisant des intervalles de prédiction

La prédiction conforme par découpage (Papadopoulos et al., 2002 ; Lei et al., 2018) est la variante la plus pratique. L'algorithme est simple :

Étape 1. Divisez les données en un ensemble d'entraînement Dtrain\mathcal{D}_{\text{train}} et un ensemble de calibration Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\}.

Étape 2. Ajustez un modèle quelconque μ^\hat{\mu} sur Dtrain\mathcal{D}_{\text{train}}.

Étape 3. Calculez les scores de non-conformité sur l'ensemble de calibration :

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

Étape 4. Pour un niveau de mauvaise couverture souhaité α(0,1)\alpha \in (0, 1), prenez q^\hat{q} comme le quantile empirique (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n} de R1,,RnR_1, \ldots, R_n. Concrètement, il s'agit du (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil-ième résidu le plus petit (et q^=+\hat{q} = +\infty dès que (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n, c'est-à-dire pour un nn très petit).

Étape 5. L'intervalle de prédiction pour un nouveau point Xn+1X_{n+1} est :

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]

La garantie de couverture

Sous l'échangeabilité des données de calibration et du nouveau point de test :

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

Il s'agit d'une garantie à échantillon fini — pas d'une approximation asymptotique. Elle s'applique à n'importe quel modèle μ^\hat{\mu}, n'importe quelle distribution des données, et n'importe quelle taille d'échantillon nn. Si μ^\hat{\mu} est un prédicteur médiocre, les intervalles seront simplement plus larges. La garantie de couverture tient toujours.

Il existe aussi une borne supérieure lorsque les scores n'ont pas d'égalités : 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}, de sorte que la couverture n'est pas inutilement conservatrice.

Pourquoi cela compte pour le trading

Les intervalles de prédiction traditionnels, issus par exemple d'une régression linéaire, supposent des erreurs gaussiennes. Un intervalle gaussien calibré sur le gros des données peut mal juger les queues lorsque les résidus réels sont à queues épaisses (par exemple une loi de Student-tt à peu de degrés de liberté) : la masse centrale est plus fine que la gaussienne, donc un intervalle gaussien ajusté sur la variance surcouvre près du centre mais souscouvre dans les queues, et un intervalle ajusté sur les queues fait l'inverse. Le point n'est pas un seul chiffre magique — c'est que la couverture réalisée d'un intervalle paramétrique dépend d'une hypothèse de distribution que vous n'avez pas vérifiée.

Les intervalles de prédiction conformes contournent cela. Ils s'élargissent automatiquement lorsque le modèle est incertain, et maintiennent une couverture marginale quelle que soit la distribution réelle des erreurs. Pour un trader, cela signifie :

  • Si vous dimensionnez les positions de manière inversement proportionnelle à la largeur de l'intervalle, vous réduisez automatiquement l'exposition lorsque le modèle est incertain.
  • La garantie de couverture signifie que vos estimations de risque sont honnêtes — si vous dites « 90 % des rendements réalisés tomberont dans cet intervalle », cette affirmation est statistiquement valide (marginalement, sous échangeabilité).

Conformal complet et jackknife+

Le conformal par découpage est simple mais gaspille des données : l'ensemble de calibration ne peut pas servir à l'entraînement. Deux alternatives y remédient.

Prédiction conforme complète (full conformal)

La prédiction conforme complète (Vovk et al., 2005) utilise toutes les données à la fois pour l'entraînement et la calibration. Pour chaque valeur candidate yy de Yn+1Y_{n+1} :

  1. Augmentez le jeu de données avec (Xn+1,y)(X_{n+1}, y).
  2. Réajustez le modèle sur le jeu de données augmenté.
  3. Calculez tous les scores de non-conformité.
  4. Incluez yy dans l'ensemble de prédiction si le score pour (Xn+1,y)(X_{n+1}, y) n'est pas trop extrême.

L'ensemble de prédiction est :

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 sont les scores de non-conformité calculés avec le jeu de données augmenté.

Le conformal complet fournit les intervalles les plus étroits, mais est prohibitif en calcul pour la plupart des modèles — il faut réajuster le modèle pour chaque candidat yy sur une grille. Pour une prédiction de rendement, cela pourrait signifier des milliers de réajustements par prédiction.

Jackknife+ (Barber et al., 2021)

Le jackknife+ trouve un équilibre. Il utilise des résidus de type « leave-one-out » (LOO) mais tient compte de la variabilité du modèle ajusté à travers les plis LOO.

Soit μ^i\hat{\mu}_{-i} le modèle entraîné sur toutes les données sauf le point ii. Définissez le score de non-conformité LOO avec le résidu absolu unique :

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

L'intervalle de prédiction jackknife+ est alors construit à partir des prédictions LOO au point de test, élargi par ces résidus :

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]

Ici q1α+{vi}q_{1-\alpha}^{+}\{v_i\} désigne la (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil-ième plus petite valeur de l'ensemble {vi}i=1n\{v_i\}_{i=1}^n, et qα{vi}q_{\alpha}^{-}\{v_i\} la α(n+1)\lfloor \alpha(n+1)\rfloor-ième plus petite valeur. La borne inférieure soustrait le résidu de chaque prédiction LOO ; la borne supérieure l'ajoute. Cette asymétrie est tout l'enjeu — réduire les deux bornes à μ^i+Ri\hat{\mu}_{-i} + R_i placerait la borne inférieure au-dessus de la prédiction, ce qui est incorrect.

Le jackknife+ offre une garantie de couverture d'au moins 12α1 - 2\alpha (légèrement plus faible que le 1α1-\alpha du conformal par découpage), mais utilise toutes les données à la fois pour l'entraînement et la calibration. En pratique, la couverture est généralement proche de 1α1-\alpha.

Pour les modèles de trading entraînés sur des données limitées (par exemple des modèles spécifiques à un régime avec seulement quelques centaines d'observations), le jackknife+ est souvent le meilleur choix — il ne sacrifie pas des données rares pour la calibration. Le coût est de nn réajustements du modèle.

Le problème des séries temporelles financières : la non-échangeabilité

Non-échangeabilité dans les séries temporelles financières : une série non stationnaire avec un changement de régime, la couverture se fissurant à mesure que la distribution dérive

La garantie conforme standard exige l'échangeabilité : la distribution conjointe de (Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) est invariante par permutations. Pour des données i.i.d., cela se vérifie trivialement.

Les séries temporelles financières ne sont pas échangeables. Les rendements présentent :

  • Un regroupement de volatilité (volatility clustering) : les périodes de forte volatilité suivent des périodes de forte volatilité (effets GARCH).
  • Momentum et retour à la moyenne : autocorrélation dans les rendements ou les rendements au carré.
  • Des changements de régime : la distribution évolue dans le temps (marchés haussiers vs baissiers).

Si vous appliquez naïvement le conformal par découpage à une série temporelle — en utilisant une répartition de calibration aléatoire —, vous violez la structure temporelle. Les scores de calibration d'une année 2017 calme ne refléteront pas l'incertitude d'une année 2020 volatile. Votre garantie de couverture s'effondre.

Inférence conforme adaptative (ACI)

Inférence conforme adaptative : un intervalle de prédiction qui s'élargit et se resserre via une boucle de rétroaction suivant la couverture réalisée vers la cible

Gibbs et Candes (2021, NeurIPS) ont introduit l'inférence conforme adaptative (ACI) pour gérer le décalage de distribution et les données non échangeables. L'idée est élégante : au lieu d'utiliser un niveau de couverture fixe, adapter en ligne le niveau cible de mauvaise couverture selon que les intervalles récents ont couvert le résultat réel, et re-dériver le quantile à partir de la distribution des scores à chaque étape.

L'algorithme ACI

ACI n'ajuste pas directement la largeur de l'intervalle. Il maintient un paramètre de mauvaise couverture adaptatif αt\alpha_t et recalcule le quantile conforme à partir de celui-ci. À chaque pas de temps tt :

  1. Calculez le seuil conforme comme le quantile empirique (1αt)(1 - \alpha_t) de l'ensemble de résidus actuel (les scores de calibration plus tous les scores réalisés jusqu'à présent) : q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\}).
  2. Observez les caractéristiques XtX_t, produisez l'intervalle 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. Observez la valeur réelle YtY_t et calculez l'indicateur d'erreur errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}.
  4. Mettez à jour le niveau (pas la largeur) :

α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 est une taille de pas et α\alpha est la mauvaise couverture cible. Si un intervalle a raté (errt=1\text{err}_t = 1), αt\alpha_t diminue, ce qui pousse le prochain quantile à la hausse et élargit l'intervalle ; s'il a couvert, αt\alpha_t augmente et les intervalles se resserrent. Point crucial : γ\gamma est ici en unités de probabilité — il ajuste un niveau dans [0,1][0,1], pas le seuil en unités de rendement brutes — de sorte que le même γ\gamma se comporte raisonnablement, que les résidus soient de l'ordre de 10310^{-3} ou non.

Garantie de couverture pour ACI

ACI fournit une garantie de couverture à long terme qui ne dépend pas d'un modèle de distribution :

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}

Comme αt\alpha_t est borné à [0,1][0,1], le numérateur est borné par 1/γ1/\gamma fois une constante, donc le membre de droite est en O(1/T)O(1/T) et la fréquence empirique de mauvaise couverture converge vers α\alpha. L'énoncé précis : ACI garantit que la fréquence empirique de mauvaise couverture à long terme converge vers α\alpha pour des séquences arbitraires (y compris adversariales), à condition que les niveaux adaptés restent bornés — ce que le clipping impose. C'est une garantie sur la fréquence de couverture, pas sur l'informativité de l'intervalle : sous une séquence véritablement adversariale, les intervalles peuvent devenir inutilement larges tout en atteignant l'objectif de couverture.

ACI à réglage dynamique (DtACI)

Gibbs et Candes (2024, JMLR) ont raffiné ACI avec un réglage dynamique de la taille de pas γ\gamma. Au lieu de fixer γ\gamma, ils maintiennent un ensemble de candidats Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\} et les combinent via une règle d'agrégation d'experts, favorisant le γ\gamma dont la couverture récente est la plus proche de la cible.

Cela résout un problème pratique : un grand γ\gamma s'adapte rapidement aux changements de régime mais produit des largeurs d'intervalle volatiles ; un petit γ\gamma est stable mais lent à s'adapter. DtACI arbitre cela automatiquement.

Pourquoi cela compte pour le trading

Considérez une stratégie de tenue de marché utilisant un modèle de prévision des rendements. Pendant les marchés calmes, les intervalles conformes sont étroits — le modèle est confiant, et vous pouvez prendre des positions plus importantes. Lorsque la volatilité s'envole (saison des résultats, annonces du FOMC, chocs géopolitiques), le niveau ACI s'adapte et les intervalles s'élargissent en quelques pas de temps. Votre dimensionnement de position se réduit en conséquence, sans aucun modèle de volatilité explicite ni logique de détection de régime.

C'est de la quantification d'incertitude en tant que signal de premier ordre, pas une réflexion après coup.

Dimensionnement de position avec des intervalles conformes

Association d'un intervalle d'incertitude calibré à une taille de position : un intervalle étroit produit une grande position, un intervalle large une petite

Relions maintenant la prédiction conforme au dimensionnement concret de position. La variable clé est la demi-largeur de l'intervalle de prédiction relative au cas symétrique du résidu absolu. Avec l'intervalle symétrique [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t], la largeur totale est wt=2q^tw_t = 2\hat{q}_t. Pour garder les formules et le code cohérents, nous mesurons tout par rapport à la largeur totale wtw_t tout au long de l'article.

Dimensionnement par largeur inverse

L'approche la plus simple : dimensionner de manière inversement proportionnelle à la largeur de l'intervalle.

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

kk est une constante d'échelle calibrée sur votre budget de risque. Lorsque le modèle est confiant (intervalle étroit), vous prenez une position plus importante. Lorsqu'il est incertain (intervalle large), vous prenez une position plus petite.

C'est analogue au ciblage de volatilité (size1/σ\text{size} \propto 1/\sigma), mais avec une différence cruciale : la largeur de l'intervalle conforme est une mesure d'incertitude sans hypothèse de distribution, pas une estimation paramétrique de la volatilité. Elle capture l'incertitude prédictive sous la garantie de couverture, pas seulement la variance des rendements.

Dimensionnement par edge ratio et le filtre de non-trading

Le dimensionnement par largeur inverse pure ignore la force du signal lui-même. Un raffinement naturel met à l'échelle par le edge ratio — la prédiction ponctuelle relative à la largeur de l'intervalle :

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

C'est un analogue conforme d'un rapport signal/bruit : rendement attendu divisé par une mesure d'incertitude sans hypothèse de distribution. Nous l'utilisons à la fois pour le dimensionnement et pour un filtre de non-trading.

Le filtre est fondé en principe. Si l'intervalle chevauche zéro,

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

alors l'intervalle de prédiction (1α)(1-\alpha) inclut à la fois des rendements positifs et négatifs — le rendement réalisé peut plausiblement avoir le signe opposé à votre prédiction. Définissez un seuil minimal de edge θ\theta et ne tradez que lorsque et>θe_t > \theta. Notez que le contenu géométrique de « ete_t suffisamment grand pour que l'intervalle ne chevauche plus zéro » est exactement et>1/2e_t > 1/2 (puisque l'intervalle dépasse zéro lorsque μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2). Choisissez θ\theta à l'échelle réelle de et=μ^/wte_t = |\hat{\mu}|/w_t par backtesting ; pour des résidus de rendements quotidiens, ete_t est habituellement bien en dessous de 1/21/2, donc un θ\theta minuscule peut admettre presque tous les trades et un grand n'en admettre aucun. Calibrez-le sur vos données.

À propos du « Kelly conforme »

Il est tentant de greffer des intervalles conformes sur la fraction de Kelly f=pb(1p)bf^* = \frac{pb - (1-p)}{b}. Mais ff^* est déjà une fraction complète et bornée dérivée d'une probabilité de gain pp et d'un ratio de gain bb ; la multiplier par un ratio non borné comme μ^/q^\hat{\mu}/\hat{q} n'a aucune justification en théorie de la décision — cela peut dépasser 1 ou inverser le signe indépendamment de ff^*, et cela compte l'edge deux fois puisque ff^* l'encode déjà. Nous ne présentons donc pas de multiplicateur « Kelly conforme ».

Si vous voulez faire dériver Kelly de l'intervalle, vous devez réellement en dériver pp et bb, ce qui nécessite une hypothèse explicite sur la distribution à l'intérieur de l'intervalle (les intervalles conformes ne disent délibérément rien à ce sujet — voir Limitations). Par exemple, sous une forme supposée à l'intérieur de l'intervalle, vous pouvez approximer pP(rendement>0)p \approx \mathbb{P}(\text{rendement} > 0) et un ratio de gain à partir de la géométrie de l'intervalle — mais signalez cette hypothèse haut et fort, car elle réintroduit exactement l'engagement paramétrique que la prédiction conforme visait à éviter.

L'alternative honnête et peu contraignante consiste à utiliser le edge ratio et=μ^/wte_t = |\hat{\mu}|/w_t comme réduction fractionnaire de Kelly : augmenter la taille lorsque le rendement attendu est grand par rapport à l'intervalle, la réduire lorsqu'il est petit, et appliquer cela au-dessus d'un plafond dur — explicitement comme une heuristique, pas comme « la fraction de Kelly ».

Implémentation Python

Voici une implémentation pratique. Nous présentons à la fois le chemin split/prefit et le chemin temporel (EnbPI), car tout l'enjeu de cet article est que les données financières ne sont pas échangeables.

Configuration et préparation des données

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()

Split conformal avec MAPIE (prefit)

Pour le conformal split/prefit, cv="prefit" requiert method="base" (l'estimateur split-conformal naïf). L'option method="plus" est l'estimateur CV+/jackknife+ et est incompatible avec cv="prefit" — elle nécessite plutôt un objet de validation croisée. Nous utilisons ici la bonne combinaison, et standardisons le dimensionnement sur la largeur totale.

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)

Conformal des séries temporelles avec EnbPI

Comme les rendements ne sont pas échangeables, la répartition aléatoire/prefit ci-dessus n'est qu'une référence de base. Le MapieTimeSeriesRegressor de MAPIE avec method="enbpi" (Xu & Xie, 2021) utilise un bootstrap par blocs et une mise à jour des résidus conçus pour la dépendance temporelle. C'est l'outil qui correspond à l'argument même de cet article.

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)

Inférence conforme adaptative (en ligne)

Pour le trading en direct, nous implémentons une véritable ACI à partir de zéro : maintenir le niveau de mauvaise couverture αt\alpha_t, le mettre à jour de manière additive, et re-dériver le quantile à partir de l'ensemble des résidus à chaque étape. Deux détails à échantillon fini comptent :

  • Utilisez la statistique d'ordre, pas un quantile interpolé. np.quantile interpole par défaut, ce qui peut tomber juste en dessous du rang requis et sous-couvrir ; passez method="higher" (équivalent à "inverted_cdf").
  • Lorsque le rang requis dépasse nn (nn petit, couverture cible élevée), le seuil correct est ++\infty (intervalle = toute la droite), pas un plafonnement au plus grand résidu. Le plafonnement brise silencieusement la garantie 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))

Assemblage : la boucle de backtest

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

Évaluation des résultats

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}")

Considérations pratiques

Choisir le score de non-conformité

Le résidu absolu Yμ^(X)|Y - \hat{\mu}(X)| est celui par défaut, mais il suppose que l'intervalle de prédiction doit être symétrique autour de la prédiction ponctuelle. Pour les rendements financiers, des intervalles asymétriques sont souvent plus pertinents :

  • Régression quantile conformalisée (CQR) : ajustez des régressseurs quantiles aux niveaux α/2\alpha/2 et 1α/21 - \alpha/2, puis conformalisez (Romano et al., 2019). Les intervalles adaptent leur forme à la distribution locale — plus larges à la baisse pendant les drawdowns, plus larges à la hausse pendant les rallyes. (Avec CQR, l'intervalle n'est plus symétrique, donc wtw_t est la véritable largeur haut-moins-bas — continuez à utiliser wtw_t comme dénominateur partout.)
  • Scores normalisés : Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i), où σ^\hat{\sigma} est une estimation de volatilité locale. Cela produit des intervalles conditionnellement adaptatifs — plus étroits en régime de faible volatilité, plus larges en régime de forte volatilité — tout en maintenant la couverture marginale.

Taille de l'ensemble de calibration

La garantie de couverture du conformal par découpage tient pour toute taille d'ensemble de calibration nn, mais la largeur de l'intervalle diminue à mesure que nn augmente. Pour un nn très petit, la statistique d'ordre requise peut ne pas exister ; dans ce cas, le seuil honnête est ++\infty (un intervalle non informatif mais valide) — plafonner au plus grand résidu annule silencieusement la garantie. Pour un trading pratique :

  • n100n \geq 100 points de calibration donne des intervalles raisonnablement étroits.
  • n500n \geq 500 est préférable pour une estimation stable des quantiles.
  • Avec ACI, l'ensemble de calibration ne sert qu'à l'initialisation ; les mises à jour de niveau en ligne s'occupent du reste.

Fréquence de réentraînement

Le modèle de base μ^\hat{\mu} peut devenir obsolète. Deux approches :

  1. Réentraîner périodiquement (par exemple mensuellement) et recalibrer le quantile conforme.
  2. Utiliser ACI et laisser le niveau adaptatif compenser l'obsolescence du modèle — les intervalles s'élargissent automatiquement à mesure que les résidus du modèle croissent.

L'option 2 est plus simple et étonnamment efficace. La couche conforme agit comme un filet de sécurité : même si le modèle se dégrade, la fréquence de couverture ACI à long terme continue de converger vers la cible.

Coûts de transaction

Les intervalles conformes interagissent utilement avec les coûts de transaction. Lorsque les intervalles sont larges (incertitude élevée), les positions sont petites, donc le turnover est faible. Lorsque les intervalles se resserrent (le modèle est confiant), les positions grandissent — mais le modèle a plus de chances d'avoir raison, donc le turnover vaut la peine d'être payé.

Vous pouvez aussi incorporer les coûts de transaction directement dans le filtre de non-trading :

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

Cela garantit que vous ne tradez que lorsque l'edge net attendu dépasse un seuil mis à l'échelle par la largeur conforme — en utilisant le même dénominateur wtw_t que partout ailleurs.

Comparaison avec les méthodes traditionnelles

Propriété IC gaussien IC bootstrap IP conforme
Hypothèse de distribution Erreurs normales i.i.d. + asymptotique Aucune (échangeabilité)
Garantie à échantillon fini Non Non Oui
Fonctionne avec n'importe quel modèle Non Oui Oui
S'adapte à l'hétéroscédasticité Non Partiellement Avec CQR / scores normalisés
Gère le décalage de distribution Non Non Variante ACI / EnbPI
Coût computationnel Faible Moyen Split : faible ; jackknife+ : O(n)O(n) réajustements ; complet : prohibitif

Le bootstrap n'est « asymptotique » que dans sa garantie ; il suppose toujours des données i.i.d./échangeables et de la régularité, donc sa cellule d'hypothèse de distribution n'est pas « sans hypothèse ». Et l'unique colonne « conforme » masque des coûts très différents selon les variantes, que la ligne des coûts détaille désormais.

Limitations

La prédiction conforme n'est pas magique. Principales limitations pour le trading :

  1. Couverture marginale, pas conditionnelle. La garantie est que P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha tient de manière marginale — en moyenne sur l'aléa à la fois de XX et de YY. Pour une valeur de conditionnement spécifique X=xX = x, la couverture locale peut être supérieure ou inférieure à 1α1 - \alpha. La régression quantile conformalisée y remédie partiellement.

  2. L'échangeabilité est une exigence réelle. Pour le conformal par découpage, les données de calibration et le point de test doivent être échangeables. Les données financières ne le sont pas. ACI et EnbPI assouplissent cela en une garantie à long terme, mais la couverture à court terme peut dévier.

  3. La largeur de l'intervalle n'est pas une densité de probabilité. Un intervalle conforme vous dit où YY tombera avec une probabilité 1α1-\alpha, mais ne dit rien sur la distribution à l'intérieur de l'intervalle. Ce n'est pas un substitut à une distribution prédictive complète — c'est précisément pourquoi dériver un pp et un bb de Kelly à partir de celui-ci nécessite une hypothèse supplémentaire et explicite.

  4. Données médiocres en entrée, intervalles plus larges en sortie. Un mauvais modèle de base produit des intervalles larges. La prédiction conforme garantit la couverture mais pas l'utilité. Si votre modèle n'a aucun pouvoir prédictif, les intervalles seront si larges que le dimensionneur de position ne tradera jamais.

Résumé

La prédiction conforme offre un cadre rigoureux et sans hypothèse de distribution pour la quantification de l'incertitude, qui s'intègre naturellement au dimensionnement de position :

  • Le conformal par découpage pour une calibration statique et hors ligne avec couverture à échantillon fini.
  • Le jackknife+ lorsque les données de calibration sont rares et que vous voulez utiliser toutes les observations (au prix de nn réajustements).
  • L'inférence conforme adaptative / EnbPI pour le trading en ligne avec des marchés non stationnaires.
  • Le dimensionnement de position via la largeur inverse de l'intervalle et un filtre de non-trading par edge ratio — et, si vous passez à Kelly, seulement après avoir dérivé pp et bb honnêtement, pas via un multiplicateur injustifié.

L'avantage clé par rapport aux alternatives paramétriques : vous n'avez jamais besoin de spécifier ou de valider une hypothèse de distribution paramétrique. Les intervalles sont honnêtes par construction (marginalement, sous échangeabilité). Pour un trader systématique, cela signifie une source de risque de modèle en moins — et dans un métier où le risque de modèle est existentiel, cela compte.


Références :

  • 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

Gardez une longueur d'avance sur le marché

Abonnez-vous à notre newsletter pour des insights exclusifs sur le trading IA, des analyses de marché et des mises à jour de la plateforme.

Nous respectons votre vie privée. Désabonnement possible à tout moment.