Prédiction conforme pour un dimensionnement de position tenant compte du risque
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é

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 :
où est n'importe quel prédicteur ponctuel (régression linéaire, forêt aléatoire, réseau de neurones — peu importe) et est un point de donnée.
L'idée clé : si les points de données sont échangeables, alors le rang de parmi est uniformément distribué sur . C'est un fait purement combinatoire — il ne nécessite aucune hypothèse sur la distribution de ou .
À 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)

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 et un ensemble de calibration .
Étape 2. Ajustez un modèle quelconque sur .
Étape 3. Calculez les scores de non-conformité sur l'ensemble de calibration :
Étape 4. Pour un niveau de mauvaise couverture souhaité , prenez comme le quantile empirique de . Concrètement, il s'agit du -ième résidu le plus petit (et dès que , c'est-à-dire pour un très petit).
Étape 5. L'intervalle de prédiction pour un nouveau point est :
La garantie de couverture
Sous l'échangeabilité des données de calibration et du nouveau point de test :
Il s'agit d'une garantie à échantillon fini — pas d'une approximation asymptotique. Elle s'applique à n'importe quel modèle , n'importe quelle distribution des données, et n'importe quelle taille d'échantillon . Si 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 : , 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- à 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 de :
- Augmentez le jeu de données avec .
- Réajustez le modèle sur le jeu de données augmenté.
- Calculez tous les scores de non-conformité.
- Incluez dans l'ensemble de prédiction si le score pour n'est pas trop extrême.
L'ensemble de prédiction est :
où 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 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 le modèle entraîné sur toutes les données sauf le point . Définissez le score de non-conformité LOO avec le résidu absolu unique :
L'intervalle de prédiction jackknife+ est alors construit à partir des prédictions LOO au point de test, élargi par ces résidus :
Ici désigne la -ième plus petite valeur de l'ensemble , et la -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 à 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 (légèrement plus faible que le 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 .
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 réajustements du modèle.
Le problème des séries temporelles financières : la non-échangeabilité

La garantie conforme standard exige l'échangeabilité : la distribution conjointe de 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)

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 et recalcule le quantile conforme à partir de celui-ci. À chaque pas de temps :
- Calculez le seuil conforme comme le quantile empirique de l'ensemble de résidus actuel (les scores de calibration plus tous les scores réalisés jusqu'à présent) : .
- Observez les caractéristiques , produisez l'intervalle .
- Observez la valeur réelle et calculez l'indicateur d'erreur .
- Mettez à jour le niveau (pas la largeur) :
où est une taille de pas et est la mauvaise couverture cible. Si un intervalle a raté (), diminue, ce qui pousse le prochain quantile à la hausse et élargit l'intervalle ; s'il a couvert, augmente et les intervalles se resserrent. Point crucial : est ici en unités de probabilité — il ajuste un niveau dans , pas le seuil en unités de rendement brutes — de sorte que le même se comporte raisonnablement, que les résidus soient de l'ordre de 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 :
Comme est borné à , le numérateur est borné par fois une constante, donc le membre de droite est en et la fréquence empirique de mauvaise couverture converge vers . L'énoncé précis : ACI garantit que la fréquence empirique de mauvaise couverture à long terme converge vers 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 . Au lieu de fixer , ils maintiennent un ensemble de candidats et les combinent via une règle d'agrégation d'experts, favorisant le dont la couverture récente est la plus proche de la cible.
Cela résout un problème pratique : un grand s'adapte rapidement aux changements de régime mais produit des largeurs d'intervalle volatiles ; un petit 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

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 , la largeur totale est . Pour garder les formules et le code cohérents, nous mesurons tout par rapport à la largeur totale 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.
où 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é (), 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 :
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,
alors l'intervalle de prédiction 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 et ne tradez que lorsque . Notez que le contenu géométrique de « suffisamment grand pour que l'intervalle ne chevauche plus zéro » est exactement (puisque l'intervalle dépasse zéro lorsque ). Choisissez à l'échelle réelle de par backtesting ; pour des résidus de rendements quotidiens, est habituellement bien en dessous de , donc un 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 . Mais est déjà une fraction complète et bornée dérivée d'une probabilité de gain et d'un ratio de gain ; la multiplier par un ratio non borné comme n'a aucune justification en théorie de la décision — cela peut dépasser 1 ou inverser le signe indépendamment de , et cela compte l'edge deux fois puisque 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 et , 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 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 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 , 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.quantileinterpole par défaut, ce qui peut tomber juste en dessous du rang requis et sous-couvrir ; passezmethod="higher"(équivalent à"inverted_cdf"). - Lorsque le rang requis dépasse ( petit, couverture cible élevée), le seuil correct est (intervalle = toute la droite), pas un plafonnement au plus grand résidu. Le plafonnement brise silencieusement la garantie .
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 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 et , 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 est la véritable largeur haut-moins-bas — continuez à utiliser comme dénominateur partout.)
- Scores normalisés : , où 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 , mais la largeur de l'intervalle diminue à mesure que augmente. Pour un très petit, la statistique d'ordre requise peut ne pas exister ; dans ce cas, le seuil honnête est (un intervalle non informatif mais valide) — plafonner au plus grand résidu annule silencieusement la garantie. Pour un trading pratique :
- points de calibration donne des intervalles raisonnablement étroits.
- 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 peut devenir obsolète. Deux approches :
- Réentraîner périodiquement (par exemple mensuellement) et recalibrer le quantile conforme.
- 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 :
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 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+ : 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 :
-
Couverture marginale, pas conditionnelle. La garantie est que tient de manière marginale — en moyenne sur l'aléa à la fois de et de . Pour une valeur de conditionnement spécifique , la couverture locale peut être supérieure ou inférieure à . La régression quantile conformalisée y remédie partiellement.
-
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.
-
La largeur de l'intervalle n'est pas une densité de probabilité. Un intervalle conforme vous dit où tombera avec une probabilité , 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 et un de Kelly à partir de celui-ci nécessite une hypothèse supplémentaire et explicite.
-
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 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é et 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.
Authors
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.