← Voltar aos artigos
June 12, 2026
5 min read

Predição Conformal para Dimensionamento de Posição Sensível ao Risco

Predição Conformal para Dimensionamento de Posição Sensível ao Risco
#uncertainty
#conformal-prediction
#risk
#position-sizing
#statistics
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

Toda fórmula de dimensionamento de posição precisa de uma estimativa de incerteza. O critério de Kelly precisa de uma probabilidade de vitória e uma razão de payoff (veja O critério de Kelly para estratégias). A otimização de média-variância precisa de uma matriz de covariância. O VaR precisa de uma distribuição de retornos. Todas essas abordagens exigem suposições sobre o processo gerador de dados — suposições que os mercados financeiros violam rotineiramente.

A predição conformal oferece algo diferente: intervalos de predição com garantias de cobertura em amostra finita, sem qualquer suposição distributiva paramétrica. Se você pedir 90% de cobertura, obtém pelo menos 90% de cobertura — independentemente de os retornos serem gaussianos, de cauda pesada, assimétricos ou heterocedásticos. O único requisito é a permutabilidade (exchangeability), ou condições mais fracas, como veremos.

Este artigo cobre a teoria, as principais variantes e uma implementação prática para dimensionamento de posição em Python.

A Ideia Central: Scores de Não Conformidade

Distribuição dos scores de não conformidade (resíduos) com um limiar de quantil marcado — o núcleo da calibração conformal

A predição conformal funciona medindo o quão "estranha" é uma nova observação em relação aos dados passados. A estranheza é quantificada por um score de não conformidade — qualquer função que meça o quão mal um ponto de dado se conforma ao padrão visto no restante dos dados.

Para regressão (predizer um valor contínuo como retornos), o score de não conformidade mais simples é o resíduo absoluto:

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

onde μ^\hat{\mu} é qualquer preditor pontual (regressão linear, random forest, rede neural — não importa) e (Xi,Yi)(X_i, Y_i) é um ponto de dado.

O insight central: se os pontos de dado (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) são permutáveis, então o ranking de Rn+1R_{n+1} entre R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} é uniformemente distribuído sobre {1,,n+1}\{1, \ldots, n+1\}. Este é um fato puramente combinatório — não exige suposições sobre a distribuição de XX ou YY.

A partir dessa uniformidade de ranking, podemos construir intervalos de predição com cobertura em amostra finita.

Predição Conformal por Divisão (Split Conformal)

Predição conformal por divisão: dados divididos em conjuntos de treino e calibração, os resíduos de calibração produzindo intervalos de predição

A predição conformal por divisão (Papadopoulos et al., 2002; Lei et al., 2018) é a variante mais prática. O algoritmo é simples:

Passo 1. Divida os dados em um conjunto de treino Dtrain\mathcal{D}_{\text{train}} e um conjunto de calibração Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\}.

Passo 2. Ajuste qualquer modelo μ^\hat{\mu} em Dtrain\mathcal{D}_{\text{train}}.

Passo 3. Calcule os scores de não conformidade no conjunto de calibração:

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

Passo 4. Para um nível de não cobertura desejado α(0,1)\alpha \in (0, 1), tome q^\hat{q} como o quantil empírico (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n} de R1,,RnR_1, \ldots, R_n. Concretamente, este é o (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil-ésimo menor resíduo (e q^=+\hat{q} = +\infty sempre que (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n, ou seja, para nn muito pequeno).

Passo 5. O intervalo de predição para um novo ponto Xn+1X_{n+1} é:

C(Xn+1)=[μ^(Xn+1)q^,  μ^(Xn+1)+q^]C(X_{n+1}) = \left[\hat{\mu}(X_{n+1}) - \hat{q}, \; \hat{\mu}(X_{n+1}) + \hat{q}\right]

A Garantia de Cobertura

Sob permutabilidade dos dados de calibração e do novo ponto de teste:

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

Esta é uma garantia em amostra finita — não uma aproximação assintótica. Vale para qualquer modelo μ^\hat{\mu}, qualquer distribuição dos dados e qualquer tamanho de amostra nn. Se μ^\hat{\mu} é um preditor péssimo, os intervalos simplesmente serão mais largos. A garantia de cobertura ainda vale.

Também existe um limite superior quando os scores não têm 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}, então a cobertura não é desnecessariamente conservadora.

Por Que Isso Importa Para o Trading

Intervalos de predição tradicionais de, digamos, uma regressão linear assumem erros gaussianos. Um intervalo gaussiano calibrado na maior parte dos dados pode julgar muito mal as caudas quando os resíduos reais têm cauda pesada (por exemplo, Student-tt com poucos graus de liberdade): a massa central é mais fina que a gaussiana, então um intervalo gaussiano ajustado pela variância cobre em excesso perto do centro mas cobre de menos nas caudas, e um ajustado pela cauda faz o oposto. O ponto não é um único número mágico — é que a cobertura realizada de um intervalo paramétrico depende de uma suposição distributiva que você não verificou.

Os intervalos de predição conformal contornam isso. Eles se ampliam automaticamente quando o modelo está incerto, e mantêm a cobertura marginal independentemente da distribuição real do erro. Para um trader, isso significa:

  • Se você dimensiona posições inversamente proporcionais à largura do intervalo, você reduz automaticamente a exposição quando o modelo está incerto.
  • A garantia de cobertura significa que suas estimativas de risco são honestas — se você diz "90% dos retornos realizados cairão dentro deste intervalo", essa afirmação é estatisticamente válida (marginalmente, sob permutabilidade).

Predição Conformal Completa e Jackknife+

O split conformal é simples, mas desperdiça dados: o conjunto de calibração não pode ser usado para treino. Duas alternativas resolvem isso.

Predição Conformal Completa (Full Conformal)

A predição conformal completa (Vovk et al., 2005) usa todos os dados tanto para treino quanto para calibração. Para cada valor candidato yy de Yn+1Y_{n+1}:

  1. Aumente o conjunto de dados com (Xn+1,y)(X_{n+1}, y).
  2. Reajuste o modelo no conjunto de dados aumentado.
  3. Calcule todos os scores de não conformidade.
  4. Inclua yy no conjunto de predição se o score de (Xn+1,y)(X_{n+1}, y) não for muito extremo.

O conjunto de predição é:

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

onde RiyR_i^y são os scores de não conformidade calculados com o conjunto de dados aumentado.

O conformal completo fornece os intervalos mais estreitos, mas é computacionalmente proibitivo para a maioria dos modelos — é preciso reajustar o modelo para cada yy candidato em uma grade. Para uma predição de retorno, isso pode significar milhares de reajustes por predição.

Jackknife+ (Barber et al., 2021)

O jackknife+ encontra um equilíbrio. Ele usa resíduos leave-one-out (LOO), mas leva em conta a variabilidade do modelo ajustado entre os folds LOO.

Seja μ^i\hat{\mu}_{-i} o modelo treinado em todos os dados exceto o ponto ii. Defina o score de não conformidade LOO com o resíduo absoluto único:

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

O intervalo de predição do jackknife+ é então construído a partir das predições LOO no ponto de teste, ampliado por esses resíduos:

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]

Aqui q1α+{vi}q_{1-\alpha}^{+}\{v_i\} denota o (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil-ésimo menor valor do conjunto {vi}i=1n\{v_i\}_{i=1}^n, e qα{vi}q_{\alpha}^{-}\{v_i\} o α(n+1)\lfloor \alpha(n+1)\rfloor-ésimo menor valor. O limite inferior subtrai o resíduo de cada predição LOO; o limite superior soma. Essa assimetria é todo o ponto — colapsar ambos os limites para μ^i+Ri\hat{\mu}_{-i} + R_i colocaria o limite inferior acima da predição, o que está errado.

O jackknife+ fornece uma garantia de cobertura de pelo menos 12α1 - 2\alpha (ligeiramente mais fraca que o 1α1-\alpha do split conformal), mas usa todos os dados tanto para treino quanto para calibração. Na prática, a cobertura tipicamente fica próxima de 1α1-\alpha.

Para modelos de trading treinados com dados limitados (por exemplo, modelos específicos de regime com apenas algumas centenas de observações), o jackknife+ é frequentemente a melhor escolha — não sacrifica dados escassos para calibração. O custo é nn reajustes do modelo.

O Problema Com Séries Temporais Financeiras: Não Permutabilidade

Não permutabilidade em séries temporais financeiras: uma série não estacionária com mudança de regime, cobertura rachando à medida que a distribuição se desloca

A garantia conformal padrão exige permutabilidade: a distribuição conjunta de (Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) é invariante sob permutações. Para dados i.i.d., isso vale trivialmente.

Séries temporais financeiras não são permutáveis. Os retornos exibem:

  • Agrupamento de volatilidade: Períodos de alta volatilidade seguem períodos de alta volatilidade (efeitos GARCH).
  • Momentum e reversão à média: Autocorrelação nos retornos ou nos retornos ao quadrado.
  • Mudanças de regime: A distribuição muda ao longo do tempo (mercados em alta vs. em baixa).

Se você aplicar ingenuamente o split conformal a uma série temporal — usando uma divisão de calibração aleatória — você viola a estrutura temporal. Os scores de calibração de um 2017 calmo não refletirão a incerteza de um 2020 volátil. Sua garantia de cobertura se quebra.

Inferência Conformal Adaptativa (ACI)

Inferência conformal adaptativa: um intervalo de predição que se amplia e se estreita via um loop de feedback rastreando a cobertura realizada em direção ao alvo

Gibbs e Candes (2021, NeurIPS) introduziram a Inferência Conformal Adaptativa (ACI) para lidar com deslocamento de distribuição e dados não permutáveis. A ideia é elegante: em vez de usar um nível de cobertura fixo, adapte o nível-alvo de não cobertura online com base em se os intervalos recentes cobriram o resultado real, e reobtenha o quantil da distribuição de scores em cada passo.

O Algoritmo ACI

O ACI não ajusta a largura do intervalo diretamente. Ele mantém um parâmetro adaptativo de não cobertura αt\alpha_t e recalcula o quantil conformal a partir dele. Em cada instante tt:

  1. Calcule o limiar conformal como o quantil empírico (1αt)(1 - \alpha_t) do conjunto atual de resíduos (os scores de calibração, mais quaisquer scores realizados até agora): q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\}).
  2. Observe as características XtX_t, produza o 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. Observe o valor real YtY_t e calcule o indicador de erro errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}.
  4. Atualize o nível (não a largura):

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

onde γ>0\gamma > 0 é um tamanho de passo e α\alpha é a não cobertura alvo. Se um intervalo falhou (errt=1\text{err}_t = 1), αt\alpha_t diminui, o que empurra o próximo quantil para cima e amplia o intervalo; se cobriu, αt\alpha_t aumenta e os intervalos se estreitam. Crucialmente, γ\gamma aqui está em unidades de probabilidade — ele ajusta um nível em [0,1][0,1], não o limiar em unidades brutas de retorno — então o mesmo γ\gamma se comporta de forma sensata quer os resíduos sejam da ordem de 10310^{-3} ou não.

Garantia de Cobertura Para ACI

O ACI fornece uma garantia de cobertura de longo prazo que não depende de um modelo distributivo:

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}

Como αt\alpha_t é limitado a [0,1][0,1], o numerador é limitado por 1/γ1/\gamma vezes uma constante, então o lado direito é O(1/T)O(1/T) e a frequência empírica de não cobertura converge para α\alpha. A afirmação precisa: o ACI garante que a frequência empírica de não cobertura de longo prazo converge para α\alpha para sequências arbitrárias (incluindo adversariais), desde que os níveis adaptados permaneçam limitados — o que o clipe garante. É uma garantia sobre a frequência de cobertura, não sobre a informatividade do intervalo: sob uma sequência verdadeiramente adversarial, os intervalos podem crescer de forma não informativa e ainda assim atingir a meta de cobertura.

ACI Dinamicamente Ajustado (DtACI)

Gibbs e Candes (2024, JMLR) refinaram o ACI com ajuste dinâmico do tamanho de passo γ\gamma. Em vez de fixar γ\gamma, eles mantêm um conjunto de candidatos Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\} e os combinam via uma regra de agregação de especialistas, favorecendo o γ\gamma cuja cobertura recente está mais próxima do alvo.

Isso resolve um problema prático: um γ\gamma grande se adapta rapidamente a mudanças de regime mas produz larguras de intervalo voláteis; um γ\gamma pequeno é estável mas lento para se adaptar. O DtACI equilibra isso automaticamente.

Por Que Isso Importa Para o Trading

Considere uma estratégia de market-making que usa um modelo de previsão de retorno. Durante mercados calmos, os intervalos conformais são estreitos — o modelo está confiante, e você pode assumir posições maiores. Quando a volatilidade dispara (temporada de resultados, anúncios do FOMC, choques geopolíticos), o nível ACI se adapta e os intervalos se ampliam em poucos passos de tempo. Seu dimensionamento de posição encolhe em resposta, sem qualquer modelo explícito de volatilidade ou lógica de detecção de regime.

Isso é quantificação de incerteza como sinal de primeira classe, não como uma reflexão tardia.

Dimensionamento de Posição Com Intervalos Conformais

Mapeando um intervalo de incerteza calibrado para o tamanho da posição: um intervalo estreito impulsiona uma posição grande, um largo, uma posição pequena

Agora vamos conectar a predição conformal ao dimensionamento concreto de posição. A variável-chave é a meia-largura do intervalo de predição em relação ao caso simétrico do resíduo absoluto. Com o intervalo simétrico [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t], a largura total é wt=2q^tw_t = 2\hat{q}_t. Para manter as fórmulas e o código consistentes, medimos tudo em relação à largura total wtw_t ao longo de todo o texto.

Dimensionamento pelo Inverso da Largura

A abordagem mais simples: dimensione inversamente proporcional à largura do intervalo.

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

onde kk é uma constante de escala calibrada para seu orçamento de risco. Quando o modelo está confiante (intervalo estreito), você assume uma posição maior. Quando incerto (intervalo largo), assume uma menor.

Isso é análogo ao volatility targeting (size1/σ\text{size} \propto 1/\sigma), mas com uma diferença crucial: a largura do intervalo conformal é uma medida de incerteza livre de distribuição, não uma estimativa paramétrica de volatilidade. Ela captura a incerteza preditiva sob a garantia de cobertura, não apenas a variância dos retornos.

Dimensionamento por Razão de Edge e o Filtro de Não Negociação

O dimensionamento puro pelo inverso da largura ignora a força do próprio sinal. Um refinamento natural escala pela razão de edge — a predição pontual em relação à largura do intervalo:

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

Este é um análogo conformal de uma razão sinal-ruído: retorno esperado dividido por uma medida de incerteza livre de distribuição. Nós o usamos tanto para dimensionamento quanto para um filtro de não negociação.

O filtro é fundamentado. Se o intervalo abrange o zero,

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

então o intervalo de predição (1α)(1-\alpha) inclui tanto retornos positivos quanto negativos — o retorno realizado pode plausivelmente ter o sinal oposto ao de sua predição. Defina um limiar mínimo de edge θ\theta e negocie apenas quando et>θe_t > \theta. Note que o conteúdo geométrico de "ete_t grande o suficiente para que o intervalo não abranja mais o zero" é exatamente et>1/2e_t > 1/2 (já que o intervalo limpa o zero quando μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2). Escolha θ\theta na escala real de et=μ^/wte_t = |\hat{\mu}|/w_t via backtesting; para resíduos de retornos diários, ete_t geralmente está bem abaixo de 1/21/2, então um θ\theta minúsculo pode admitir quase todas as negociações e um grande pode não admitir nenhuma. Calibre-o para seus dados.

Sobre o "Kelly Conformal"

É tentador acoplar intervalos conformais à fração de Kelly f=pb(1p)bf^* = \frac{pb - (1-p)}{b}. Mas ff^* já é uma fração completa e limitada derivada de uma probabilidade de vitória pp e uma razão de payoff bb; multiplicá-la por uma razão ilimitada como μ^/q^\hat{\mu}/\hat{q} não tem justificativa na teoria da decisão — pode exceder 1 ou inverter o sinal independentemente de ff^*, e conta a edge duplamente, algo que ff^* já codifica. Portanto, não apresentamos um multiplicador de "Kelly conformal".

Se você quiser conduzir o Kelly a partir do intervalo, deve efetivamente derivar pp e bb dele, o que exige uma suposição explícita sobre a distribuição dentro do intervalo (os intervalos conformais deliberadamente não dizem nada sobre isso — veja Limitações). Por exemplo, sob uma forma assumida dentro do intervalo, você pode aproximar pP(retorno>0)p \approx \mathbb{P}(\text{retorno} > 0) e uma razão de payoff a partir da geometria do intervalo — mas sinalize essa suposição em alto e bom som, porque ela reintroduz exatamente o compromisso paramétrico que a predição conformal pretendia evitar.

A alternativa honesta e com poucas suposições é usar a razão de edge et=μ^/wte_t = |\hat{\mu}|/w_t como uma redução fracionária de Kelly: aumente o tamanho quando o retorno esperado for grande em relação ao intervalo, reduza quando for pequeno, e aplique isso sobre um limite máximo rígido — explicitamente como uma heurística, não como "a fração de Kelly".

Implementação em Python

Aqui está uma implementação prática. Mostramos tanto o caminho split/prefit quanto o caminho temporal (EnbPI), já que todo o ponto deste artigo é que os dados financeiros não são permutáveis.

Configuração e Preparação dos Dados

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 Com MAPIE (Prefit)

Para o conformal split/prefit, cv="prefit" exige method="base" (o estimador conformal por divisão ingênuo). A opção method="plus" é o estimador CV+/jackknife+ e é incompatível com cv="prefit" — ela precisa de um objeto de validação cruzada em vez disso. Usamos a combinação correta aqui, e padronizamos o dimensionamento na largura 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 Série Temporal Com EnbPI

Como os retornos não são permutáveis, a divisão aleatória/prefit acima é apenas uma linha de base. O MapieTimeSeriesRegressor do MAPIE com method="enbpi" (Xu & Xie, 2021) usa block bootstrap e atualização de resíduos projetados para dependência temporal. Esta é a ferramenta que corresponde ao próprio argumento deste artigo.

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)

Inferência Conformal Adaptativa (Online)

Para negociação ao vivo, implementamos o ACI verdadeiro do zero: mantemos o nível de não cobertura αt\alpha_t, o atualizamos aditivamente, e reobtemos o quantil do conjunto de resíduos a cada passo. Dois detalhes de amostra finita importam:

  • Use a estatística de ordem, não um quantil interpolado. np.quantile interpola por padrão, o que pode cair um pouco abaixo do ranking exigido e subcobrir; passe method="higher" (equivalente a "inverted_cdf").
  • Quando o ranking exigido excede nn (nn pequeno, alta cobertura alvo), o limiar correto é ++\infty (intervalo = a reta inteira), não um limite fixado no maior resíduo. Fixar o limite quebra silenciosamente a garantia 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))

Juntando Tudo: Loop 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

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

Considerações Práticas

Escolhendo o Score de Não Conformidade

O resíduo absoluto Yμ^(X)|Y - \hat{\mu}(X)| é o padrão, mas assume que o intervalo de predição deve ser simétrico em torno da predição pontual. Para retornos financeiros, intervalos assimétricos frequentemente fazem mais sentido:

  • Regressão Quantílica Conformalizada (CQR): Ajuste regressores quantílicos nos níveis α/2\alpha/2 e 1α/21 - \alpha/2, e depois conformalize (Romano et al., 2019). Os intervalos adaptam sua forma à distribuição local — mais largos no lado negativo durante drawdowns, mais largos no lado positivo durante rallies. (Com CQR, o intervalo deixa de ser simétrico, então wtw_t é a genuína largura superior-menos-inferior — continue usando wtw_t como denominador em todo lugar.)
  • Scores normalizados: Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i), onde σ^\hat{\sigma} é uma estimativa de volatilidade local. Isso produz intervalos condicionalmente adaptativos — mais estreitos em regimes de baixa volatilidade, mais largos em regimes de alta volatilidade — mantendo a cobertura marginal.

Tamanho do Conjunto de Calibração

A garantia de cobertura do split conformal vale para qualquer tamanho de conjunto de calibração nn, mas a largura do intervalo diminui à medida que nn aumenta. Para nn muito pequeno, a estatística de ordem exigida pode não existir, caso em que o limiar honesto é ++\infty (um intervalo não informativo, mas válido) — fixar o limite no maior resíduo anula silenciosamente a garantia. Para negociação prática:

  • n100n \geq 100 pontos de calibração fornece intervalos razoavelmente estreitos.
  • n500n \geq 500 é preferível para estimação estável de quantis.
  • Com o ACI, o conjunto de calibração serve apenas para inicialização; as atualizações do nível online cuidam do resto.

Frequência de Retreinamento

O modelo base μ^\hat{\mu} pode ficar desatualizado. Duas abordagens:

  1. Retreinar periodicamente (por exemplo, mensalmente) e recalibrar o quantil conformal.
  2. Usar ACI e deixar o nível adaptativo compensar a defasagem do modelo — os intervalos se ampliam automaticamente à medida que os resíduos do modelo crescem.

A opção 2 é mais simples e surpreendentemente eficaz. A camada conformal atua como uma rede de segurança: mesmo que o modelo se deteriore, a frequência de cobertura ACI de longo prazo ainda converge para o alvo.

Custos de Transação

Os intervalos conformais interagem com os custos de transação de forma útil. Quando os intervalos são largos (alta incerteza), as posições são pequenas, então o turnover é baixo. Quando os intervalos se estreitam (o modelo está confiante), as posições crescem — mas o modelo tem mais chance de estar certo, então o turnover vale a pena.

Você também pode incorporar custos de transação diretamente no filtro de não negociação:

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

Isso garante que você só negocie quando a edge líquida esperada exceder um limiar escalado pela largura conformal — usando o mesmo denominador wtw_t de sempre.

Comparação Com Métodos Tradicionais

Propriedade IC Gaussiano IC Bootstrap IP Conformal
Suposição de distribuição Erros normais i.i.d. + assintótico Nenhuma (permutabilidade)
Garantia em amostra finita Não Não Sim
Funciona com qualquer modelo Não Sim Sim
Adapta-se à heterocedasticidade Não Parcialmente Com CQR / scores normalizados
Lida com deslocamento de distribuição Não Não Variante ACI / EnbPI
Custo computacional Baixo Médio Split: baixo; jackknife+: O(n)O(n) reajustes; completo: proibitivo

O bootstrap é "assintótico" apenas em sua garantia; ele ainda assume dados i.i.d./permutáveis e suavidade, então sua célula de suposição de distribuição não é "livre de suposições". E a única coluna "conformal" esconde custos muito diferentes entre variantes, o que a linha de custo agora deixa explícito.

Limitações

A predição conformal não é mágica. Principais limitações para negociação:

  1. Cobertura marginal, não condicional. A garantia é que P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha marginalmente — em média sobre a aleatoriedade tanto em XX quanto em YY. Para um valor específico de condicionamento X=xX = x, a cobertura local pode estar acima ou abaixo de 1α1 - \alpha. A regressão quantílica conformalizada aborda isso parcialmente.

  2. A permutabilidade é uma exigência real. Para o split conformal, os dados de calibração e o ponto de teste devem ser permutáveis. Os dados financeiros não são. ACI e EnbPI relaxam isso para uma garantia de longo prazo, mas a cobertura de curto prazo pode se desviar.

  3. A largura do intervalo não é uma densidade de probabilidade. Um intervalo conformal informa onde YY cairá com probabilidade 1α1-\alpha, mas não diz nada sobre a distribuição dentro do intervalo. Não é um substituto para uma distribuição preditiva completa — exatamente por isso derivar um pp e um bb de Kelly a partir dele exige uma suposição adicional e explícita.

  4. Lixo entra, mais largo sai. Um modelo base ruim produz intervalos largos. A predição conformal garante cobertura, mas não utilidade. Se seu modelo não tem poder preditivo, os intervalos serão tão largos que o dimensionador de posição nunca negociará.

Resumo

A predição conformal fornece um arcabouço rigoroso e livre de distribuição para quantificação de incerteza que se encaixa naturalmente no dimensionamento de posição:

  • Split conformal para calibração estática e offline com cobertura em amostra finita.
  • Jackknife+ quando os dados de calibração são escassos e você quer usar todas as observações (ao custo de nn reajustes).
  • Inferência conformal adaptativa / EnbPI para negociação online com mercados não estacionários.
  • Dimensionamento de posição via inverso da largura do intervalo e um filtro de não negociação por razão de edge — e, se você for para o Kelly, apenas após derivar pp e bb honestamente, não via um multiplicador injustificado.

A vantagem chave sobre as alternativas paramétricas: você nunca precisa especificar ou validar uma suposição distributiva paramétrica. Os intervalos são honestos por construção (marginalmente, sob permutabilidade). Para um trader sistemático, isso significa uma fonte a menos de risco de modelo — e em um negócio onde o risco de modelo é existencial, isso importa.


Referências:

  • 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

Fique à frente do mercado

Assine nossa newsletter para insights exclusivos sobre trading com IA, análises de mercado e atualizações da plataforma.

Respeitamos sua privacidade. Cancele a inscrição a qualquer momento.