← Volver a los artículos
June 12, 2026
5 min de lectura

Predicción conforme para el dimensionamiento de posiciones consciente del riesgo

Predicción conforme para el dimensionamiento de posiciones consciente del riesgo
#uncertainty
#conformal-prediction
#risk
#position-sizing
#statistics
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

Toda fórmula de dimensionamiento de posiciones necesita una estimación de la incertidumbre. El criterio de Kelly necesita una probabilidad de ganancia y una relación de pago (ver El criterio de Kelly para estrategias). La optimización media-varianza necesita una matriz de covarianza. El VaR necesita una distribución de rendimientos. Todo esto requiere supuestos sobre el proceso generador de datos — supuestos que los mercados financieros violan de forma rutinaria.

La predicción conforme ofrece algo distinto: intervalos de predicción con garantías de cobertura de muestra finita, sin ningún supuesto de distribución paramétrica. Si pides una cobertura del 90%, obtienes al menos un 90% de cobertura, sin importar si los rendimientos son gaussianos, de colas pesadas, asimétricos o heterocedásticos. El único requisito es la intercambiabilidad (o condiciones más débiles, como veremos).

Este artículo cubre la teoría, las variantes clave y una implementación práctica para el dimensionamiento de posiciones en Python.

La idea central: puntuaciones de no conformidad

Distribución de las puntuaciones de no conformidad (residuos) con un umbral de cuantil marcado — el núcleo de la calibración conforme

La predicción conforme funciona midiendo cuán "extraña" es una nueva observación en relación con los datos pasados. Esa rareza se cuantifica mediante una puntuación de no conformidad: cualquier función que mida cuán mal se ajusta un punto de datos al patrón observado en el resto de los datos.

Para regresión (predicción de un valor continuo como los rendimientos), la puntuación de no conformidad más simple es el residuo absoluto:

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

donde μ^\hat{\mu} es cualquier predictor puntual (regresión lineal, random forest, red neuronal — no importa cuál) y (Xi,Yi)(X_i, Y_i) es un punto de datos.

La observación clave: si los puntos de datos (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) son intercambiables, entonces el rango de Rn+1R_{n+1} entre R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} se distribuye uniformemente sobre {1,,n+1}\{1, \ldots, n+1\}. Este es un hecho puramente combinatorio — no requiere ningún supuesto sobre la distribución de XX o YY.

A partir de esta uniformidad de rango, podemos construir intervalos de predicción con cobertura de muestra finita.

Predicción conforme split (split conformal)

Predicción conforme split: los datos divididos en pliegues de entrenamiento y calibración, los residuos de calibración produciendo intervalos de predicción

La predicción conforme split (Papadopoulos et al., 2002; Lei et al., 2018) es la variante más práctica. El algoritmo es simple:

Paso 1. Divide los datos en un conjunto de entrenamiento Dtrain\mathcal{D}_{\text{train}} y un conjunto de calibración Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\}.

Paso 2. Ajusta cualquier modelo μ^\hat{\mu} sobre Dtrain\mathcal{D}_{\text{train}}.

Paso 3. Calcula las puntuaciones de no conformidad en el conjunto de calibración:

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

Paso 4. Para un nivel de no cobertura deseado α(0,1)\alpha \in (0, 1), toma q^\hat{q} como el cuantil empírico (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n} de R1,,RnR_1, \ldots, R_n. Concretamente, es el residuo (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil-ésimo más pequeño (y q^=+\hat{q} = +\infty siempre que (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n, es decir, para nn muy pequeño).

Paso 5. El intervalo de predicción para un nuevo punto Xn+1X_{n+1} es:

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 garantía de cobertura

Bajo la intercambiabilidad de los datos de calibración y del nuevo punto de prueba:

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

Esta es una garantía de muestra finita, no una aproximación asintótica. Se cumple para cualquier modelo μ^\hat{\mu}, cualquier distribución de los datos y cualquier tamaño de muestra nn. Si μ^\hat{\mu} es un predictor pésimo, los intervalos simplemente serán más anchos. La garantía de cobertura se mantiene de todos modos.

También existe un límite superior cuando las puntuaciones no tienen empates: 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 modo que la cobertura no es inútilmente conservadora.

Por qué esto importa para el trading

Los intervalos de predicción tradicionales, digamos de una regresión lineal, asumen errores gaussianos. Un intervalo gaussiano calibrado sobre la mayor parte de los datos puede juzgar mal las colas cuando los residuos reales son de colas pesadas (por ejemplo, una t de Student con pocos grados de libertad): la masa central es más delgada que la gaussiana, así que un intervalo gaussiano ajustado por varianza sobrecubre cerca del centro pero subcubre en las colas, y uno ajustado a las colas hace lo contrario. El punto no es un único número mágico — es que la cobertura realizada de un intervalo paramétrico depende de un supuesto de distribución que no has verificado.

Los intervalos de predicción conformes evitan esto. Se ensanchan automáticamente cuando el modelo está incierto y mantienen la cobertura marginal independientemente de la distribución real del error. Para un trader, esto significa:

  • Si dimensionas las posiciones de forma inversamente proporcional al ancho del intervalo, reduces automáticamente la exposición cuando el modelo está incierto.
  • La garantía de cobertura significa que tus estimaciones de riesgo son honestas: si dices "el 90% de los rendimientos realizados caerán dentro de este intervalo", esa afirmación es estadísticamente válida (marginalmente, bajo intercambiabilidad).

Conformal completo y jackknife+

La conformidad split es simple pero desperdicia datos: el conjunto de calibración no puede usarse para entrenar. Dos alternativas abordan esto.

Predicción conforme completa (full conformal)

La predicción conforme completa (Vovk et al., 2005) usa todos los datos tanto para entrenamiento como para calibración. Para cada valor candidato yy de Yn+1Y_{n+1}:

  1. Aumenta el conjunto de datos con (Xn+1,y)(X_{n+1}, y).
  2. Reajusta el modelo sobre el conjunto de datos aumentado.
  3. Calcula todas las puntuaciones de no conformidad.
  4. Incluye yy en el conjunto de predicción si la puntuación de (Xn+1,y)(X_{n+1}, y) no es demasiado extrema.

El conjunto de predicción es:

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\}

donde RiyR_i^y son las puntuaciones de no conformidad calculadas con el conjunto de datos aumentado.

El conformal completo ofrece los intervalos más ajustados, pero es computacionalmente prohibitivo para la mayoría de los modelos — hay que reajustar el modelo para cada candidato yy en una malla. Para una predicción de rendimiento, esto podría significar miles de reajustes por predicción.

Jackknife+ (Barber et al., 2021)

El jackknife+ logra un equilibrio. Utiliza residuos de dejar-uno-fuera (LOO) pero tiene en cuenta la variabilidad del modelo ajustado a través de los pliegues LOO.

Sea μ^i\hat{\mu}_{-i} el modelo entrenado con todos los datos excepto el punto ii. Define la puntuación de no conformidad LOO con el residuo absoluto único:

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

El intervalo de predicción jackknife+ se construye entonces a partir de las predicciones LOO en el punto de prueba, ensanchado por estos residuos:

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]

Aquí q1α+{vi}q_{1-\alpha}^{+}\{v_i\} denota el valor (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil-ésimo más pequeño del conjunto {vi}i=1n\{v_i\}_{i=1}^n, y qα{vi}q_{\alpha}^{-}\{v_i\} el valor α(n+1)\lfloor \alpha(n+1)\rfloor-ésimo más pequeño. El límite inferior resta el residuo de cada predicción LOO; el límite superior lo suma. Esa asimetría es todo el punto — colapsar ambos límites a μ^i+Ri\hat{\mu}_{-i} + R_i pondría el límite inferior por encima de la predicción, lo cual es incorrecto.

El jackknife+ ofrece una garantía de cobertura de al menos 12α1 - 2\alpha (ligeramente más débil que el 1α1-\alpha de la conformidad split), pero usa todos los datos tanto para entrenamiento como para calibración. En la práctica, la cobertura suele acercarse a 1α1-\alpha.

Para modelos de trading entrenados con datos limitados (por ejemplo, modelos específicos de régimen con solo unos cientos de observaciones), el jackknife+ suele ser la mejor opción: no sacrifica datos escasos para la calibración. El costo son nn reajustes del modelo.

El problema con las series temporales financieras: no intercambiabilidad

No intercambiabilidad en series temporales financieras: una serie no estacionaria con un cambio de régimen, la cobertura resquebrajándose a medida que la distribución se desplaza

La garantía conforme estándar requiere intercambiabilidad: la distribución conjunta de (Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) es invariante bajo permutaciones. Para datos i.i.d., esto se cumple trivialmente.

Las series temporales financieras no son intercambiables. Los rendimientos exhiben:

  • Agrupamiento de volatilidad (volatility clustering): Los periodos de alta volatilidad siguen a periodos de alta volatilidad (efectos GARCH).
  • Momentum y reversión a la media: Autocorrelación en los rendimientos o en los rendimientos al cuadrado.
  • Cambios de régimen: La distribución se desplaza con el tiempo (mercados alcistas frente a bajistas).

Si aplicas ingenuamente la conformidad split a una serie temporal — usando una división de calibración aleatoria —, violas la estructura temporal. Las puntuaciones de calibración de un tranquilo 2017 no reflejarán la incertidumbre de un volátil 2020. Tu garantía de cobertura se rompe.

Inferencia conforme adaptativa (ACI)

Inferencia conforme adaptativa: un intervalo de predicción que se ensancha y estrecha mediante un bucle de retroalimentación que rastrea la cobertura realizada hacia el objetivo

Gibbs y Candes (2021, NeurIPS) introdujeron la inferencia conforme adaptativa (ACI) para manejar el desplazamiento de distribución y los datos no intercambiables. La idea es elegante: en lugar de usar un nivel de cobertura fijo, adaptar en línea el nivel objetivo de no cobertura según si los intervalos recientes cubrieron el resultado real, y re-derivar el cuantil a partir de la distribución de puntuaciones en cada paso.

El algoritmo ACI

ACI no ajusta directamente el ancho del intervalo. Mantiene un parámetro de no cobertura adaptativo αt\alpha_t y recalcula el cuantil conforme a partir de él. En cada paso de tiempo tt:

  1. Calcula el umbral conforme como el cuantil empírico (1αt)(1 - \alpha_t) del conjunto de residuos actual (las puntuaciones de calibración más cualquier puntuación realizada hasta el momento): q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\}).
  2. Observa las características XtX_t, produce el intervalo 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. Observa el valor real YtY_t y calcula el indicador de error errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}.
  4. Actualiza el nivel (no el ancho):

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

donde γ>0\gamma > 0 es un tamaño de paso y α\alpha es la no cobertura objetivo. Si un intervalo falló (errt=1\text{err}_t = 1), αt\alpha_t se reduce, lo que empuja el siguiente cuantil hacia arriba y ensancha el intervalo; si cubrió, αt\alpha_t crece y los intervalos se estrechan. Fundamentalmente, γ\gamma aquí está en unidades de probabilidad — ajusta un nivel en [0,1][0,1], no el umbral en unidades de rendimiento crudas —, de modo que el mismo γ\gamma se comporta razonablemente ya sea que los residuos estén del orden de 10310^{-3} o no.

Garantía de cobertura para ACI

ACI ofrece una garantía de cobertura a largo plazo que no depende de un modelo de distribución:

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}

Dado que αt\alpha_t está acotado a [0,1][0,1], el numerador está acotado por 1/γ1/\gamma por una constante, así que el lado derecho es O(1/T)O(1/T) y la frecuencia empírica de no cobertura converge a α\alpha. La afirmación precisa: ACI garantiza que la frecuencia empírica de no cobertura a largo plazo converge a α\alpha para secuencias arbitrarias (incluso adversariales), siempre que los niveles adaptados permanezcan acotados, lo cual el clip impone. Es una garantía sobre la frecuencia de cobertura, no sobre la informatividad del intervalo: bajo una secuencia verdaderamente adversarial, los intervalos pueden crecer de forma no informativa mientras siguen alcanzando el objetivo de cobertura.

ACI de ajuste dinámico (DtACI)

Gibbs y Candes (2024, JMLR) refinaron ACI con ajuste dinámico del tamaño de paso γ\gamma. En lugar de fijar γ\gamma, mantienen un conjunto de candidatos Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\} y los combinan mediante una regla de agregación de expertos, favoreciendo el γ\gamma cuya cobertura reciente esté más cerca del objetivo.

Esto aborda un problema práctico: un γ\gamma grande se adapta rápidamente a los cambios de régimen pero produce anchos de intervalo volátiles; un γ\gamma pequeño es estable pero lento para adaptarse. DtACI equilibra esto automáticamente.

Por qué esto importa para el trading

Considera una estrategia de creación de mercado que usa un modelo de predicción de rendimientos. Durante mercados tranquilos, los intervalos conformes son estrechos: el modelo está seguro, y puedes tomar posiciones más grandes. Cuando la volatilidad se dispara (temporada de resultados, anuncios de la FOMC, choques geopolíticos), el nivel ACI se adapta y los intervalos se ensanchan en pocos pasos de tiempo. Tu dimensionamiento de posiciones se reduce en respuesta, sin ningún modelo de volatilidad explícito ni lógica de detección de régimen.

Esto es cuantificación de incertidumbre como señal de primera clase, no como una idea de último momento.

Dimensionamiento de posiciones con intervalos conformes

Mapeo de un intervalo de incertidumbre calibrado al tamaño de posición: un intervalo estrecho impulsa una posición grande, uno ancho una pequeña

Conectemos ahora la predicción conforme con el dimensionamiento concreto de posiciones. La variable clave es la media anchura del intervalo de predicción relativa al caso simétrico de residuo absoluto. Con el intervalo simétrico [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t], el ancho total es wt=2q^tw_t = 2\hat{q}_t. Para mantener las fórmulas y el código coherentes, medimos todo frente al ancho total wtw_t a lo largo del texto.

Dimensionamiento por ancho inverso

El enfoque más simple: dimensionar de forma inversamente proporcional al ancho del intervalo.

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

donde kk es una constante de escala calibrada a tu presupuesto de riesgo. Cuando el modelo está seguro (intervalo estrecho), tomas una posición más grande. Cuando está incierto (intervalo ancho), tomas una más pequeña.

Esto es análogo al volatility targeting (size1/σ\text{size} \propto 1/\sigma), pero con una diferencia crucial: el ancho del intervalo conforme es una medida de incertidumbre libre de distribución, no una estimación paramétrica de volatilidad. Captura la incertidumbre predictiva bajo la garantía de cobertura, no solo la varianza de los rendimientos.

Dimensionamiento por edge ratio y el filtro de no operar

El dimensionamiento por ancho inverso puro ignora la fuerza de la propia señal. Un refinamiento natural escala por el edge ratio — la predicción puntual relativa al ancho del intervalo:

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

Este es un análogo conforme de una relación señal-ruido: rendimiento esperado dividido por una medida de incertidumbre libre de distribución. Lo usamos tanto para el dimensionamiento como para un filtro de no operar.

El filtro es principiado. Si el intervalo abarca el cero,

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

entonces el intervalo de predicción (1α)(1-\alpha) incluye tanto rendimientos positivos como negativos: el rendimiento realizado puede plausiblemente tener el signo opuesto a tu predicción. Define un umbral mínimo de edge θ\theta y opera solo cuando et>θe_t > \theta. Ten en cuenta que el contenido geométrico de "ete_t suficientemente grande como para que el intervalo ya no abarque el cero" es exactamente et>1/2e_t > 1/2 (ya que el intervalo despeja el cero cuando μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2). Elige θ\theta en la escala real de et=μ^/wte_t = |\hat{\mu}|/w_t mediante backtesting; para residuos de rendimientos diarios, ete_t suele estar muy por debajo de 1/21/2, así que un θ\theta diminuto puede admitir casi todas las operaciones y uno grande puede no admitir ninguna. Calíbralo con tus datos.

Sobre el "Kelly conforme"

Es tentador acoplar intervalos conformes a la fracción de Kelly f=pb(1p)bf^* = \frac{pb - (1-p)}{b}. Pero ff^* ya es una fracción completa y acotada derivada de una probabilidad de ganancia pp y una relación de pago bb; multiplicarla por una relación no acotada como μ^/q^\hat{\mu}/\hat{q} no tiene justificación en teoría de la decisión — puede superar 1 o invertir el signo independientemente de ff^*, y cuenta dos veces el edge que ff^* ya codifica. Por eso no presentamos un multiplicador de "Kelly conforme".

Si quieres derivar Kelly a partir del intervalo, debes obtener realmente pp y bb de él, lo que requiere un supuesto explícito sobre la distribución dentro del intervalo (los intervalos conformes deliberadamente no dicen nada sobre eso — ver Limitaciones). Por ejemplo, bajo una forma supuesta dentro del intervalo, puedes aproximar pP(rendimiento>0)p \approx \mathbb{P}(\text{rendimiento} > 0) y una relación de pago a partir de la geometría del intervalo — pero señala ese supuesto en voz alta, porque reintroduce exactamente el compromiso paramétrico que la predicción conforme pretendía evitar.

La alternativa honesta y con pocos supuestos es usar el edge ratio et=μ^/wte_t = |\hat{\mu}|/w_t como reducción fraccional de Kelly: aumentar el tamaño cuando el rendimiento esperado es grande en relación con el intervalo, reducirlo cuando es pequeño, y aplicar esto sobre un límite máximo estricto — explícitamente como una heurística, no como "la fracción de Kelly".

Implementación en Python

Aquí una implementación práctica. Mostramos tanto la ruta split/prefit como la ruta temporal (EnbPI), ya que todo el propósito de este artículo es que los datos financieros no son intercambiables.

Configuración y preparación de datos

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

Conformal split con MAPIE (prefit)

Para conformal split/prefit, cv="prefit" requiere method="base" (el estimador conforme split ingenuo). La opción method="plus" es el estimador CV+/jackknife+ y es incompatible con cv="prefit" — necesita un objeto de validación cruzada en su lugar. Aquí usamos la combinación correcta y estandarizamos el dimensionamiento sobre el ancho total.

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 de series temporales con EnbPI

Dado que los rendimientos no son intercambiables, la división aleatoria/prefit anterior es solo una línea base. El MapieTimeSeriesRegressor de MAPIE con method="enbpi" (Xu & Xie, 2021) usa bootstrap por bloques y actualización de residuos diseñados para la dependencia temporal. Esta es la herramienta que coincide con el propio argumento de este artículo.

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)

Inferencia conforme adaptativa (en línea)

Para el trading en vivo implementamos ACI real desde cero: mantener el nivel de no cobertura αt\alpha_t, actualizarlo de forma aditiva y re-derivar el cuantil a partir del conjunto de residuos en cada paso. Dos detalles de muestra finita importan:

  • Usa el estadístico de orden, no un cuantil interpolado. np.quantile interpola por defecto, lo que puede caer justo por debajo del rango requerido y causar subcobertura; pasa method="higher" (equivalente a "inverted_cdf").
  • Cuando el rango requerido excede nn (nn pequeño, cobertura objetivo alta), el umbral correcto es ++\infty (intervalo = toda la recta), no un recorte al residuo más grande. Recortar rompe silenciosamente la garantía 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))

Uniéndolo todo: bucle 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

Evaluación de los resultados

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

Consideraciones prácticas

Elección de la puntuación de no conformidad

El residuo absoluto Yμ^(X)|Y - \hat{\mu}(X)| es el predeterminado, pero asume que el intervalo de predicción debe ser simétrico alrededor de la predicción puntual. Para rendimientos financieros, los intervalos asimétricos a menudo tienen más sentido:

  • Regresión cuantílica conformalizada (CQR): Ajusta regresores cuantílicos en los niveles α/2\alpha/2 y 1α/21 - \alpha/2, luego conformaliza (Romano et al., 2019). Los intervalos adaptan su forma a la distribución local — más anchos a la baja durante drawdowns, más anchos al alza durante rallies. (Con CQR el intervalo ya no es simétrico, así que wtw_t es el ancho genuino superior-menos-inferior — sigue usando wtw_t como denominador en todas partes.)
  • Puntuaciones normalizadas: Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i), donde σ^\hat{\sigma} es una estimación de volatilidad local. Esto produce intervalos condicionalmente adaptativos — más estrechos durante regímenes de baja volatilidad, más anchos durante los de alta volatilidad — manteniendo la cobertura marginal.

Tamaño del conjunto de calibración

La garantía de cobertura de la conformidad split se cumple para cualquier tamaño de conjunto de calibración nn, pero el ancho del intervalo disminuye a medida que nn aumenta. Para un nn muy pequeño, el estadístico de orden requerido puede no existir, en cuyo caso el umbral honesto es ++\infty (un intervalo no informativo pero válido) — recortar al residuo más grande anula silenciosamente la garantía. Para trading práctico:

  • n100n \geq 100 puntos de calibración da intervalos razonablemente ajustados.
  • n500n \geq 500 es preferible para una estimación estable de cuantiles.
  • Con ACI, el conjunto de calibración es solo para inicialización; las actualizaciones de nivel en línea se encargan del resto.

Frecuencia de reentrenamiento

El modelo base μ^\hat{\mu} puede quedar obsoleto. Dos enfoques:

  1. Reentrenar periódicamente (por ejemplo, mensualmente) y recalibrar el cuantil conforme.
  2. Usar ACI y dejar que el nivel adaptativo compense la obsolescencia del modelo — los intervalos se ensanchan automáticamente a medida que crecen los residuos del modelo.

La opción 2 es más simple y sorprendentemente efectiva. La capa conforme actúa como una red de seguridad: incluso si el modelo se degrada, la frecuencia de cobertura ACI a largo plazo sigue convergiendo hacia el objetivo.

Costos de transacción

Los intervalos conformes interactúan con los costos de transacción de una manera útil. Cuando los intervalos son anchos (alta incertidumbre), las posiciones son pequeñas, por lo que el turnover es bajo. Cuando los intervalos se estrechan (el modelo está seguro), las posiciones crecen — pero es más probable que el modelo acierte, así que vale la pena pagar el turnover.

También puedes incorporar los costos de transacción directamente en el filtro de no operar:

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

Esto asegura que solo operas cuando el edge neto esperado supera un umbral escalado por el ancho conforme — usando el mismo denominador wtw_t que en todas partes.

Comparación con métodos tradicionales

Propiedad IC gaussiano IC bootstrap IP conforme
Supuesto de distribución Errores normales i.i.d. + asintótico Ninguno (intercambiabilidad)
Garantía de muestra finita No No
Funciona con cualquier modelo No
Se adapta a la heterocedasticidad No Parcialmente Con CQR / puntuaciones normalizadas
Maneja el desplazamiento de distribución No No Variante ACI / EnbPI
Costo computacional Bajo Medio Split: bajo; jackknife+: O(n)O(n) reajustes; completo: prohibitivo

El bootstrap es "asintótico" solo en su garantía; sigue asumiendo datos i.i.d./intercambiables y suavidad, por lo que su celda de supuesto de distribución no es "libre de supuestos". Y la única columna "conforme" oculta costos muy diferentes entre las variantes, lo que la fila de costos ahora detalla.

Limitaciones

La predicción conforme no es magia. Limitaciones clave para el trading:

  1. Cobertura marginal, no condicional. La garantía es que P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha se cumple de forma marginal — promediada sobre la aleatoriedad tanto en XX como en YY. Para un valor de condicionamiento específico X=xX = x, la cobertura local puede estar por encima o por debajo de 1α1 - \alpha. La regresión cuantílica conformalizada aborda esto parcialmente.

  2. La intercambiabilidad es un requisito real. Para la conformidad split, los datos de calibración y el punto de prueba deben ser intercambiables. Los datos financieros no lo son. ACI y EnbPI relajan esto a una garantía a largo plazo, pero la cobertura a corto plazo puede desviarse.

  3. El ancho del intervalo no es una densidad de probabilidad. Un intervalo conforme te dice dónde caerá YY con probabilidad 1α1-\alpha, pero no dice nada sobre la distribución dentro del intervalo. No es un sustituto de una distribución predictiva completa — precisamente por eso derivar una pp y una bb de Kelly a partir de él requiere un supuesto adicional y explícito.

  4. Basura entra, más ancho sale. Un mal modelo base produce intervalos anchos. La predicción conforme garantiza cobertura, pero no utilidad. Si tu modelo no tiene poder predictivo, los intervalos serán tan anchos que el dimensionador de posiciones nunca operará.

Resumen

La predicción conforme ofrece un marco riguroso y libre de distribución para la cuantificación de la incertidumbre que encaja de forma natural en el dimensionamiento de posiciones:

  • Conformidad split para calibración estática, sin conexión, con cobertura de muestra finita.
  • Jackknife+ cuando los datos de calibración son escasos y quieres usar todas las observaciones (al costo de nn reajustes).
  • Inferencia conforme adaptativa / EnbPI para trading en línea con mercados no estacionarios.
  • Dimensionamiento de posiciones mediante el ancho inverso del intervalo y un filtro de no operar por edge ratio — y, si te decantas por Kelly, solo después de derivar pp y bb honestamente, no mediante un multiplicador injustificado.

La ventaja clave frente a las alternativas paramétricas: nunca necesitas especificar ni validar un supuesto de distribución paramétrico. Los intervalos son honestos por construcción (marginalmente, bajo intercambiabilidad). Para un trader sistemático, esto significa una fuente menos de riesgo de modelo — y en un negocio donde el riesgo de modelo es existencial, eso importa.


Referencias:

  • 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

Mantente a la vanguardia

Suscríbete a nuestro boletín para recibir información exclusiva sobre trading con IA, análisis de mercado y actualizaciones de la plataforma.

Respetamos tu privacidad. Puedes darte de baja en cualquier momento.