Conformal Prediction per il dimensionamento delle posizioni consapevole del rischio
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à

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:
dove è un qualsiasi predittore puntuale (regressione lineare, random forest, rete neurale: non importa) e è un punto dato.
L'intuizione chiave: se i punti dati sono scambiabili, allora il rango di tra è distribuito uniformemente su . Questo è un fatto puramente combinatorio: non richiede alcuna assunzione sulla distribuzione di o .
Da questa uniformità del rango possiamo costruire intervalli di previsione con copertura a campione finito.
Split conformal prediction

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 e un calibration set .
Passo 2. Addestra un qualsiasi modello su .
Passo 3. Calcola i punteggi di nonconformità sul calibration set:
Passo 4. Per un livello di mancata copertura desiderato , prendi come il quantile empirico di . Concretamente, questo è il -esimo residuo più piccolo (e ogni volta che , cioè per molto piccoli).
Passo 5. L'intervallo di previsione per un nuovo punto è:
La garanzia di copertura
Sotto scambiabilità dei dati di calibrazione e del nuovo punto di test:
Questa è una garanzia a campione finito, non un'approssimazione asintotica. Vale per qualsiasi modello , qualsiasi distribuzione dei dati e qualsiasi dimensione campionaria . Se è 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: , 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 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 di :
- Aumenta il dataset con .
- Riaddestra il modello sul dataset aumentato.
- Calcola tutti i punteggi di nonconformità.
- Includi nel set di previsione se il punteggio per non è troppo estremo.
Il set di previsione è:
dove gli 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 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 il modello addestrato su tutti i dati eccetto il punto . Definisci il punteggio di nonconformità LOO con il singolo residuo assoluto:
L'intervallo di previsione jackknife+ viene quindi costruito a partire dalle previsioni LOO nel punto di test, allargate da questi residui:
Qui denota il -esimo valore più piccolo dell'insieme , e il -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 porterebbe il limite inferiore sopra la previsione, il che è sbagliato.
Il jackknife+ fornisce una garanzia di copertura di almeno (leggermente più debole di della split conformal), ma usa tutti i dati sia per il training sia per la calibrazione. In pratica la copertura è tipicamente vicina a .
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 riaddestramenti del modello.
Il problema con le serie temporali finanziarie: la non-scambiabilità

La garanzia conformal standard richiede la scambiabilità: la distribuzione congiunta di è 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)

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 e ricalcola da esso il quantile conformal. A ogni passo temporale :
- Calcola la soglia conformal come il quantile empirico dell'insieme corrente di residui (i punteggi di calibrazione, più eventuali punteggi realizzati finora): .
- Osserva le feature , produci l'intervallo .
- Osserva il valore vero e calcola l'indicatore di errore .
- Aggiorna il livello (non la larghezza):
dove è uno step size e è la mancata copertura target. Se un intervallo ha mancato (), si riduce, il che spinge il quantile successivo più in alto e allarga l'intervallo; se ha coperto, cresce e gli intervalli si restringono. Fondamentale: qui è in unità di probabilità — modifica un livello in , non la soglia in unità grezze di rendimento — quindi lo stesso si comporta in modo sensato sia che i residui siano dell'ordine di 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:
Poiché è limitato a , il numeratore è limitato da moltiplicato per una costante, quindi il membro di destra è e la frequenza empirica di mancata copertura converge ad . L'enunciato preciso: ACI garantisce che la frequenza empirica di mancata copertura sul lungo periodo converga ad 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 . Invece di fissare , mantengono un insieme di candidati e li combinano tramite una regola di aggregazione di esperti, favorendo il la cui copertura recente è più vicina al target.
Questo affronta un problema pratico: un grande si adatta rapidamente ai cambi di regime ma produce larghezze di intervallo volatili; un 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

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 , la larghezza completa è . Per mantenere coerenti formule e codice, misuriamo tutto rispetto alla larghezza completa in tutto l'articolo.
Dimensionamento inverso alla larghezza
L'approccio più semplice: dimensionare in modo inversamente proporzionale alla larghezza dell'intervallo.
dove è 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 (), 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:
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,
allora l'intervallo di previsione include sia rendimenti positivi sia negativi: il rendimento realizzato può plausibilmente avere segno opposto alla tua previsione. Definisci una soglia minima di edge e opera solo quando . Nota che il contenuto geometrico di " abbastanza grande da far sì che l'intervallo non attraversi più lo zero" è esattamente (poiché l'intervallo supera lo zero quando ). Scegli sulla scala effettiva di tramite backtesting; per i residui dei rendimenti giornalieri è di solito ben al di sotto di , quindi un 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 . Ma è già una frazione completa e limitata, derivata da una probabilità di vincita e da un rapporto di payoff ; moltiplicarla per un rapporto non limitato come non ha alcuna giustificazione dal punto di vista della teoria delle decisioni: può superare 1 o cambiare segno indipendentemente da , e conteggia due volte l'edge che già codifica. Per questo non presentiamo un moltiplicatore "conformal Kelly".
Se vuoi guidare Kelly a partire dall'intervallo, devi effettivamente derivare e 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 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 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 , 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.quantileinterpola per impostazione predefinita, il che può cadere appena sotto il rango richiesto e sotto-coprire; passamethod="higher"(equivalente a"inverted_cdf"). - Quando il rango richiesto supera ( piccolo, copertura target alta), la soglia corretta è (intervallo = tutta la retta), non un clamp al residuo più grande. Il clamping rompe silenziosamente la garanzia .
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 è 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 e , 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 è l'autentica larghezza upper-meno-lower — continua a usare come denominatore ovunque.)
- Punteggi normalizzati: , dove è 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 , ma la larghezza dell'intervallo diminuisce all'aumentare di . Per molto piccolo la statistica d'ordine richiesta potrebbe non esistere, nel qual caso la soglia onesta è (un intervallo non informativo ma valido): fare clamp al residuo più grande annulla silenziosamente la garanzia. Per il trading pratico:
- punti di calibrazione danno intervalli ragionevolmente stretti.
- è 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 può diventare obsoleto. Due approcci:
- Riaddestrare periodicamente (per esempio mensilmente) e ricalibrare il quantile conformal.
- 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:
Questo assicura che tu operi solo quando l'edge netto atteso supera una soglia scalata dalla larghezza conformal, usando lo stesso denominatore 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 | Sì |
| Funziona con qualsiasi modello | No | Sì | Sì |
| 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+: 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:
-
Copertura marginale, non condizionale. La garanzia è che marginalmente, cioè mediata sulla casualità sia di sia di . Per uno specifico valore di condizionamento , la copertura locale può essere superiore o inferiore a . La conformalized quantile regression affronta in parte questo aspetto.
-
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.
-
La larghezza dell'intervallo non è una densità di probabilità. Un intervallo conformal ti dice dove cadrà con probabilità , 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 e un alla Kelly richiede un'assunzione aggiuntiva ed esplicita.
-
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 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 e 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.
Autori
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.