Conformal Prediction voor Risicobewuste Positiegrootte
Elke formule voor positiegrootte heeft een schatting van onzekerheid nodig. Het Kelly-criterium heeft een winkans en een uitbetalingsratio nodig (zie Het Kelly-criterium voor strategieën). Mean-variance optimalisatie heeft een covariantiematrix nodig. VaR heeft een rendementsverdeling nodig. Al deze methoden vereisen aannames over het datagenererende proces — aannames die financiële markten routinematig schenden.
Conformal prediction biedt iets anders: predictie-intervallen met eindige-steekproefdekkingsgaranties, zonder enige parametrische verdelingsaanname. Als je vraagt om 90% dekking, krijg je ten minste 90% dekking — ongeacht of rendementen Gaussisch, dikstaartig, scheef of heteroskedastisch zijn. De enige vereiste is uitwisselbaarheid (exchangeability), of zwakkere voorwaarden, zoals we zullen zien.
Dit artikel behandelt de theorie, de belangrijkste varianten en een praktische implementatie voor positiegrootte in Python.
Het Kernidee: Nonconformity Scores

Conformal prediction werkt door te meten hoe "vreemd" een nieuwe observatie is ten opzichte van historische data. Deze vreemdheid wordt gekwantificeerd door een nonconformity score — elke functie die meet hoe slecht een datapunt overeenkomt met het patroon dat in de rest van de data wordt gezien.
Voor regressie (het voorspellen van een continue waarde zoals rendementen) is de eenvoudigste nonconformity score het absolute residu:
waarbij elke puntvoorspeller is (lineaire regressie, random forest, neuraal netwerk — het maakt niet uit) en een datapunt is.
Het kerninzicht: als de datapunten uitwisselbaar zijn, dan is de rangorde van onder uniform verdeeld over . Dit is een puur combinatorisch feit — het vereist geen aannames over de verdeling van of .
Vanuit deze rangorde-uniformiteit kunnen we predictie-intervallen construeren met eindige-steekproefdekking.
Split Conformal Prediction

Split conformal prediction (Papadopoulos et al., 2002; Lei et al., 2018) is de meest praktische variant. Het algoritme is eenvoudig:
Stap 1. Splits de data in een trainingsset en een kalibratieset .
Stap 2. Fit een willekeurig model op .
Stap 3. Bereken nonconformity scores op de kalibratieset:
Stap 4. Voor een gewenst miscoverage-niveau , neem als het empirische kwantiel van . Concreet is dit het -de kleinste residu (en wanneer , d.w.z. voor zeer kleine ).
Stap 5. Het predictie-interval voor een nieuw punt is:
De Dekkingsgarantie
Onder uitwisselbaarheid van de kalibratiedata en het nieuwe testpunt:
Dit is een eindige-steekproefgarantie — geen asymptotische benadering. Ze geldt voor elk model , elke verdeling van de data en elke steekproefgrootte . Als een slechte voorspeller is, worden de intervallen simpelweg breder. De dekkingsgarantie blijft gelden.
Er is ook een bovengrens wanneer de scores geen gelijke waarden hebben: , dus de dekking is niet nodeloos conservatief.
Waarom Dit Belangrijk Is Voor Trading
Traditionele predictie-intervallen van bijvoorbeeld een lineaire regressie gaan uit van Gaussische fouten. Een Gaussisch interval gekalibreerd op het gros van de data kan de staarten flink verkeerd inschatten wanneer de werkelijke residuen dikstaartig zijn (bijv. Student- met weinig vrijheidsgraden): de centrale massa is dunner dan Gaussisch, dus een variantie-gematchte Gaussische interval overdekkt nabij het centrum maar onderdekt in de staarten, en een op de staart gefitte versie doet het omgekeerde. Het punt is niet één magisch getal — het is dat de gerealiseerde dekking van een parametrisch interval afhangt van een verdelingsaanname die je niet hebt geverifieerd.
Conformal predictie-intervallen omzeilen dit. Ze verbreden automatisch wanneer het model onzeker is, en ze behouden marginale dekking ongeacht de werkelijke foutverdeling. Voor een trader betekent dit:
- Als je posities omgekeerd evenredig met de intervalbreedte bepaalt, verminder je automatisch je blootstelling wanneer het model onzeker is.
- De dekkingsgarantie betekent dat je risicoschattingen eerlijk zijn — als je zegt "90% van de gerealiseerde rendementen valt binnen dit interval", dan is die uitspraak statistisch geldig (marginaal, onder uitwisselbaarheid).
Full Conformal en Jackknife+
Split conformal is eenvoudig maar verspilt data: de kalibratieset kan niet voor training worden gebruikt. Twee alternatieven pakken dit aan.
Full Conformal Prediction
Full conformal prediction (Vovk et al., 2005) gebruikt alle data zowel voor training als kalibratie. Voor elke kandidaatwaarde van :
- Voeg toe aan de dataset.
- Herfit het model op de uitgebreide dataset.
- Bereken alle nonconformity scores.
- Neem op in de predictieverzameling als de score voor niet te extreem is.
De predictieverzameling is:
waarbij de nonconformity scores zijn berekend met de uitgebreide dataset.
Full conformal levert de smalste intervallen, maar is voor de meeste modellen rekenkundig onbetaalbaar — je moet het model herfitten voor elke kandidaat op een raster. Voor een rendementsvoorspelling kan dit duizenden herfits per predictie betekenen.
Jackknife+ (Barber et al., 2021)
De jackknife+ vindt een balans. Het gebruikt leave-one-out (LOO) residuen maar houdt rekening met de variabiliteit van het gefitte model over LOO-folds.
Laat het model aanduiden dat getraind is op alle data behalve punt . Definieer de LOO nonconformity score met het enkele absolute residu:
Het jackknife+ predictie-interval wordt dan opgebouwd uit de LOO-voorspellingen op het testpunt, verbreed door deze residuen:
Hier duidt de -de kleinste waarde van de verzameling aan, en de -de kleinste waarde. De ondergrens trekt het residu af van elke LOO-voorspelling; de bovengrens telt het op. Die asymmetrie is precies het punt — beide grenzen samenvoegen tot zou de ondergrens boven de voorspelling plaatsen, wat fout is.
De jackknife+ biedt een dekkingsgarantie van ten minste (iets zwakker dan de van split conformal), maar gebruikt alle data voor zowel training als kalibratie. In de praktijk ligt de dekking meestal dicht bij .
Voor tradingmodellen getraind op beperkte data (bijv. regime-specifieke modellen met slechts enkele honderden observaties) is de jackknife+ vaak de beste keuze — het offert geen schaarse data op voor kalibratie. De kosten zijn modelherfits.
Het Probleem Met Financiële Tijdreeksen: Non-Exchangeability

De standaard conformal garantie vereist uitwisselbaarheid: de gezamenlijke verdeling van is invariant onder permutaties. Voor i.i.d.-data geldt dit triviaal.
Financiële tijdreeksen zijn niet uitwisselbaar. Rendementen vertonen:
- Volatiliteitsclustering: Periodes met hoge volatiliteit volgen op periodes met hoge volatiliteit (GARCH-effecten).
- Momentum en mean reversion: Autocorrelatie in rendementen of gekwadrateerde rendementen.
- Regimeveranderingen: De verdeling verschuift in de tijd (bull- vs. bearmarkten).
Als je split conformal naïef toepast op een tijdreeks — met een willekeurige kalibratiesplitsing — schend je de temporele structuur. De kalibratiescores van een rustig 2017 zullen de onzekerheid van een volatiel 2020 niet weerspiegelen. Je dekkingsgarantie breekt.
Adaptive Conformal Inference (ACI)

Gibbs en Candes (2021, NeurIPS) introduceerden Adaptive Conformal Inference (ACI) om distributieverschuiving en niet-uitwisselbare data aan te pakken. Het idee is elegant: in plaats van een vast dekkingsniveau te gebruiken, past het het doel-miscoverage-niveau online aan op basis van of recente intervallen de werkelijke uitkomst dekten, en het kwantiel wordt bij elke stap opnieuw afgeleid uit de scoreverdeling.
Het ACI-Algoritme
ACI stelt de intervalbreedte niet direct bij. Het houdt een adaptieve miscoverage-parameter bij en herberekent het conformal kwantiel daaruit. Op elke tijdstap :
- Bereken de conformal drempel als het empirische -kwantiel van de huidige residuenverzameling (de kalibratiescores, plus eventuele tot dusver gerealiseerde scores): .
- Observeer kenmerken , produceer het interval .
- Observeer de werkelijke waarde en bereken de foutindicator .
- Werk het niveau bij (niet de breedte):
waarbij een stapgrootte is en de doel-miscoverage. Als een interval miste (), krimpt , wat het volgende kwantiel omhoog duwt en het interval verbreedt; als het dekte, groeit en verkleinen de intervallen. Cruciaal is dat hier in kansmaateenheden is — het duwt een niveau in , niet de drempel in ruwe rendementseenheden — zodat dezelfde zich verstandig gedraagt of residuen nu van de orde zijn of niet.
Dekkingsgarantie Voor ACI
ACI biedt een langetermijn-dekkingsgarantie die niet afhangt van een verdelingsmodel:
Omdat geclipt wordt tot , is de teller begrensd door maal een constante, dus de rechterkant is en de empirische miscoverage-frequentie convergeert naar . De precieze uitspraak: ACI garandeert dat de langetermijn-empirische miscoverage-frequentie convergeert naar voor willekeurige (inclusief adversariële) reeksen, mits de aangepaste niveaus begrensd blijven — wat de clip afdwingt. Het is een garantie over dekkingsfrequentie, niet over intervalinformativiteit: onder een echt adversariële reeks kunnen de intervallen oninformatief breed worden en toch het dekkingsdoel halen.
Dynamically-Tuned ACI (DtACI)
Gibbs en Candes (2024, JMLR) verfijnden ACI met dynamische afstemming van de stapgrootte . In plaats van vast te zetten, houden ze een kandidatenset bij en combineren ze deze via een expert-aggregatieregel, waarbij de wordt bevoordeeld wiens recente dekking het dichtst bij het doel ligt.
Dit pakt een praktisch probleem aan: een grote past zich snel aan regimeveranderingen aan maar produceert volatiele intervalbreedtes; een kleine is stabiel maar past zich langzaam aan. DtACI weegt deze automatisch af.
Waarom Dit Belangrijk Is Voor Trading
Bekijk een market-making-strategie die een rendementsvoorspellingsmodel gebruikt. Tijdens rustige markten zijn de conformal intervallen smal — het model is zeker, en je kunt grotere posities innemen. Wanneer de volatiliteit piekt (winstseizoen, FOMC-aankondigingen, geopolitieke schokken), past het ACI-niveau zich aan en verbreden de intervallen binnen een paar tijdstappen. Je positiegrootte krimpt als reactie, zonder enig expliciet volatiliteitsmodel of regime-detectielogica.
Dit is onzekerheidskwantificering als volwaardig signaal, niet als bijzaak.
Positiegrootte Met Conformal Intervallen

Laten we nu conformal prediction verbinden met concrete positiegrootte. De sleutelvariabele is de halve breedte van het predictie-interval ten opzichte van het symmetrische absolute-residu-geval. Met het symmetrische interval , is de volledige breedte . Om formules en code consistent te houden, meten we alles overal tegen de volledige breedte .
Inverse-Breedte Sizing
De eenvoudigste aanpak: bepaal de grootte omgekeerd evenredig met de intervalbreedte.
waarbij een schaalconstante is gekalibreerd op je risicobudget. Wanneer het model zeker is (smal interval), neem je een grotere positie. Wanneer onzeker (breed interval), neem je een kleinere.
Dit is analoog aan volatility targeting (), maar met een cruciaal verschil: de conformal intervalbreedte is een distributievrije onzekerheidsmaat, geen parametrische volatiliteitsschatting. Het vangt predictieve onzekerheid onder de dekkingsgarantie, niet slechts rendementsvariantie.
Edge-Ratio Sizing En Het No-Trade Filter
Zuivere inverse-breedte sizing negeert de sterkte van het signaal zelf. Een natuurlijke verfijning schaalt met de edge ratio — de puntvoorspelling ten opzichte van de intervalbreedte:
Dit is een conformal analogon van een signaal-ruisverhouding: verwacht rendement gedeeld door een distributievrije onzekerheidsmaat. We gebruiken het zowel voor sizing als voor een no-trade filter.
Het filter is gefundeerd. Als het interval nul overspant,
dan bevat het predictie-interval zowel positieve als negatieve rendementen — het gerealiseerde rendement kan plausibel het tegenovergestelde teken van je voorspelling hebben. Definieer een minimale edge-drempel en handel alleen wanneer . Merk op dat de geometrische inhoud van " groot genoeg zodat het interval nul niet meer overspant" precies is (aangezien het interval nul vrijmaakt wanneer ). Kies op de daadwerkelijke schaal van via backtesting; voor residuen van dagrendementen ligt meestal ver onder , dus een piepklein kan bijna alle trades toelaten en een grote kan er geen enkele toelaten. Kalibreer het op je eigen data.
Over "Conformal Kelly"
Het is verleidelijk om conformal intervallen te koppelen aan de Kelly-fractie . Maar is al een complete, begrensde fractie afgeleid van een winkans en een uitbetalingsratio ; het vermenigvuldigen ervan met een onbegrensde ratio zoals heeft geen besliskundige rechtvaardiging — het kan groter zijn dan 1 of van teken wisselen onafhankelijk van , en het telt de edge die al vastlegt dubbel. Dus presenteren we geen "conformal Kelly"-vermenigvuldiger.
Als je Kelly wilt aansturen vanuit het interval, moet je daadwerkelijk en eruit afleiden, wat een expliciete aanname vereist over de verdeling binnen het interval (conformal intervallen zeggen daar bewust niets over — zie Beperkingen). Bijvoorbeeld, onder een aangenomen vorm binnen het interval kun je benaderen en een uitbetalingsratio uit de intervalgeometrie — maar markeer die aanname luid en duidelijk, want ze herintroduceert precies de parametrische verbintenis die conformal prediction juist wilde vermijden.
Het eerlijke, aanname-lichte alternatief is om de edge ratio te gebruiken als een fractional-Kelly-verkleining: vergroot bij een groot verwacht rendement ten opzichte van het interval, verklein bij een klein verwacht rendement, en pas dit toe bovenop een harde bovengrens — expliciet als een heuristiek, niet als "de Kelly-fractie".
Python-Implementatie
Hier is een praktische implementatie. We tonen zowel het split/prefit-pad als het temporele (EnbPI) pad, aangezien het hele punt van dit artikel is dat financiële data niet uitwisselbaar is.
Setup En Datavoorbereiding
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 Met MAPIE (Prefit)
Voor split/prefit conformal vereist cv="prefit" method="base" (de naïeve split-conformal-schatter). De optie method="plus" is de CV+/jackknife+-schatter en is niet compatibel met cv="prefit" — het heeft in plaats daarvan een cross-validatie-object nodig. We gebruiken hier de juiste combinatie, en standaardiseren sizing op de volledige breedte.
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)
Tijdreeks-Conformal Met EnbPI
Omdat rendementen niet uitwisselbaar zijn, is de willekeurige/prefit-splitsing hierboven slechts een baseline. MAPIE's MapieTimeSeriesRegressor met method="enbpi" (Xu & Xie, 2021) gebruikt block bootstrap en residu-updating ontworpen voor temporele afhankelijkheid. Dit is het gereedschap dat overeenkomt met het eigen argument van dit artikel.
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)
Voor live trading implementeren we echte ACI vanaf de grond af aan: houd het miscoverage-niveau bij, werk het additief bij, en herleid het kwantiel uit de residuenverzameling bij elke stap. Twee eindige-steekproefdetails zijn belangrijk:
- Gebruik de orde-statistiek, geen geïnterpoleerd kwantiel.
np.quantileinterpoleert standaard, wat net onder de vereiste rangorde kan vallen en onderdekking kan veroorzaken; geefmethod="higher"mee (equivalent aan"inverted_cdf"). - Wanneer de vereiste rangorde groter is dan (kleine , hoge doeldekking), is de correcte drempel (interval = de gehele lijn), geen klem op het grootste residu. Klemmen doorbreekt stilzwijgend de -garantie.
class AdaptiveConformalSizer:
"""
Online position sizing with Adaptive Conformal Inference (Gibbs & Candes,
2021). Updates the miscoverage LEVEL alpha_t and re-derives the quantile
from the residual set each step -- gamma is in probability units.
"""
def __init__(self, base_model, alpha=0.1, gamma=0.02,
max_position=1.0, min_edge=0.05, k=1e-3):
self.base_model = base_model
self.alpha_target = alpha # target miscoverage
self.alpha_t = alpha # adaptive miscoverage level
self.gamma = gamma # step size, in [0, 1] units
self.max_position = max_position
self.min_edge = min_edge
self.k = k
self.residuals = []
self.q_hat = np.inf
self.coverage_history = []
@staticmethod
def _conformal_quantile(residuals, alpha_t):
"""(1 - alpha_t) conformal quantile via the order statistic."""
n = len(residuals)
if n == 0:
return np.inf
rank = int(np.ceil((1.0 - alpha_t) * (n + 1)))
if rank > n: # required order statistic does not exist
return np.inf # -> interval is the whole line
level = rank / n
return float(np.quantile(residuals, min(level, 1.0), method="higher"))
def calibrate(self, X_cal, y_cal):
preds = self.base_model.predict(X_cal)
self.residuals = list(np.abs(np.asarray(y_cal) - preds))
self.q_hat = self._conformal_quantile(self.residuals, self.alpha_t)
def predict_and_size(self, X_t) -> dict:
mu_hat = self.base_model.predict(np.asarray(X_t).reshape(1, -1))[0]
lower, upper = mu_hat - self.q_hat, mu_hat + self.q_hat
width = upper - lower # = 2 * q_hat
edge_ratio = abs(mu_hat) / width if np.isfinite(width) and width > 0 else 0.0
if edge_ratio < self.min_edge:
size = 0.0
else:
size = min(self.k / width, self.max_position) if width > 0 else 0.0
return {
"prediction": mu_hat, "lower": lower, "upper": upper,
"width": width, "edge_ratio": edge_ratio,
"position_size": size * np.sign(mu_hat),
"alpha_t": self.alpha_t, "q_hat": self.q_hat,
}
def update(self, X_t, y_t: float):
"""ACI update: adapt the LEVEL, then re-derive the quantile."""
mu_hat = self.base_model.predict(np.asarray(X_t).reshape(1, -1))[0]
residual = abs(y_t - mu_hat)
covered = int(residual <= self.q_hat)
err_t = 1 - covered
self.alpha_t = float(np.clip(
self.alpha_t + self.gamma * (self.alpha_target - err_t), 0.0, 1.0
))
self.residuals.append(residual)
self.q_hat = self._conformal_quantile(self.residuals, self.alpha_t)
self.coverage_history.append(covered)
@property
def running_coverage(self) -> float:
if not self.coverage_history:
return float("nan")
return float(np.mean(self.coverage_history))
Alles Samenbrengen: Backtest-Lus
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
De Resultaten Evalueren
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}")
Praktische Overwegingen
De Nonconformity Score Kiezen
Het absolute residu is de standaard, maar het gaat ervan uit dat het predictie-interval symmetrisch moet zijn rond de puntvoorspelling. Voor financiële rendementen zijn asymmetrische intervallen vaak zinvoller:
- Conformalized Quantile Regression (CQR): Fit kwantielregressoren op niveaus en , en conformaliseer vervolgens (Romano et al., 2019). De intervallen passen hun vorm aan de lokale verdeling aan — breder aan de onderkant tijdens drawdowns, breder aan de bovenkant tijdens rally's. (Met CQR is het interval niet langer symmetrisch, dus is de echte boven-min-onder-breedte — blijf overal als noemer gebruiken.)
- Genormaliseerde scores: , waarbij een lokale volatiliteitsschatting is. Dit produceert intervallen die conditioneel adaptief zijn — smaller tijdens laagvolatiele regimes, breder tijdens hoogvolatiele — terwijl marginale dekking behouden blijft.
Grootte Van De Kalibratieset
De dekkingsgarantie van split conformal geldt voor elke kalibratiesetgrootte , maar de intervalbreedte neemt af naarmate toeneemt. Voor zeer kleine bestaat de vereiste orde-statistiek mogelijk niet, in welk geval de eerlijke drempel is (een oninformatief maar geldig interval) — klemmen op het grootste residu maakt de garantie stilzwijgend ongeldig. Voor praktische trading:
- kalibratiepunten geeft redelijk smalle intervallen.
- heeft de voorkeur voor stabiele kwantielschatting.
- Met ACI is de kalibratieset alleen voor initialisatie; de online niveau-updates doen de rest.
Frequentie Van Herscholing
Het basismodel kan verouderen. Twee benaderingen:
- Periodiek herscholen (bijv. maandelijks) en het conformal kwantiel opnieuw kalibreren.
- ACI gebruiken en het adaptieve niveau laten compenseren voor modelveroudering — de intervallen verbreden automatisch naarmate de residuen van het model groeien.
Optie 2 is eenvoudiger en verrassend effectief. De conformal laag fungeert als vangnet: zelfs als het model verslechtert, convergeert de langetermijn-ACI-dekkingsfrequentie nog steeds naar het doel.
Transactiekosten
Conformal intervallen interacteren op een nuttige manier met transactiekosten. Wanneer intervallen breed zijn (hoge onzekerheid), zijn posities klein, dus de turnover is laag. Wanneer intervallen smaller worden (het model is zeker), groeien de posities — maar het model heeft dan een grotere kans om gelijk te hebben, dus de turnover is de moeite waard.
Je kunt transactiekosten ook direct in het no-trade filter opnemen:
Dit zorgt ervoor dat je alleen handelt wanneer de verwachte netto-edge een drempel overschrijdt geschaald met de conformal breedte — met dezelfde -noemer als overal elders.
Vergelijking Met Traditionele Methoden
| Eigenschap | Gaussisch BI | Bootstrap BI | Conformal PI |
|---|---|---|---|
| Verdelingsaanname | Normale fouten | i.i.d. + asymptotisch | Geen (uitwisselbaarheid) |
| Eindige-steekproefgarantie | Nee | Nee | Ja |
| Werkt met elk model | Nee | Ja | Ja |
| Past zich aan heteroskedasticiteit aan | Nee | Gedeeltelijk | Met CQR / genormaliseerde scores |
| Behandelt distributieverschuiving | Nee | Nee | ACI / EnbPI-variant |
| Rekenkosten | Laag | Gemiddeld | Split: laag; jackknife+: herfits; full: onbetaalbaar |
De bootstrap is alleen "asymptotisch" in zijn garantie; hij veronderstelt nog steeds i.i.d./uitwisselbare data en gladheid, dus zijn cel voor verdelingsaanname is niet "aannamevrij". En de enkele "conformal"-kolom verbergt zeer verschillende kosten tussen varianten, wat de kostenrij nu uitspelt.
Beperkingen
Conformal prediction is geen magie. Belangrijke beperkingen voor trading:
-
Marginale, geen conditionele dekking. De garantie is dat marginaal — gemiddeld over de willekeur in zowel als . Voor een specifieke conditioneringswaarde kan de lokale dekking boven of onder liggen. Conformalized quantile regression pakt dit gedeeltelijk aan.
-
Uitwisselbaarheid is een echte vereiste. Voor split conformal moeten de kalibratiedata en het testpunt uitwisselbaar zijn. Financiële data is dat niet. ACI en EnbPI verzwakken dit tot een langetermijngarantie, maar korte-termijndekking kan afwijken.
-
Intervalbreedte is geen kansdichtheid. Een conformal interval vertelt je waar zal vallen met kans , maar zegt niets over de verdeling binnen het interval. Het is geen vervanging voor een volledige predictieve verdeling — precies waarom het afleiden van een Kelly en eruit een extra, expliciete aanname vereist.
-
Rommel erin, breder eruit. Een slecht basismodel produceert brede intervallen. Conformal prediction garandeert dekking maar niet bruikbaarheid. Als je model geen voorspellende kracht heeft, worden de intervallen zo breed dat de positiegrootte-bepaler nooit handelt.
Samenvatting
Conformal prediction biedt een rigoureus, distributievrij raamwerk voor onzekerheidskwantificering dat natuurlijk past bij positiegrootte:
- Split conformal voor statische, offline kalibratie met eindige-steekproefdekking.
- Jackknife+ wanneer kalibratiedata schaars is en je alle observaties wilt gebruiken (tegen de kosten van herfits).
- Adaptive conformal inference / EnbPI voor online trading met niet-stationaire markten.
- Positiegrootte via inverse intervalbreedte en een edge-ratio-no-trade-filter — en, als je naar Kelly gaat, pas nadat je en eerlijk hebt afgeleid, niet via een ongerechtvaardigde vermenigvuldiger.
Het belangrijkste voordeel ten opzichte van parametrische alternatieven: je hoeft nooit een parametrische verdelingsaanname te specificeren of te valideren. De intervallen zijn per constructie eerlijk (marginaal, onder uitwisselbaarheid). Voor een systematische trader betekent dit één bron van modelrisico minder — en in een business waar modelrisico existentieel is, telt dat.
Referenties:
- 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.
Auteurs
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.