← Torna agli articoli
June 12, 2026
5 min di lettura

Conformal Prediction per il dimensionamento delle posizioni consapevole del rischio

Conformal Prediction per il dimensionamento delle posizioni consapevole del rischio
#incertezza
#conformal-prediction
#rischio
#position-sizing
#statistica
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

Ogni formula di dimensionamento delle posizioni necessita di una stima dell'incertezza. Il criterio di Kelly richiede una probabilità di vincita e un rapporto di payoff (vedi Il criterio di Kelly per le strategie). L'ottimizzazione media-varianza richiede una matrice di covarianza. Il VaR richiede una distribuzione dei rendimenti. Tutti questi metodi richiedono assunzioni sul processo generatore dei dati, assunzioni che i mercati finanziari violano abitualmente.

La conformal prediction offre qualcosa di diverso: intervalli di previsione con garanzie di copertura a campione finito, senza alcuna assunzione distribuzionale parametrica. Se chiedi una copertura del 90%, ottieni almeno il 90% di copertura, indipendentemente dal fatto che i rendimenti siano gaussiani, a code spesse, asimmetrici o eteroschedastici. L'unico requisito è la scambiabilità (o condizioni più deboli, come vedremo).

Questo articolo copre la teoria, le varianti principali e un'implementazione pratica per il dimensionamento delle posizioni in Python.

L'idea centrale: i punteggi di nonconformità

Distribuzione dei punteggi di nonconformità (residui) con una soglia quantile marcata — il cuore della calibrazione conformal

La conformal prediction funziona misurando quanto sia "strana" una nuova osservazione rispetto ai dati passati. La stranezza è quantificata da un punteggio di nonconformità: una qualsiasi funzione che misura quanto male un punto dato si conforma allo schema osservato nel resto dei dati.

Per la regressione (predire un valore continuo come i rendimenti), il punteggio di nonconformità più semplice è il residuo assoluto:

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

dove μ^\hat{\mu} è un qualsiasi predittore puntuale (regressione lineare, random forest, rete neurale: non importa) e (Xi,Yi)(X_i, Y_i) è un punto dato.

L'intuizione chiave: se i punti dati (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) sono scambiabili, allora il rango di Rn+1R_{n+1} tra R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} è distribuito uniformemente su {1,,n+1}\{1, \ldots, n+1\}. Questo è un fatto puramente combinatorio: non richiede alcuna assunzione sulla distribuzione di XX o YY.

Da questa uniformità del rango possiamo costruire intervalli di previsione con copertura a campione finito.

Split conformal prediction

Split conformal prediction: dati suddivisi in fold di training e calibrazione, con i residui di calibrazione che producono gli intervalli di previsione

La split conformal prediction (Papadopoulos et al., 2002; Lei et al., 2018) è la variante più pratica. L'algoritmo è semplice:

Passo 1. Dividi i dati in un training set Dtrain\mathcal{D}_{\text{train}} e un calibration set Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\}.

Passo 2. Addestra un qualsiasi modello μ^\hat{\mu} su Dtrain\mathcal{D}_{\text{train}}.

Passo 3. Calcola i punteggi di nonconformità sul calibration set:

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

Passo 4. Per un livello di mancata copertura desiderato α(0,1)\alpha \in (0, 1), prendi q^\hat{q} come il quantile empirico (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n} di R1,,RnR_1, \ldots, R_n. Concretamente, questo è il (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil-esimo residuo più piccolo (e q^=+\hat{q} = +\infty ogni volta che (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n, cioè per nn molto piccoli).

Passo 5. L'intervallo di previsione per un nuovo punto 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]

La garanzia di copertura

Sotto scambiabilità dei dati di calibrazione e del nuovo punto di test:

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

Questa è una garanzia a campione finito, non un'approssimazione asintotica. Vale per qualsiasi modello μ^\hat{\mu}, qualsiasi distribuzione dei dati e qualsiasi dimensione campionaria nn. Se μ^\hat{\mu} è un predittore pessimo, gli intervalli saranno semplicemente più ampi. La garanzia di copertura continua a valere.

Esiste anche un limite superiore quando i punteggi non hanno pareggi: 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}, quindi la copertura non è inutilmente conservativa.

Perché questo è importante per il trading

Gli intervalli di previsione tradizionali, ad esempio quelli di una regressione lineare, assumono errori gaussiani. Un intervallo gaussiano calibrato sul grosso dei dati può valutare male le code quando i veri residui sono a code spesse (per esempio una tt di Student con pochi gradi di libertà): la massa centrale è più sottile di quella gaussiana, quindi un intervallo gaussiano con varianza adattata sovra-copre vicino al centro ma sotto-copre nelle code, mentre uno adattato alle code fa l'opposto. Il punto non è un singolo numero magico, ma il fatto che la copertura realizzata di un intervallo parametrico dipende da un'assunzione distribuzionale che non hai verificato.

Gli intervalli di conformal prediction aggirano questo problema. Si allargano automaticamente quando il modello è incerto e mantengono la copertura marginale indipendentemente dalla vera distribuzione degli errori. Per un trader, questo significa:

  • Se dimensioni le posizioni in modo inversamente proporzionale alla larghezza dell'intervallo, riduci automaticamente l'esposizione quando il modello è incerto.
  • La garanzia di copertura significa che le tue stime di rischio sono oneste: se affermi che "il 90% dei rendimenti realizzati cadrà all'interno di questo intervallo", tale affermazione è statisticamente valida (marginalmente, sotto scambiabilità).

Full conformal e jackknife+

La split conformal è semplice ma spreca dati: il calibration set non può essere usato per il training. Due alternative affrontano questo problema.

Full conformal prediction

La full conformal prediction (Vovk et al., 2005) usa tutti i dati sia per il training sia per la calibrazione. Per ogni valore candidato yy di Yn+1Y_{n+1}:

  1. Aumenta il dataset con (Xn+1,y)(X_{n+1}, y).
  2. Riaddestra il modello sul dataset aumentato.
  3. Calcola tutti i punteggi di nonconformità.
  4. Includi yy nel set di previsione se il punteggio per (Xn+1,y)(X_{n+1}, y) non è troppo estremo.

Il set di previsione è:

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

dove gli RiyR_i^y sono i punteggi di nonconformità calcolati con il dataset aumentato.

La full conformal fornisce gli intervalli più stretti ma è computazionalmente proibitiva per la maggior parte dei modelli: devi riaddestrare il modello per ogni candidato yy su una griglia. Per una previsione di rendimento, questo potrebbe significare migliaia di riaddestramenti per ogni previsione.

Jackknife+ (Barber et al., 2021)

Il jackknife+ trova un equilibrio. Usa i residui leave-one-out (LOO) ma tiene conto della variabilità del modello addestrato attraverso i fold LOO.

Sia μ^i\hat{\mu}_{-i} il modello addestrato su tutti i dati eccetto il punto ii. Definisci il punteggio di nonconformità LOO con il singolo residuo assoluto:

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

L'intervallo di previsione jackknife+ viene quindi costruito a partire dalle previsioni LOO nel punto di test, allargate da questi residui:

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]

Qui q1α+{vi}q_{1-\alpha}^{+}\{v_i\} denota il (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil-esimo valore più piccolo dell'insieme {vi}i=1n\{v_i\}_{i=1}^n, e qα{vi}q_{\alpha}^{-}\{v_i\} il α(n+1)\lfloor \alpha(n+1)\rfloor-esimo valore più piccolo. Il limite inferiore sottrae il residuo da ciascuna previsione LOO; il limite superiore lo aggiunge. Questa asimmetria è il punto cruciale: collassare entrambi i limiti a μ^i+Ri\hat{\mu}_{-i} + R_i porterebbe il limite inferiore sopra la previsione, il che è sbagliato.

Il jackknife+ fornisce una garanzia di copertura di almeno 12α1 - 2\alpha (leggermente più debole di 1α1-\alpha della split conformal), ma usa tutti i dati sia per il training sia per la calibrazione. In pratica la copertura è tipicamente vicina a 1α1-\alpha.

Per i modelli di trading addestrati su dati limitati (per esempio modelli specifici per regime con solo poche centinaia di osservazioni), il jackknife+ è spesso la scelta migliore: non sacrifica dati scarsi per la calibrazione. Il costo è di nn riaddestramenti del modello.

Il problema con le serie temporali finanziarie: la non-scambiabilità

Non-scambiabilità nelle serie temporali finanziarie: una serie non stazionaria con un cambio di regime, la copertura che si incrina mentre la distribuzione deriva

La garanzia conformal standard richiede la scambiabilità: la distribuzione congiunta di (Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) è invariante rispetto alle permutazioni. Per dati i.i.d. questo vale banalmente.

Le serie temporali finanziarie non sono scambiabili. I rendimenti presentano:

  • Volatility clustering: i periodi ad alta volatilità seguono periodi ad alta volatilità (effetti GARCH).
  • Momentum e mean reversion: autocorrelazione nei rendimenti o nei rendimenti al quadrato.
  • Cambi di regime: la distribuzione si sposta nel tempo (mercati rialzisti contro ribassisti).

Se applichi ingenuamente la split conformal a una serie temporale, usando una suddivisione di calibrazione casuale, violi la struttura temporale. I punteggi di calibrazione di un 2017 tranquillo non rifletteranno l'incertezza di un 2020 volatile. La tua garanzia di copertura si rompe.

Adaptive conformal inference (ACI)

Adaptive conformal inference: un intervallo di previsione che si allarga e si restringe tramite un anello di retroazione che traccia la copertura realizzata verso il target

Gibbs e Candes (2021, NeurIPS) hanno introdotto la adaptive conformal inference (ACI) per gestire il distribution shift e i dati non scambiabili. L'idea è elegante: invece di usare un livello di copertura fisso, si adatta il livello target di mancata copertura online in base al fatto che gli intervalli recenti abbiano coperto o meno l'esito vero, e si rideriva il quantile dalla distribuzione dei punteggi a ogni passo.

L'algoritmo ACI

ACI non modifica direttamente la larghezza dell'intervallo. Mantiene un parametro adattivo di mancata copertura αt\alpha_t e ricalcola da esso il quantile conformal. A ogni passo temporale tt:

  1. Calcola la soglia conformal come il quantile empirico (1αt)(1 - \alpha_t) dell'insieme corrente di residui (i punteggi di calibrazione, più eventuali punteggi realizzati finora): q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\}).
  2. Osserva le feature XtX_t, produci l'intervallo 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. Osserva il valore vero YtY_t e calcola l'indicatore di errore errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}.
  4. Aggiorna il livello (non la larghezza):

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

dove γ>0\gamma > 0 è uno step size e α\alpha è la mancata copertura target. Se un intervallo ha mancato (errt=1\text{err}_t = 1), αt\alpha_t si riduce, il che spinge il quantile successivo più in alto e allarga l'intervallo; se ha coperto, αt\alpha_t cresce e gli intervalli si restringono. Fondamentale: qui γ\gamma è in unità di probabilità — modifica un livello in [0,1][0,1], non la soglia in unità grezze di rendimento — quindi lo stesso γ\gamma si comporta in modo sensato sia che i residui siano dell'ordine di 10310^{-3} sia che non lo siano.

Garanzia di copertura per ACI

ACI fornisce una garanzia di copertura sul lungo periodo che non dipende da un modello distribuzionale:

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}

Poiché αt\alpha_t è limitato a [0,1][0,1], il numeratore è limitato da 1/γ1/\gamma moltiplicato per una costante, quindi il membro di destra è O(1/T)O(1/T) e la frequenza empirica di mancata copertura converge ad α\alpha. L'enunciato preciso: ACI garantisce che la frequenza empirica di mancata copertura sul lungo periodo converga ad α\alpha per sequenze arbitrarie (incluse quelle avversarie), a condizione che i livelli adattati restino limitati, cosa che il clip impone. È una garanzia sulla frequenza di copertura, non sull'informatività dell'intervallo: sotto una sequenza realmente avversaria gli intervalli possono crescere fino a diventare poco informativi pur centrando il target di copertura.

Dynamically-Tuned ACI (DtACI)

Gibbs e Candes (2024, JMLR) hanno raffinato ACI con il tuning dinamico dello step size γ\gamma. Invece di fissare γ\gamma, mantengono un insieme di candidati Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\} e li combinano tramite una regola di aggregazione di esperti, favorendo il γ\gamma la cui copertura recente è più vicina al target.

Questo affronta un problema pratico: un γ\gamma grande si adatta rapidamente ai cambi di regime ma produce larghezze di intervallo volatili; un γ\gamma piccolo è stabile ma lento ad adattarsi. DtACI bilancia automaticamente questi aspetti.

Perché questo è importante per il trading

Considera una strategia di market making che usa un modello di previsione dei rendimenti. Durante i mercati tranquilli, gli intervalli conformal sono stretti: il modello è fiducioso e puoi assumere posizioni più grandi. Quando la volatilità schizza (stagione delle trimestrali, annunci FOMC, shock geopolitici), il livello ACI si adatta e gli intervalli si allargano nel giro di pochi passi temporali. Il tuo dimensionamento delle posizioni si riduce in risposta, senza alcun modello esplicito di volatilità o logica di rilevamento dei regimi.

Questa è quantificazione dell'incertezza come segnale di prima classe, non come ripensamento.

Dimensionamento delle posizioni con intervalli conformal

Mappatura di un intervallo di incertezza calibrato sulla dimensione della posizione: un intervallo stretto guida una posizione grande, uno ampio una posizione piccola

Ora colleghiamo la conformal prediction al dimensionamento concreto delle posizioni. La variabile chiave è la semi-larghezza dell'intervallo di previsione relativa al caso simmetrico del residuo assoluto. Con l'intervallo simmetrico [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t], la larghezza completa è wt=2q^tw_t = 2\hat{q}_t. Per mantenere coerenti formule e codice, misuriamo tutto rispetto alla larghezza completa wtw_t in tutto l'articolo.

Dimensionamento inverso alla larghezza

L'approccio più semplice: dimensionare in modo inversamente proporzionale alla larghezza dell'intervallo.

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

dove kk è una costante di scala calibrata sul tuo budget di rischio. Quando il modello è fiducioso (intervallo stretto), assumi una posizione più grande. Quando è incerto (intervallo ampio), ne assumi una più piccola.

Questo è analogo al volatility targeting (size1/σ\text{size} \propto 1/\sigma), ma con una differenza cruciale: la larghezza dell'intervallo conformal è una misura di incertezza distribution-free, non una stima di volatilità parametrica. Cattura l'incertezza predittiva sotto la garanzia di copertura, non solo la varianza dei rendimenti.

Dimensionamento per edge ratio e filtro no-trade

Il puro dimensionamento inverso alla larghezza ignora la forza del segnale stesso. Un raffinamento naturale scala in base all'edge ratio, cioè la previsione puntuale relativa alla larghezza dell'intervallo:

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

Questo è un analogo conformal di un rapporto segnale-rumore: rendimento atteso diviso per una misura di incertezza distribution-free. Lo usiamo sia per il dimensionamento sia per un filtro no-trade.

Il filtro è ben fondato. Se l'intervallo attraversa lo zero,

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

allora l'intervallo di previsione (1α)(1-\alpha) include sia rendimenti positivi sia negativi: il rendimento realizzato può plausibilmente avere segno opposto alla tua previsione. Definisci una soglia minima di edge θ\theta e opera solo quando et>θe_t > \theta. Nota che il contenuto geometrico di "ete_t abbastanza grande da far sì che l'intervallo non attraversi più lo zero" è esattamente et>1/2e_t > 1/2 (poiché l'intervallo supera lo zero quando μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2). Scegli θ\theta sulla scala effettiva di et=μ^/wte_t = |\hat{\mu}|/w_t tramite backtesting; per i residui dei rendimenti giornalieri ete_t è di solito ben al di sotto di 1/21/2, quindi un θ\theta minuscolo può ammettere quasi tutte le operazioni e uno grande può non ammetterne nessuna. Calibralo sui tuoi dati.

Sul "Conformal Kelly"

È allettante innestare gli intervalli conformal sulla frazione di Kelly f=pb(1p)bf^* = \frac{pb - (1-p)}{b}. Ma ff^* è già una frazione completa e limitata, derivata da una probabilità di vincita pp e da un rapporto di payoff bb; moltiplicarla per un rapporto non limitato come μ^/q^\hat{\mu}/\hat{q} non ha alcuna giustificazione dal punto di vista della teoria delle decisioni: può superare 1 o cambiare segno indipendentemente da ff^*, e conteggia due volte l'edge che ff^* già codifica. Per questo non presentiamo un moltiplicatore "conformal Kelly".

Se vuoi guidare Kelly a partire dall'intervallo, devi effettivamente derivare pp e bb da esso, il che richiede un'assunzione esplicita sulla distribuzione all'interno dell'intervallo (gli intervalli conformal deliberatamente non dicono nulla a riguardo — vedi Limitazioni). Per esempio, sotto una forma assunta all'interno dell'intervallo puoi approssimare pP(rendimento>0)p \approx \mathbb{P}(\text{rendimento} > 0) e un rapporto di payoff dalla geometria dell'intervallo, ma segnala chiaramente quell'assunzione, perché reintroduce esattamente l'impegno parametrico che la conformal prediction doveva evitare.

L'alternativa onesta e con poche assunzioni è usare l'edge ratio et=μ^/wte_t = |\hat{\mu}|/w_t come shrinkage Kelly frazionario: aumenta la dimensione quando il rendimento atteso è grande rispetto all'intervallo, riducila quando è piccolo, e applica questo sopra un tetto rigido, esplicitamente come euristica, non come "la frazione di Kelly".

Implementazione in Python

Ecco un'implementazione pratica. Mostriamo sia il percorso split/prefit sia il percorso temporale (EnbPI), dato che il punto centrale di questo articolo è che i dati finanziari non sono scambiabili.

Setup e preparazione dei dati

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 con MAPIE (prefit)

Per la conformal split/prefit, cv="prefit" richiede method="base" (lo stimatore split-conformal naive). L'opzione method="plus" è lo stimatore CV+/jackknife+ ed è incompatibile con cv="prefit": richiede invece un oggetto di cross-validation. Qui usiamo la combinazione corretta e standardizziamo il dimensionamento sulla larghezza completa.

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 per serie temporali con EnbPI

Poiché i rendimenti non sono scambiabili, la suddivisione casuale/prefit qui sopra è solo una baseline. Il MapieTimeSeriesRegressor di MAPIE con method="enbpi" (Xu & Xie, 2021) usa il block bootstrap e l'aggiornamento dei residui, progettati per la dipendenza temporale. Questo è lo strumento che corrisponde all'argomento stesso dell'articolo.

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)

Adaptive conformal inference (online)

Per il trading dal vivo implementiamo il vero ACI da zero: manteniamo il livello di mancata copertura αt\alpha_t, lo aggiorniamo in modo additivo e rideriviamo il quantile dall'insieme dei residui a ogni passo. Contano due dettagli a campione finito:

  • Usa la statistica d'ordine, non un quantile interpolato. np.quantile interpola per impostazione predefinita, il che può cadere appena sotto il rango richiesto e sotto-coprire; passa method="higher" (equivalente a "inverted_cdf").
  • Quando il rango richiesto supera nn (nn piccolo, copertura target alta), la soglia corretta è ++\infty (intervallo = tutta la retta), non un clamp al residuo più grande. Il clamping rompe silenziosamente la garanzia 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))

Mettere tutto insieme: il loop di 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

Valutare i risultati

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

Considerazioni pratiche

Scelta del punteggio di nonconformità

Il residuo assoluto Yμ^(X)|Y - \hat{\mu}(X)| è quello predefinito, ma assume che l'intervallo di previsione debba essere simmetrico attorno alla previsione puntuale. Per i rendimenti finanziari, gli intervalli asimmetrici spesso hanno più senso:

  • Conformalized Quantile Regression (CQR): addestra regressori quantilici ai livelli α/2\alpha/2 e 1α/21 - \alpha/2, poi conformalizza (Romano et al., 2019). Gli intervalli adattano la loro forma alla distribuzione locale: più ampi al ribasso durante i drawdown, più ampi al rialzo durante i rally. (Con CQR l'intervallo non è più simmetrico, quindi wtw_t è l'autentica larghezza upper-meno-lower — continua a usare wtw_t come denominatore ovunque.)
  • Punteggi normalizzati: Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i), dove σ^\hat{\sigma} è una stima di volatilità locale. Questo produce intervalli condizionatamente adattivi — più stretti durante i regimi a bassa volatilità, più ampi durante quelli ad alta volatilità — mantenendo la copertura marginale.

Dimensione del calibration set

La garanzia di copertura della split conformal vale per qualsiasi dimensione del calibration set nn, ma la larghezza dell'intervallo diminuisce all'aumentare di nn. Per nn molto piccolo la statistica d'ordine richiesta potrebbe non esistere, nel qual caso la soglia onesta è ++\infty (un intervallo non informativo ma valido): fare clamp al residuo più grande annulla silenziosamente la garanzia. Per il trading pratico:

  • n100n \geq 100 punti di calibrazione danno intervalli ragionevolmente stretti.
  • n500n \geq 500 è preferibile per una stima stabile dei quantili.
  • Con ACI, il calibration set serve solo per l'inizializzazione; gli aggiornamenti online del livello fanno il resto.

Frequenza di riaddestramento

Il modello base μ^\hat{\mu} può diventare obsoleto. Due approcci:

  1. Riaddestrare periodicamente (per esempio mensilmente) e ricalibrare il quantile conformal.
  2. Usare ACI e lasciare che il livello adattivo compensi l'obsolescenza del modello: gli intervalli si allargano automaticamente man mano che i residui del modello crescono.

L'opzione 2 è più semplice e sorprendentemente efficace. Lo strato conformal funge da rete di sicurezza: anche se il modello degrada, la frequenza di copertura ACI sul lungo periodo converge comunque al target.

Costi di transazione

Gli intervalli conformal interagiscono con i costi di transazione in modo utile. Quando gli intervalli sono ampi (alta incertezza), le posizioni sono piccole, quindi il turnover è basso. Quando gli intervalli si restringono (il modello è fiducioso), le posizioni crescono, ma il modello ha più probabilità di avere ragione, quindi vale la pena pagare per quel turnover.

Puoi anche incorporare i costi di transazione direttamente nel filtro no-trade:

opera solo se μ^(Xt)cost>θwt\text{opera solo se } |\hat{\mu}(X_t)| - \text{cost} > \theta \cdot w_t

Questo assicura che tu operi solo quando l'edge netto atteso supera una soglia scalata dalla larghezza conformal, usando lo stesso denominatore wtw_t di ovunque.

Confronto con i metodi tradizionali

Proprietà CI gaussiano CI bootstrap PI conformal
Assunzione distribuzionale Errori normali i.i.d. + asintotica Nessuna (scambiabilità)
Garanzia a campione finito No No
Funziona con qualsiasi modello No
Si adatta all'eteroschedasticità No Parzialmente Con CQR / punteggi normalizzati
Gestisce il distribution shift No No Variante ACI / EnbPI
Costo computazionale Basso Medio Split: basso; jackknife+: O(n)O(n) riaddestramenti; full: proibitivo

Il bootstrap è "asintotico" solo nella sua garanzia; assume comunque dati i.i.d./scambiabili e regolarità, quindi la sua casella di assunzione distribuzionale non è "senza assunzioni". E la singola colonna "conformal" nasconde costi molto diversi tra le varianti, cosa che la riga del costo ora esplicita.

Limitazioni

La conformal prediction non è magia. Limitazioni chiave per il trading:

  1. Copertura marginale, non condizionale. La garanzia è che P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha marginalmente, cioè mediata sulla casualità sia di XX sia di YY. Per uno specifico valore di condizionamento X=xX = x, la copertura locale può essere superiore o inferiore a 1α1 - \alpha. La conformalized quantile regression affronta in parte questo aspetto.

  2. La scambiabilità è un requisito reale. Per la split conformal, i dati di calibrazione e il punto di test devono essere scambiabili. I dati finanziari non lo sono. ACI ed EnbPI rilassano questo a una garanzia sul lungo periodo, ma la copertura sul breve periodo può deviare.

  3. La larghezza dell'intervallo non è una densità di probabilità. Un intervallo conformal ti dice dove cadrà YY con probabilità 1α1-\alpha, ma non dice nulla sulla distribuzione all'interno dell'intervallo. Non è un sostituto di una distribuzione predittiva completa, ed è esattamente per questo che derivarne un pp e un bb alla Kelly richiede un'assunzione aggiuntiva ed esplicita.

  4. Spazzatura in ingresso, più ampiezza in uscita. Un cattivo modello base produce intervalli ampi. La conformal prediction garantisce la copertura ma non l'utilità. Se il tuo modello non ha potere predittivo, gli intervalli saranno così ampi che il dimensionatore di posizioni non opererà mai.

Riepilogo

La conformal prediction fornisce un framework rigoroso e distribution-free per la quantificazione dell'incertezza che si inserisce naturalmente nel dimensionamento delle posizioni:

  • Split conformal per la calibrazione statica e offline con copertura a campione finito.
  • Jackknife+ quando i dati di calibrazione sono scarsi e vuoi usare tutte le osservazioni (al costo di nn riaddestramenti).
  • Adaptive conformal inference / EnbPI per il trading online con mercati non stazionari.
  • Dimensionamento delle posizioni tramite la larghezza inversa dell'intervallo e un filtro no-trade basato sull'edge ratio — e, se passi a Kelly, solo dopo aver derivato pp e bb in modo onesto, non tramite un moltiplicatore ingiustificato.

Il vantaggio chiave rispetto alle alternative parametriche: non hai mai bisogno di specificare o validare un'assunzione distribuzionale parametrica. Gli intervalli sono onesti per costruzione (marginalmente, sotto scambiabilità). Per un trader sistematico, questo significa una fonte di rischio di modello in meno — e in un'attività in cui il rischio di modello è esistenziale, questo conta.


Riferimenti:

  • 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.
Disclaimer: le informazioni fornite in questo articolo hanno solo scopo didattico e informativo e non costituiscono consulenza finanziaria, di investimento o di trading. Il trading di criptovalute comporta un rischio significativo di perdita.

Autori

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

Resta un Passo Avanti al Mercato

Iscriviti alla nostra newsletter per approfondimenti esclusivi sul trading con IA, analisi di mercato e aggiornamenti sulla piattaforma.

Rispettiamo la tua privacy. Annulla l'iscrizione in qualsiasi momento.